When I start learning Move and looking at the stdlib starcoin-framework and starcoin-framework-commons. Then I realized there are must some magic during the block execution in runtime. To roll the world, the runtime should provide some built in types and call some function in the stdlib.

How does StarcoinVM Validate Transactions?

As a miner, it’s responsible for executing block, it follows:

  1. Received some transactions from P2P network: EventHandler of PeerTransactionsMessage.

  2. Import transactions into transactions pool as pending transactions: txpool::pool::queue::TransactionQueue::import

    During importing, it’ll verify those transactions by calling verify_transaction.

  3. Retrieve pending transactions and put it into the a block prepare to execute.

In the step 2, the miner need to verify the received transactions before put it into queue as pending transactions.

The actually verify logic are defined at StarcoinVM::verify_transaction, it follows:

  1. Check signature.
  2. Load configs from chain by calling some smart contract functions, see below.
  3. Verify transactions.
  4. Run 0x01::TransactionManager::prologue smart contract.

How is StarcoinVM be Involved?

The main functionality is provided by a struct StarcoinVM which is a wrapper of MoveVM.

A starcoin_open_block::OpenedBlock will be created, when a block is created on chain by starcoin_chain::chain::BlockChain::create_block_template. Some pending transactions (if have any) will push into it by its push_txns, then it’s time to get StarcoinVM involved.

StarcoinVM is on duty to execute those transactions.

How does StarcoinVM Execute Transactions in Block?

StarcoinVM::execute_block_transactions will be invoked to execute transactions.

Preparation

Inject natives to MoveVM

When we created a StarcoinVM by StarcoinVM::new, its will create MoveVM by:

pub fn new(metrics: Option<VMMetrics>) -> Self {
	let inner = MoveVM::new(super::natives::starcoin_natives())
		.expect("should be able to create Move VM; check if there are duplicated natives");
	Self {
		move_vm: Arc::new(inner),
		vm_config: None,
		version: None,
		move_version: None,
		metrics,
	}
}

All the natives that been injected to MoveVM are defined at starcoin_natives::starcoin_natives().

Load Configs by Calling Move Module Defined in stdlib

The first thing it will do is load configs and call some functions in stdlib(Here we will skip the operations of genesis, there in different branch).

Those smart contract functions are invoked by StarcoinVM::execute_readonly_function:

With those configurations, the StarcoinVM knows how many gas will be cost during a execution. I think this way to maintain config is very smart, as it can change the gas cost way without change the node(Rust). Further more, can change it with DAO.

Wait a minute, How those smart contract functions are executed?

Execute Readonly Function

Let’s see what have done in StarcoinVM::execute_readonly_function:

In short words, provide a StateViewCache to a new session of MoveVM, then call session.execute_function.

StateViewCache implements some necessary resolver traits that help MoveVM session to locate the module and resource:

As the stdlib has already deployed on the chain at 0x01 address in the genesis process, so the StateViewCache with those resolver resolver implementations will lead MoveVM locate the stdlib module.

Execute Block Transactions

Now it’s time to check the remain logic, there two types of transactions we need to care about:

TransactionBlock::Prologue

Each block whether it contains transactions or not, a prologue always need to be done, mainly invoke a smart contract function in stdlib:

TransactionBlock::UserTransaction

When a block contains transactions, it have three kinds of payload, which are defined as:

#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum TransactionPayload {
	/// A transaction that executes code.
	Script(Script),
	/// A transaction that publish or update module code by a package.
	Package(Package),
	/// A transaction that executes an existing script function published on-chain.
	ScriptFunction(ScriptFunction),
}

TransactionPayload::Script and TransactionPayload::ScriptFuntion have the same behaviour, that defined in StarcoinVM::execute_script_or_script_function. Before the script or script funciton been executed, a prologue which defined in stdlib will be executed first, and then an epilogue will be executed when the transactions is exeucted successfully:

  • 0x01::TransactionManager::prologue

  • 0x01::TransactionManager::epilogue or 0x01::TransactionManager::epilogue_v2, it depends on which version of stdlib is used in current runtime.

    TransactionPayload::Package invovled the same smart contract functions, but it’s code logic is more complex, check StarcoinVM::execute_package:

    1. Publish the package as move module bundle.

      session
      	.publish_module_bundle_with_option(
      		package
      			.modules()
      			.iter()
      			.map(|m| m.code().to_vec())
      			.collect(),
      		package.package_address(), // be careful with the sender.
      		cost_strategy,
      		PublishModuleBundleOption {
      			force_publish: enforced,
      			only_new_module,
      		},
      	)
      	.map_err(|e| e.into_vm_status())?;
      
    2. Invoke init_script of the package if has any.

Same question as above, how this module stored into chain? Let’s check StateViewCache again, it haven’t the corresponding traits. After doing some research, I found the session we are used isn’t in the offcial Move language, it defined as move_vm::move_vm_runtime::move_vm_adapter::SessionAdapter. It’ll invoke DataStore::publish_module, it just put our package into our account cache. This explains how a smart contract is deployed.

How does a Smart Contract Be Executed

I’m curious about how a smart contract been executed.

So, with those questions, let’s see how a block is executed on chain.

To execute a smart contract, or in starcoin to invoke Move modules from a Move script, we can simply make a RPC invocation contract.call_v2, which defines at rpc/server/src/module/contract_rpc.rs#L145:

fn call_v2(&self, call: ContractCall) -> FutureResult<Vec<DecodedMoveValue>> {
	let service = self.chain_state.clone();
	let storage = self.storage.clone();
	let ContractCall {
		function_id,
		type_args,
		args,
	} = call;
	let metrics = self.playground.metrics.clone();
	let f = async move {
		let state_root = service.state_root().await?;
		let state = ChainStateDB::new(storage, Some(state_root));
		let output = call_contract(
			&state,
			function_id.0.module,
			function_id.0.function.as_str(),
			type_args.into_iter().map(|v| v.0).collect(),
			args.into_iter().map(|v| v.0).collect(),
			metrics,
		)?;
		let annotator = MoveValueAnnotator::new(&state);
		output
			.into_iter()
			.map(|(ty, v)| annotator.view_value(&ty, &v).map(Into::into))
			.collect::<anyhow::Result<Vec<_>>>()
	}
	.map_err(map_err);
	Box::pin(f.boxed())
}

The call_contract will eventually call StarcoinVM::execute_readonly_function which we have already discussed above.

Mint Block

Once the block had been created and executed, the miner is going to mint the block: generate nounces to meet the diffculty. When it has been done, the block is ready to append to the chain, which means all the transactions in the block are take effect.