Improving Move Comprehensive(3): Integration of MoveVM and Blockchain

verse2
verse2
Published in
9 min readJun 25, 2024

Integration of MoveVM and Blockchain: Key takeaways

  • Move is a stack machine that provides a black box, secure execution environment.
  • Aptos is utilizing it as a smart contract platform through AptosVM, which interacts with the black box, MoveVM.

Intro
VMs are a narrative. It’s an easy and effective go-to-market (GTM) strategy to try and gain market traction by proposing a new chain, a new VM, but it’s nearly impossible to create a new VM and integrate it with a blockchain without solid technical skills. Projects proposing new VMs as a simple branding strategy have been forgotten over time, and only time-tested projects have survived.

Comparison to VMs

Although Move is a language specialized for smart contracts, MoveVM, which runs the Move language, is actually a separate program from the blockchain. MoveVM is a program that simply stores certain modules in bytecode, and when input comes in, executes the module and returns the output, regardless of the logic behind block creation, consensus, transaction processing, etc. To make it easier to understand, here’s an illustration of how a contract is deployed and executed on a basic VM.

Smart Contract Execution Flow
  1. A developer develops a smart contract in a language such as Solidity (EVM) or Rust (WasmVM, SolVM).
  2. A compilation program (such as solc) is used to turn the contract code into bytecode. The bytecode is a collection of instructions that contain the logic of the contract.
  3. Deploy the contract to the blockchain. The deployed contract is stored on the blockchain.
  4. The contract user invokes the contract using a blockchain client (such as Geth). A contract can be invoked by including the contract information and input values to be invoked in a transaction.
  5. If the transaction is executed normally, that is, if the smart contract is executed normally, the result is returned and stored in the state of the blockchain.

Each VM has a slightly different way of storing and executing contracts, but the flow is generally the same as above. In this article, we’ll take a look at the execution flow of MoveVM and how it works in conjunction with a blockchain like Aptos.

#1. Interact with MoveVM

First of all, Move programs do not access system memory during runtime and run exclusively within the virtual machine, operating as a black box, unaffected by the external environment outside of MoveVM. Additionally, Move programs run in a stack. The contract execution stack contains the following elements

  • Call Stack

The call stack contains all the contextual information needed to run a procedure, and each instruction is encoded to reduce code size. When a procedure executes a Call instruction that calls another procedure, a new call stack object is created.

Move Call Stack Architecture

MoveVM separates data storage from callstack (process logic) storage. For example, to implement ERC20 tokens in EVM, developers need to write logic and store each user’s state in a contract, whereas in MoveVM, user state (the resource under the account address) is stored independently, and program calls must comply with permissions and required rules for the resource. This sacrifices some flexibility, but greatly improves security and execution efficiency.

  • Memory and Global Storage

Memory stores temporary execution data and variables. It is the first-order store in the heap structure, which is the area where pointers cannot be stored. Global variables contain variable information that is accessible between different functions within the same context. They store pointers to memory cells and can be indexed for fast value recall.

To access a global variable, you must provide an address and the type mapped to that address. This is why they are called struct tags in Move contract development, and you should provide a type that looks like 0x1::{MODULE_NAME}::{RESOURCE_NAME}. This separation allows Move to simplify the internal behavior of the VM and provide a grammatically structured pattern.

  • Operand Stack

The opcode is used to evaluate expressions and store intermediate results during contract execution. The stack is a place to store temporary computed values or data needed for computation during execution. It temporarily stores the input values or results of various operations and plays an important role in managing the computation process as the program executes various instructions.

For example, in a simple operation to add two numbers, the two numbers are first pushed onto the opcode stack, and when the addition instruction is executed, the two numbers are popped off the stack to perform the calculation. The result of the calculation is pushed back onto the stack and can be used as input for the next operation. In this way, it supports all the data flow and processing needed during the execution of a contract. The opcode stack plays a pivotal role in the sequential processing of complex computations, especially in the temporary storage of intermediate results and function call management.

One of the interesting aspects of MoveVM here is the static jump. A static jump means that when a branching instruction is encountered during contract execution, the instruction is moved to a predetermined location based on the branch condition. Static jumps in MoveVM have a predetermined jump offset and do not jump dynamically like EVM. This ensures that dependencies within modules are acyclic, and that modules themselves are not dynamically allocated, enforcing the immutability of function calls and eliminating the possibility of re-entrancy.

