简介
本文讨论如何在非EVM链上,移植EVM支持ETH交易。
- 代码:github.com:ethereum/go-ethereum.git
- 版本:1.14.6
总结
移植EVM步骤概述:
- 准备自己的statedb。非EVM链账户模型往往和以太坊不一致,EVM虚拟机在执行过程中,会访问statedb来更改账户状态,因此需要准备自己的statedb,搭建虚拟机和本地账户模型之间交互的桥梁。
- 准备运行环境Context以及Config。作为沙盒调用虚拟机的参数。
- 引用evm虚拟机github.com/ethereum/go-ethereum/core/vm。vm包是调用虚拟机的入口,入口为evm.Create和evm.Call。
- 实现自己的TransitionDb逻辑,主要是处理gas费的扣除,返回,以及矿工奖励。
ETH交易执行流程分析
1. EVM虚拟机工作方式
我们先了解下EVM的工作原理,这篇文章分析得非常详细易懂(https://learnblockchain.cn/2019/04/09/easy-evm)。
其中架构图如下:

核心思想为:
- EVM作为沙盒运行,每执行一笔交易,就会创建一个独立的EVM。交易会被转化为Message格式,传递给EVM执行。
- EVM运行前,需要准备环境,也就是一些参数,叫做Context,Config,用于访问链,block的信息。
- EVM运行过程中,会访问StateDB,修改StateObject(也就是账户)状态,整个过程叫做StateTransition。注意修改后的状态保存在缓存中,并未真正落盘。通过访问StateDB,能够获取修改后的状态。
- EVM内部会调用Interpreter执行OpCode。
- EVM运行完成后,并未真实改变链上状态。而是将状态修改保存在StateDB缓存中,EVM返回Gas消耗,以及执行结果。
- 调用者根据EVM的执行结果,执行最终Gas扣费,状态落盘上链逻辑。或者什么也不做,放弃本次状态更改,比如当前是eth_call调用时。
2. 交易执行入口
交易执行入口是ApplyTransaction。EVM虚拟机作为沙盒运行,需要配置具体的运行环境,主要组件为:
- blockContext,EVM用于获取block信息。
- txContext,获取交易信息。
- statedb,用于保存状态变更。
- config,链上配置,包括chainconfig以及rules灯。
代码如下:
// ApplyTransaction attempts to apply a transaction to the given state database // and uses the input parameters for its environment. It returns the receipt // for the transaction, gas used and an error if the transaction failed, // indicating the block was invalid. func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) { //1. 把Transaction转化Message为类型 msg, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee) if err != nil { return nil, err } // Create a new context to be used in the EVM environment //2. 创建BlockContext blockContext := NewEVMBlockContext(header, bc, author) //3. 创建TxContext txContext := NewEVMTxContext(msg) vmenv := vm.NewEVM(blockContext, txContext, statedb, config, cfg) //调用ApplyTransactionWithEVM return ApplyTransactionWithEVM(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) }
ApplyTransactionWithEVM调用ApplyMessage,最终执行TransitionDb。
3. 交易执行核心逻辑
交易执行核心逻辑在TransitionDb中。其中核心流程为:
- 执行金额检查,以及扣除最大gas费,gasLimit * gasPrice。
- 计算固定Gas。
- 调用虚拟机执行交易,其中合约的创建调用evm.Create函数。转账和合约调用调用evm.Call。虚拟机执行完交易后,会统计出剩下的Gas。
- 返回用户剩下的Gas, 包括Refund机制的退费,以及执行剩下的Gas。
- 给矿工付费Tip。
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { // First check this message satisfies all consensus rules before // applying the message. The rules include these clauses // // 1. the nonce of the message caller is correct // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice) // 3. the amount of gas required is available in the block // 4. the purchased gas is enough to cover intrinsic usage // 5. there is no overflow when calculating intrinsic gas // 6. caller has enough balance to cover asset transfer for **topmost** call // Check clauses 1-3, buy gas if everything is correct //执行金额检查,以及扣除最大gas费,gasLimit * gasPrice if err := st.preCheck(); err != nil { return nil, err } var ( msg = st.msg sender = vm.AccountRef(msg.From) rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time) contractCreation = msg.To == nil ) // Check clauses 4-5, subtract intrinsic gas if everything is correct // 计算固定Gas gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) if err != nil { return nil, err } if st.gasRemaining < gas { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas) } if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { t.OnGasChange(st.gasRemaining, st.gasRemaining-gas, tracing.GasChangeTxIntrinsicGas) } st.gasRemaining -= gas if rules.IsEIP4762 { st.evm.AccessEvents.AddTxOrigin(msg.From) if targetAddr := msg.To; targetAddr != nil { st.evm.AccessEvents.AddTxDestination(*targetAddr, msg.Value.Sign() != 0) } } // Check clause 6 value, overflow := uint256.FromBig(msg.Value) if overflow { return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex()) } if !value.IsZero() && !st.evm.Context.CanTransfer(st.state, msg.From, value) { return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex()) } // Check whether the init code size has been exceeded. if rules.IsShanghai && contractCreation && len(msg.Data) > params.MaxInitCodeSize { return nil, fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, len(msg.Data), params.MaxInitCodeSize) } // Execute the preparatory steps for state transition which includes: // - prepare accessList(post-berlin) // - reset transient storage(eip 1153) st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) var ( ret []byte vmerr error // vm errors do not effect consensus and are therefore not assigned to err ) //调用虚拟机执行交易,其中合约的创建调用evm.Create函数。转账和合约调用调用evm.Call //虚拟机执行完交易后,会统计出剩下的Gas if contractCreation { ret, _, st.gasRemaining, vmerr = st.evm.Create(sender, msg.Data, st.gasRemaining, value) } else { // Increment the nonce for the next transaction st.state.SetNonce(msg.From, st.state.GetNonce(sender.Address())+1) ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, value) } //返回用户剩下的Gas, 包括Refund机制的退费,以及执行剩下的Gas var gasRefund uint64 if !rules.IsLondon { // Before EIP-3529: refunds were capped to gasUsed / 2 gasRefund = st.refundGas(params.RefundQuotient) } else { // After EIP-3529: refunds are capped to gasUsed / 5 gasRefund = st.refundGas(params.RefundQuotientEIP3529) } effectiveTip := msg.GasPrice if rules.IsLondon { effectiveTip = cmath.BigMin(msg.GasTipCap, new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee)) } effectiveTipU256, _ := uint256.FromBig(effectiveTip) if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 { // Skip fee payment when NoBaseFee is set and the fee fields // are 0. This avoids a negative effectiveTip being applied to // the coinbase when simulating calls. } else { fee := new(uint256.Int).SetUint64(st.gasUsed()) fee.Mul(fee, effectiveTipU256) //给矿工账户付费Tip st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee) // add the coinbase to the witness iff the fee is greater than 0 if rules.IsEIP4762 && fee.Sign() != 0 { st.evm.AccessEvents.BalanceGas(st.evm.Context.Coinbase, true) } } //返回执行结果 return &ExecutionResult{ UsedGas: st.gasUsed(), RefundedGas: gasRefund, Err: vmerr, ReturnData: ret, }, nil }
4. 虚拟机执行入口
从上面交易执行逻辑得知,虚拟机(github.com/ethereum/go-ethereum/core/vm)的真正执行入口函数为:
- evm.Create,合约创建。
- evm.Call,转账或合约调用。
if contractCreation { //Create函数是创建合约 ret, _, st.gasRemaining, vmerr = st.evm.Create(sender, msg.Data, st.gasRemaining, value) } else { // Increment the nonce for the next transaction st.state.SetNonce(msg.From, st.state.GetNonce(sender.Address())+1) ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, value) }
实现EVM移植
经过上面分析,EVM的移植就很清楚了。对照EVM工作方式,移植者需要做到以下工作:
- 准备环境,构建Context,Config。
- 把交易转化为Message格式,创建一个新的EVM。
- 准备StateDB。
- 调用者根据EVM的执行结果,执行最终Gas扣费,状态落盘上链逻辑。或者什么也不做,放弃本次状态更改,比如当前是eth_call调用时。(可参照go-ethereum的TransitionDb函数)
实例
具体实例可参照evmos(https://github.com/evmos/evmos)。在cosmos链上移植了EVM虚拟机。
回复 agodelo 取消回复