- tags: Starcoin Web3 StarTrek,Move
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:
-
Received some transactions from P2P network: EventHandler of PeerTransactionsMessage.
-
Import transactions into transactions pool as pending transactions: txpool::pool::queue::TransactionQueue::import
During importing, it’ll verify those transactions by calling verify_transaction.
-
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:
- Check signature.
- Load configs from chain by calling some smart contract functions, see below.
- Verify transactions.
- 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
:
- 0x1::VMConfig::instruction_schedule() defines how many gas will be cost to execute each Move instruction.
- 0x1::VMConfig::native_schedule() defines how many gas will be cost to execute each native Move expression.
- 0x1::VMConfig::gas_constants() some constans about gas.
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:
- ModuleResolver find module by ModuleID
- ResourceResolver find resource by address and type
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::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:- 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())?;
- Invoke
init_script
of the package if has any.
- Publish the package as move module bundle.
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.