These memory management techniques and branching mechanisms allow programs to execute their logic clearly and efficiently, which is especially beneficial in security and efficiency-critical environments like blockchain.

#2. Aptos Core Overview

  1. How Aptos interact with MoveVM?

Aptos is a leading Layer 1 chain that uses MoveVM as its smart contract platform. All smart contracts essentially operate as black boxes that take in user input, called transactions, and output a predetermined result. AptosVM (aka Aptos MoveVM) reads the information contained in each block and executes it in a set order. Within a block, it receives input that contains information from a transaction script and a state view, each of which is briefly described below.

  • Transaction script: A script or snippet of code that a user submits when they want to perform a specific action. This can be anything from changing the state of the blockchain to querying a specific state.
  • State view: A snapshot of data or code from a particular version of the blockchain at a block height or time. The values can only be accessed on a read-only basis and serve as an interface to get the exact state of the blockchain at a particular point in time. An actual state view implementation is as following.
/// `StateView` is a trait that defines a read-only snapshot of the global state. It is passed to
/// the VM for transaction execution, during which the VM is guaranteed to read anything at the
/// given state.
pub trait TStateView {
type Key;

/// For logging and debugging purpose, identifies what this view is for.
fn id(&self) -> StateViewId {
StateViewId::Miscellaneous
}
/// Gets the state value bytes for a given state key.
fn get_state_value_bytes(&self, state_key: &Self::Key) -> Result<Option<Bytes>> {
let val_opt = self.get_state_value(state_key)?;
Ok(val_opt.map(|val| val.bytes().clone()))
}
/// Gets the state value for a given state key.
fn get_state_value(&self, state_key: &Self::Key) -> Result<Option<StateValue>>;
/// Get state storage usage info at epoch ending.
fn get_usage(&self) -> Result<StateStorageUsage>;
fn as_in_memory_state_view(&self) -> InMemoryStateView {
unreachable!("in-memory state view conversion not supported yet")
}
}

All transactions submitted to the Aptos network are processed in three phases, as follows

  • Transaction Prologue: This is where the transaction is validated to ensure that it will behave validly (i.e. not run into an infinite loop or run out of gas) before changing the actual chain state. When the transaction validation is complete, a result is returned as either ‘success’ or ‘failure’, and either result does not change the actual chain state.
  • Transaction Execution: This is the process of changing the actual chain state by executing the transaction against the verified transaction. As a result of the transaction, a value called write_set is generated from the state view that can change the chain state atomically. It is possible to preview the write_set of the transaction through simulation when calling the actual smart contract as shown below.
transaction writeset
  • Transaction Epilogue: Performs a specific action as a result of performing a transaction. For example, debiting the gas required for the transaction from the transaction submitter’s balance.

AptosVM retrieves and processes the information needed to execute a transaction, such as contract code and linked modules, from the state view. It also tracks the results of transaction updates to the chain, ensuring that transactions are processed sequentially and enabling parallel and concurrent processing.

2. Exploring implementations

Aptos has implemented the AptosVM Runtime, which is a complete wrapper implementation of the existing MoveVM, which performs transactions in the three steps mentioned above. To take a closer look at how the AptosVM Runtime is actually implemented, let’s look at the code for the three phases as follows.

First, Prologue takes as an argument the payload, which is the information contained in the transaction. Before executing the transaction, it first checks that the transaction size is valid (check_gas) and performs an action based on the contents of the payload. Typical payloads include a script to execute a Move transaction, an entry function, etc. The transaction utilizes a session as the required storage for the transaction, which will hold the information until the transaction is reflected in the chain.

// aptos-move/aptos-vm/src/aptos_vm.rs
fn run_prologue_with_payload(
&self,
session: &mut SessionExt,
resolver: &impl AptosMoveResolver,
payload: &TransactionPayload,
txn_data: &TransactionMetadata,
log_context: &AdapterLogSchema,
traversal_context: &mut TraversalContext,
) -> Result<(), VMStatus> {
check_gas(
get_or_vm_startup_failure(&self.gas_params, log_context)?,
self.gas_feature_version,
resolver,
txn_data,
self.features(),
log_context,
)?;
match payload {
TransactionPayload::Script(_) | TransactionPayload::EntryFunction(_) => {
transaction_validation::run_script_prologue(
session,
txn_data,
log_context,
traversal_context,
)
},
.....
}
}

Second, after the transaction is validated, it will perform the transaction. It checks whether the assets required to perform the transaction (gas, money for the transaction, etc.) are deposited in the account, and if the value of the payload needs to be checked to interact with the smart contract, it calls the contract to perform the transaction. In the prologue phase, the results from the simulation are checked once more to ensure that they are valid, and if they are, they are updated to the real chain.

// aptos-move/aptos-vm/src/aptos_vm.rs
pub fn execute_user_transaction(
&self,
resolver: &impl AptosMoveResolver,
txn: &SignedTransaction,
log_context: &AdapterLogSchema,
) -> (VMStatus, VMOutput) {
let balance = txn.max_gas_amount().into();
let mut gas_meter = unwrap_or_discard!(self.make_standard_gas_meter(balance, log_context));

let traversal_storage = TraversalStorage::new();
let mut traversal_context = TraversalContext::new(&traversal_storage);
self.execute_user_transaction_impl(
resolver,
txn,
log_context,
&mut gas_meter,
&mut traversal_context,
)
}

Lastly, the epilogue phase is responsible for finalizing the transaction after it has been executed, and performs functions such as gas-related validation, Move contract error handling, event generation, and more.

// aptos-move/aptos-vm/src/aptos_vm.rs
fn run_epilogue(
session: &mut SessionExt,
gas_remaining: Gas,
fee_statement: FeeStatement,
txn_data: &TransactionMetadata,
features: &Features,
traversal_context: &mut TraversalContext,
) -> VMResult<()> {
let txn_gas_price = txn_data.gas_unit_price();
let txn_max_gas_units = txn_data.max_gas_amount();
// We can unconditionally do this as this condition can only be true if the prologue
// accepted it, in which case the gas payer feature is enabled.
if let Some(fee_payer) = txn_data.fee_payer() {
let (func_name, args) = {
let mut args = vec![
MoveValue::Signer(txn_data.sender),
MoveValue::Address(fee_payer),
MoveValue::U64(fee_statement.storage_fee_refund()),
MoveValue::U64(txn_gas_price.into()),
MoveValue::U64(txn_max_gas_units.into()),
MoveValue::U64(gas_remaining.into()),
];
if txn_data.required_deposit.is_some() {
args.push(txn_data.required_deposit.as_move_value());
(
&APTOS_TRANSACTION_VALIDATION.user_epilogue_gas_payer_return_deposit_name,
args,
)
} else {
(
&APTOS_TRANSACTION_VALIDATION.user_epilogue_gas_payer_name,
args,
)
}
};
session.execute_function_bypass_visibility(
&APTOS_TRANSACTION_VALIDATION.module_id(),
func_name,
vec![],
serialize_values(&args),
&mut UnmeteredGasMeter,
traversal_context,
)
} else {
// Regular tx, run the normal epilogue
let (func_name, args) = {
let mut args = vec![
MoveValue::Signer(txn_data.sender),
MoveValue::U64(fee_statement.storage_fee_refund()),
MoveValue::U64(txn_gas_price.into()),
MoveValue::U64(txn_max_gas_units.into()),
MoveValue::U64(gas_remaining.into()),
];
if txn_data.required_deposit.is_some() {
args.push(txn_data.required_deposit.as_move_value());
(
&APTOS_TRANSACTION_VALIDATION.user_epilogue_return_deposit_name,
args,
)
} else {
(&APTOS_TRANSACTION_VALIDATION.user_epilogue_name, args)
}
};
session.execute_function_bypass_visibility(
&APTOS_TRANSACTION_VALIDATION.module_id(),
func_name,
vec![],
serialize_values(&args),
&mut UnmeteredGasMeter,
traversal_context,
)
}
.map(|_return_vals| ())
.map_err(expect_no_verification_errors)?;
// Emit the FeeStatement event
if features.is_emit_fee_statement_enabled() {
emit_fee_statement(session, fee_statement, traversal_context)?;
}
maybe_raise_injected_error(InjectedError::EndOfRunEpilogue)?;
Ok(())
}

Conclusion

This article has given you a code-level understanding of how MoveVM integrates and works with a blockchain platform like Aptos. We saw that MoveVM provides stability by providing a black-box, robust program execution environment, and Aptos is leveraging this to provide a better interface for users, resulting in a better user experience. We can see that there are challenges in utilizing MoveVM to be 100% efficient at the database and network level, not just at the transaction processing level, and we will be covering that in the future.

Author : Harvey
Reviewer : Luis

Reference

--

--

verse2
verse2
Editor for

Build, incubate, invest — Making all possible in the crypto. / verse2.io