以太坊虚拟机(上篇)

admin 2023年4月17日13:32:23评论21 views字数 35683阅读118分56秒阅读模式

文章前言

区块链上的虚拟机(Virtual Machine)是指建立在去中心化的区块链上的代码运行环境,目前市面上比较主流的便是以太坊虚拟机(Ethereum Virtual Machine,EVM)和类以太坊虚拟机,它基于Account账户模型将智能合约代码以对外完全隔离的方式在内部运行,实现了图灵完备的智能合约体系,本篇文章将从源码角度对其工作原理进行简要分析~

EVM指令

EVM执行的是字节码,而由于操作码被限制在一个字节以内,所以EVM指令集最多只能容纳256条指令,目前EVM已经定义了100多条指令,还有100多条指令可供以后扩展,这100多条指令包括算术运算指令,比较指令,位运算指令,密码学计算指令,栈、memory、storage操作指令,跳转指令,区块、智能合约相关指令等。

算术运算(0x00)
    STOP:       "STOP",    ADD:        "ADD", //加法运算    MUL:        "MUL", //乘法运算    SUB:        "SUB", //减法运算    DIV:        "DIV", //无符号整除运算    SDIV:       "SDIV", //有符号整除运算    MOD:        "MOD", //无符号取模运算    SMOD:       "SMOD", //有符号取模运算    EXP:        "EXP",  //指数运算    NOT:        "NOT",        // 从栈顶弹出两个元素,进行比较,然后把结果(1表示true,0表示false)推入栈顶    // 其中LT和GT把弹出的元素解释为无符号整数进行比较,SLT和SGT把弹出的元素解释为有符号数进行比较,EQ不关心符号    LT:         "LT",   //无符号小于比较    GT:         "GT",   //无符号大于比较    SLT:        "SLT",  //有符号小于比较    SGT:        "SGT",  //有符号大于比较    EQ:         "EQ",   // 等于比较        //  SZERO指令从栈顶弹出一个元素,判断它是否为0,如果是,则把1推入栈顶,否则把0推入栈顶    ISZERO:     "ISZERO", //布尔取反        //  SIGNEXTEND指令从栈顶依次弹出k和x,并把x解释为k+1(0 <= k <= 31)字节有符号整数,    //  然后把x符号扩展至32字节,比如x是二进制10000000,k是0,则符号扩展之后,结果为二进制1111…10000000(共249个1)    SIGNEXTEND: "SIGNEXTEND" //符号位扩展
按位运算(0x10)
    // AND、OR、XOR指令从栈顶弹出两个元素,进行按位运算,然后把结果推入栈顶    AND:    "AND",    OR:     "OR",    XOR:    "XOR",        // BYTE指令先后从栈顶弹出n和x,取x的第n个字节并推入栈顶,    // 由于EVM的字长是32个字节,所以n在[0, 31]区间内才有意义,    // 否则BYTE的运算结果就是0,另外字节是从左到右数的,因此第0个字节占据字的最高位8个比特    BYTE:   "BYTE",         // 这三条指令都是先后从栈顶弹出两个数n和x,    // 其中x是要进行位移操作顶数,n是位移比特数,然后把结果推入栈顶    SHL:    "SHL",    // SHR和SAR的区别在于,前者执行逻辑右移(空缺补0),后者执行算术右移(空缺补符号位)    SHR:    "SHR",    SAR:    "SAR",        ADDMOD: "ADDMOD",        // MULMOD指令依次从栈顶弹出x、y、z三个数,    // 先计算x和y的乘积(不受溢出限制),再计算乘积和z的模,最后把结果推入栈顶    // 假定乘积不会溢出,那么MULMOD(x, y, z)等价于x * y % z    MULMOD: "MULMOD",
加密运算(0x20)
SHA3: "SHA3"
状态指令(0x30)
    ADDRESS:        "ADDRESS",    BALANCE:        "BALANCE",    ORIGIN:         "ORIGIN",    CALLER:         "CALLER",    CALLVALUE:      "CALLVALUE",    CALLDATALOAD:   "CALLDATALOAD",    CALLDATASIZE:   "CALLDATASIZE",    CALLDATACOPY:   "CALLDATACOPY",    CODESIZE:       "CODESIZE",    CODECOPY:       "CODECOPY",    GASPRICE:       "GASPRICE",    EXTCODESIZE:    "EXTCODESIZE",    EXTCODECOPY:    "EXTCODECOPY",    RETURNDATASIZE: "RETURNDATASIZE",    RETURNDATACOPY: "RETURNDATACOPY",    EXTCODEHASH:    "EXTCODEHASH",
区块指令(0x40)
    BLOCKHASH:   "BLOCKHASH",    COINBASE:    "COINBASE",    TIMESTAMP:   "TIMESTAMP",    NUMBER:      "NUMBER",    DIFFICULTY:  "DIFFICULTY",    GASLIMIT:    "GASLIMIT",    CHAINID:     "CHAINID",    SELFBALANCE: "SELFBALANCE"
存储执行(0x50)
    POP:      "POP",     // 栈顶弹出元素    MLOAD:    "MLOAD",    MSTORE:   "MSTORE",    MSTORE8:  "MSTORE8",    SLOAD:    "SLOAD",  // 先取出栈顶元素x,然后在storage中取以x为键的值(storage[x])存入栈顶    SSTORE:   "SSTORE", // 存储storage是一个键值存储,可将256位字映射到256位字    JUMP:     "JUMP",    JUMPI:    "JUMPI",    PC:       "PC",    MSIZE:    "MSIZE",    GAS:      "GAS",    JUMPDEST: "JUMPDEST"
栈操作类(0x60)
    // PUSH系列指令把紧跟在指令后面的N(1 ~ 32)字节元素推入栈顶    PUSH1:  "PUSH1",    ...    PUSH32: "PUSH32",
// DUP系列指令复制从栈顶开始数的第N(1 ~ 16)个元素,并把复制后的元素推入栈顶 DUP1: "DUP1", DUP2: "DUP2", ... DUP16: "DUP16",
// SWAP系列指令把栈顶元素和从栈顶开始数的第N(1 ~ 16)+ 1 个元素进行交换 SWAP1: "SWAP1", ... SWAP16: "SWAP16", LOG0: "LOG0", ... LOG4: "LOG4",

体系结构

EVM的体系结构示意图如下所示,可以看到在整个EVM体系结构中EVM对象为关键核心所在,相关的交互操作皆通过vm.Config(配置)、Interpreter(解释器)、StateDB(状态数据库)来实现:

  • vm.Config:为EVM和解释器提供了配置信息

  • Interpreter:由EVMInterpreter实现具体功能

  • StateDB:提供数据的永久存储和查询

以太坊虚拟机(上篇)


源码分析

交易剖析

下面我们看一下一笔交易在从创建到最后的广播过程中EVM所起到的作用,每当用户发起交易请求时实际上会调用的以太坊客户端的sendTx方法,之后将交易添加到本地交易列表中:

// filedir:go-ethereum-1.10.2ethapi_backend.go  L229func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {  return b.eth.txPool.AddLocal(signedTx)}

之后将交易添加到交易池中去:

// filedir:go-ethereum-1.10.2coretx_pool.go  L756// AddLocals enqueues a batch of transactions into the pool if they are valid, marking the// senders as a local ones, ensuring they go around the local pricing constraints.//// This method is used to add transactions from the RPC API and performs synchronous pool// reorganization and event propagation.func (pool *TxPool) AddLocals(txs []*types.Transaction) []error {  return pool.addTxs(txs, !pool.config.NoLocals, true)}
// AddLocal enqueues a single local transaction into the pool if it is valid. This is// a convenience wrapper aroundd AddLocals.func (pool *TxPool) AddLocal(tx *types.Transaction) error { errs := pool.AddLocals([]*types.Transaction{tx}) return errs[0]}

之后对交易进行检查(是否已知、是否是新交易等),让后将交易添加到交易队列中去:

// addTxs attempts to queue a batch of transactions if they are valid.func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error {  // Filter out known ones without obtaining the pool lock or recovering signatures  var (    errs = make([]error, len(txs))    news = make([]*types.Transaction, 0, len(txs))  )  for i, tx := range txs {    // If the transaction is known, pre-set the error slot    if pool.all.Get(tx.Hash()) != nil {      errs[i] = ErrAlreadyKnown      knownTxMeter.Mark(1)      continue    }    // Exclude transactions with invalid signatures as soon as    // possible and cache senders in transactions before    // obtaining lock    _, err := types.Sender(pool.signer, tx)    if err != nil {      errs[i] = ErrInvalidSender      invalidTxMeter.Mark(1)      continue    }    // Accumulate all unknown transactions for deeper processing    news = append(news, tx)  }  if len(news) == 0 {    return errs  }
// Process all the new transaction and merge any errors into the original slice pool.mu.Lock() newErrs, dirtyAddrs := pool.addTxsLocked(news, local) pool.mu.Unlock()
var nilSlot = 0 for _, err := range newErrs { for errs[nilSlot] != nil { nilSlot++ } errs[nilSlot] = err nilSlot++ } // Reorg the pool internals if needed and return done := pool.requestPromoteExecutables(dirtyAddrs) if sync { <-done } return errs}

矿工之后会将交易队列中的交易打包进区块,在下面的commitNetwork函数中前半部分为coinbase、链状态、分叉检查等,之后调用w.eth.TxPool().Pending()将处于pending状态的交易从交易池中取出,之后将交易分为本地交易和远程交易,之后调用commitTransactions将需要打包的交易逐个打包进区块:

// filedir: go-ethereum-1.10.2minerworker.go  L867// commitNewWork generates several new sealing tasks based on the parent block.func (w *worker) commitNewWork(interrupt *int32, noempty bool, timestamp int64) {  w.mu.RLock()  defer w.mu.RUnlock()
tstart := time.Now() parent := w.chain.CurrentBlock()
if parent.Time() >= uint64(timestamp) { timestamp = int64(parent.Time() + 1) } num := parent.Number() header := &types.Header{ ParentHash: parent.Hash(), Number: num.Add(num, common.Big1), GasLimit: core.CalcGasLimit(parent, w.config.GasFloor, w.config.GasCeil), Extra: w.extra, Time: uint64(timestamp), } // Only set the coinbase if our consensus engine is running (avoid spurious block rewards) if w.isRunning() { if w.coinbase == (common.Address{}) { log.Error("Refusing to mine without etherbase") return } header.Coinbase = w.coinbase } if err := w.engine.Prepare(w.chain, header); err != nil { log.Error("Failed to prepare header for mining", "err", err) return } // If we are care about TheDAO hard-fork check whether to override the extra-data or not if daoBlock := w.chainConfig.DAOForkBlock; daoBlock != nil { // Check whether the block is among the fork extra-override range limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange) if header.Number.Cmp(daoBlock) >= 0 && header.Number.Cmp(limit) < 0 { // Depending whether we support or oppose the fork, override differently if w.chainConfig.DAOForkSupport { header.Extra = common.CopyBytes(params.DAOForkBlockExtra) } else if bytes.Equal(header.Extra, params.DAOForkBlockExtra) { header.Extra = []byte{} // If miner opposes, don't let it use the reserved extra-data } } } // Could potentially happen if starting to mine in an odd state. err := w.makeCurrent(parent, header) if err != nil { log.Error("Failed to create mining context", "err", err) return } // Create the current work task and check any fork transitions needed env := w.current if w.chainConfig.DAOForkSupport && w.chainConfig.DAOForkBlock != nil && w.chainConfig.DAOForkBlock.Cmp(header.Number) == 0 { misc.ApplyDAOHardFork(env.state) } // Accumulate the uncles for the current block uncles := make([]*types.Header, 0, 2) commitUncles := func(blocks map[common.Hash]*types.Block) { // Clean up stale uncle blocks first for hash, uncle := range blocks { if uncle.NumberU64()+staleThreshold <= header.Number.Uint64() { delete(blocks, hash) } } for hash, uncle := range blocks { if len(uncles) == 2 { break } if err := w.commitUncle(env, uncle.Header()); err != nil { log.Trace("Possible uncle rejected", "hash", hash, "reason", err) } else { log.Debug("Committing new uncle to block", "hash", hash) uncles = append(uncles, uncle.Header()) } } } // Prefer to locally generated uncle commitUncles(w.localUncles) commitUncles(w.remoteUncles)
// Create an empty block based on temporary copied state for // sealing in advance without waiting block execution finished. if !noempty && atomic.LoadUint32(&w.noempty) == 0 { w.commit(uncles, nil, false, tstart) }
// Fill the block with all available pending transactions. pending, err := w.eth.TxPool().Pending() if err != nil { log.Error("Failed to fetch pending transactions", "err", err) return } // Short circuit if there is no available pending transactions. // But if we disable empty precommit already, ignore it. Since // empty block is necessary to keep the liveness of the network. if len(pending) == 0 && atomic.LoadUint32(&w.noempty) == 0 { w.updateSnapshot() return } // Split the pending transactions into locals and remotes localTxs, remoteTxs := make(map[common.Address]types.Transactions), pending for _, account := range w.eth.TxPool().Locals() { if txs := remoteTxs[account]; len(txs) > 0 { delete(remoteTxs, account) localTxs[account] = txs } } if len(localTxs) > 0 { txs := types.NewTransactionsByPriceAndNonce(w.current.signer, localTxs) if w.commitTransactions(txs, w.coinbase, interrupt) { return } } if len(remoteTxs) > 0 { txs := types.NewTransactionsByPriceAndNonce(w.current.signer, remoteTxs) if w.commitTransactions(txs, w.coinbase, interrupt) { return } } w.commit(uncles, w.fullTaskHook, true, tstart)}

commitTransactions会检查gas费用是否足够,之后检查txs并取出gasPrice最小的,之后调用w.commitTransaction(tx, coinbase)开始执行交易

// filedir: go-ethereum-1.10.2minerworker.go  L750func (w *worker) commitTransactions(txs *types.TransactionsByPriceAndNonce, coinbase common.Address, interrupt *int32) bool {  // Short circuit if current is nil  if w.current == nil {    return true  }
if w.current.gasPool == nil { w.current.gasPool = new(core.GasPool).AddGas(w.current.header.GasLimit) }
var coalescedLogs []*types.Log
for { // In the following three cases, we will interrupt the execution of the transaction. // (1) new head block event arrival, the interrupt signal is 1 // (2) worker start or restart, the interrupt signal is 1 // (3) worker recreate the mining block with any newly arrived transactions, the interrupt signal is 2. // For the first two cases, the semi-finished work will be discarded. // For the third case, the semi-finished work will be submitted to the consensus engine. if interrupt != nil && atomic.LoadInt32(interrupt) != commitInterruptNone { // Notify resubmit loop to increase resubmitting interval due to too frequent commits. if atomic.LoadInt32(interrupt) == commitInterruptResubmit { ratio := float64(w.current.header.GasLimit-w.current.gasPool.Gas()) / float64(w.current.header.GasLimit) if ratio < 0.1 { ratio = 0.1 } w.resubmitAdjustCh <- &intervalAdjust{ ratio: ratio, inc: true, } } return atomic.LoadInt32(interrupt) == commitInterruptNewHead } // If we don't have enough gas for any further transactions then we're done if w.current.gasPool.Gas() < params.TxGas { log.Trace("Not enough gas for further transactions", "have", w.current.gasPool, "want", params.TxGas) break } // Retrieve the next transaction and abort if all done tx := txs.Peek() if tx == nil { break } // Error may be ignored here. The error has already been checked // during transaction acceptance is the transaction pool. // // We use the eip155 signer regardless of the current hf. from, _ := types.Sender(w.current.signer, tx) // Check whether the tx is replay protected. If we're not in the EIP155 hf // phase, start ignoring the sender until we do. if tx.Protected() && !w.chainConfig.IsEIP155(w.current.header.Number) { log.Trace("Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", w.chainConfig.EIP155Block)
txs.Pop() continue } // Start executing the transaction w.current.state.Prepare(tx.Hash(), common.Hash{}, w.current.tcount)
logs, err := w.commitTransaction(tx, coinbase) switch { case errors.Is(err, core.ErrGasLimitReached): // Pop the current out-of-gas transaction without shifting in the next from the account log.Trace("Gas limit exceeded for current block", "sender", from) txs.Pop()
case errors.Is(err, core.ErrNonceTooLow): // New head notification data race between the transaction pool and miner, shift log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce()) txs.Shift()
case errors.Is(err, core.ErrNonceTooHigh): // Reorg notification data race between the transaction pool and miner, skip account = log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce()) txs.Pop()
case errors.Is(err, nil): // Everything ok, collect the logs and shift in the next transaction from the same account coalescedLogs = append(coalescedLogs, logs...) w.current.tcount++ txs.Shift()
case errors.Is(err, core.ErrTxTypeNotSupported): // Pop the unsupported transaction without shifting in the next from the account log.Trace("Skipping unsupported transaction type", "sender", from, "type", tx.Type()) txs.Pop()
default: // Strange error, discard the transaction and get the next in line (note, the // nonce-too-high clause will prevent us from executing in vain). log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err) txs.Shift() } }
if !w.isRunning() && len(coalescedLogs) > 0 { // We don't push the pendingLogsEvent while we are mining. The reason is that // when we are mining, the worker will regenerate a mining block every 3 seconds. // In order to avoid pushing the repeated pendingLog, we disable the pending log pushing.
// make a copy, the state caches the logs and these logs get "upgraded" from pending to mined // logs by filling in the block hash when the block was mined by the local miner. This can // cause a race condition if a log was "upgraded" before the PendingLogsEvent is processed. cpy := make([]*types.Log, len(coalescedLogs)) for i, l := range coalescedLogs { cpy[i] = new(types.Log) *cpy[i] = *l } w.pendingLogsFeed.Send(cpy) } // Notify resubmit loop to decrease resubmitting interval if current interval is larger // than the user-specified one. if interrupt != nil { w.resubmitAdjustCh <- &intervalAdjust{inc: false} } return false}

在commitTransaction函数中紧接着创建一个镜像,之后调用core.ApplyTransaction()处理交易:

// filedir: go-ethereum-1.10.2minerworker.go  L736func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) {  snap := w.current.state.Snapshot()
receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &coinbase, w.current.gasPool, w.current.state, w.current.header, tx, &w.current.header.GasUsed, *w.chain.GetVMConfig()) if err != nil { w.current.state.RevertToSnapshot(snap) return nil, err } w.current.txs = append(w.current.txs, tx) w.current.receipts = append(w.current.receipts, receipt)
return receipt.Logs, nil}

ApplyTransaction的实现如下所示,在这里首先将tx转换为一个Message,之后调用NewEVMBlockContext(header, bc, author)构建一个EVM的context

// filedir: go-ethereum-1.10.2corestate_processor.go  L140// 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) {  msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))  if err != nil {    return nil, err  }  // Create a new context to be used in the EVM environment  blockContext := NewEVMBlockContext(header, bc, author)  vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg)  return applyTransaction(msg, config, bc, author, gp, statedb, header, tx, usedGas, vmenv)}

之后调用vm.NewEVM()来创建一个EVM

// NewEVM returns a new EVM. The returned EVM is not thread safe and should// only ever be used *once*.func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, vmConfig Config) *EVM {  evm := &EVM{    Context:      blockCtx,    TxContext:    txCtx,    StateDB:      statedb,    vmConfig:     vmConfig,    chainConfig:  chainConfig,    chainRules:   chainConfig.Rules(blockCtx.BlockNumber),    interpreters: make([]Interpreter, 0, 1),  }
if chainConfig.IsEWASM(blockCtx.BlockNumber) { // to be implemented by EVM-C and Wagon PRs. // if vmConfig.EWASMInterpreter != "" { // extIntOpts := strings.Split(vmConfig.EWASMInterpreter, ":") // path := extIntOpts[0] // options := []string{} // if len(extIntOpts) > 1 { // options = extIntOpts[1..] // } // evm.interpreters = append(evm.interpreters, NewEVMVCInterpreter(evm, vmConfig, options)) // } else { // evm.interpreters = append(evm.interpreters, NewEWASMInterpreter(evm, vmConfig)) // } panic("No supported ewasm interpreter yet.") }
// vmConfig.EVMInterpreter will be used by EVM-C, it won't be checked here // as we always want to have the built-in EVM as the failover option. evm.interpreters = append(evm.interpreters, NewEVMInterpreter(evm, vmConfig)) evm.interpreter = evm.interpreters[0]
return evm}

之后调用applyTransaction来进行具体的交易处理,该方法又会进一步去调用ApplyMessage来处理交易,之后更新处于pending状态的交易状态信息,并未交易创建收据(receipt),如果此时的交易类型是合约创建则存储一份创建地址到收据中,最后更新日志并创建Bloom过滤器:

// filedir: go-ethereum-1.10.2corestate_processor.go  L92func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, error) {  // Create a new context to be used in the EVM environment.  txContext := NewEVMTxContext(msg)  evm.Reset(txContext, statedb)
// Apply the transaction to the current state (included in the env). result, err := ApplyMessage(evm, msg, gp) if err != nil { return nil, err }
// Update the state with pending changes. var root []byte if config.IsByzantium(header.Number) { statedb.Finalise(true) } else { root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes() } *usedGas += result.UsedGas
// Create a new receipt for the transaction, storing the intermediate root and gas used // by the tx. receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} if result.Failed() { receipt.Status = types.ReceiptStatusFailed } else { receipt.Status = types.ReceiptStatusSuccessful } receipt.TxHash = tx.Hash() receipt.GasUsed = result.UsedGas
// If the transaction created a contract, store the creation address in the receipt. if msg.To() == nil { receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) }
// Set the receipt logs and create the bloom filter. receipt.Logs = statedb.GetLogs(tx.Hash()) receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) receipt.BlockHash = statedb.BlockHash() receipt.BlockNumber = header.Number receipt.TransactionIndex = uint(statedb.TxIndex()) return receipt, err}

ApplyMessage函数实现如下,在这里紧接着调用TransitionDb函数:

// filedir: go-ethereum-1.10.2corestate_transition.go  L162// ApplyMessage computes the new state by applying the given message// against the old state within the environment.//// ApplyMessage returns the bytes returned by any EVM execution (if it took place),// the gas used (which includes gas refunds) and an error if it failed. An error always// indicates a core error meaning that the message would always fail for that particular// state and would never be accepted within a block.func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) (*ExecutionResult, error) {  return NewStateTransition(evm, msg, gp).TransitionDb()}

TransitionDb函数通过执行当前消息并返回evm执行结果来更新交易状态,但在执行消息之前,首先会检查此消息是否满足所有一致性规则,在下述代码中可以看到这里首先会调用preCheck进行一次预先检查(主要是账户余额和Nonce),之后使用布尔类型的变量contractCreation来暂存交易的to地址是否为空,之后检查gas费用,检查调用者是否有足够的资产,之后检查contractCreation是否为true,如果是则调用evm.Create创建一个合约,否则调用evm.Call来执行交易,之后计算剩余gas并返回执行结果,在这里可以看出不论是合约创建还是普通的转账交易,其底层的执行还是通过EVM来完成的,我们可以将EVM想象成一个厨房,而我们的普通交易和合约创建即为烹饪食材,经过EVM的烹饪之后输出美味的菜肴:

// filedir:go-ethereum-1.10.2corestate_transition.go  L212// TransitionDb will transition the state by applying the current message and// returning the evm execution result with following fields.//// - used gas://      total gas used (including gas being refunded)// - returndata://      the returned data from evm// - concrete execution error://      various **EVM** error which aborts the execution,//      e.g. ErrOutOfGas, ErrExecutionReverted//// However if any consensus issue encountered, return the error directly with// nil evm execution result.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 if err := st.preCheck(); err != nil { return nil, err } msg := st.msg sender := vm.AccountRef(msg.From()) homestead := st.evm.ChainConfig().IsHomestead(st.evm.Context.BlockNumber) istanbul := st.evm.ChainConfig().IsIstanbul(st.evm.Context.BlockNumber) contractCreation := msg.To() == nil
// Check clauses 4-5, subtract intrinsic gas if everything is correct gas, err := IntrinsicGas(st.data, st.msg.AccessList(), contractCreation, homestead, istanbul) if err != nil { return nil, err } if st.gas < gas { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas) } st.gas -= gas
// Check clause 6 if msg.Value().Sign() > 0 && !st.evm.Context.CanTransfer(st.state, msg.From(), msg.Value()) { return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From().Hex()) }
// Set up the initial access list. if rules := st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber); rules.IsBerlin { st.state.PrepareAccessList(msg.From(), 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 ) if contractCreation { ret, _, st.gas, vmerr = st.evm.Create(sender, st.data, st.gas, st.value) } else { // Increment the nonce for the next transaction st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value) } st.refundGas() st.state.AddBalance(st.evm.Context.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
return &ExecutionResult{ UsedGas: st.gasUsed(), Err: vmerr, ReturnData: ret, }, nil}
数据结构

合约数据结构定义如下,这里主要定义了初始化合约的调用者信息、合约代码、合约代码的Hash、代码地址、输入参数、Gas信息:

// filedir:go-ethereum-1.10.2corevmcontract.go L25// ContractRef is a reference to the contract's backing objecttype ContractRef interface {  Address() common.Address}
// AccountRef implements ContractRef.//// Account references are used during EVM initialisation and// it's primary use is to fetch addresses. Removing this object// proves difficult because of the cached jump destinations which// are fetched from the parent contract (i.e. the caller), which// is a ContractRef.type AccountRef common.Address
// Address casts AccountRef to a Addressfunc (ar AccountRef) Address() common.Address { return (common.Address)(ar) }
// Contract represents an ethereum contract in the state database. It contains// the contract code, calling arguments. Contract implements ContractReftype Contract struct { // CallerAddress is the result of the caller which initialised this // contract. However when the "call method" is delegated this value // needs to be initialised to that of the caller's caller. CallerAddress common.Address caller ContractRef self ContractRef
jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis. analysis bitvec // Locally cached result of JUMPDEST analysis
Code []byte CodeHash common.Hash CodeAddr *common.Address Input []byte
Gas uint64 value *big.Int}

EVM数据结构如下,主要提供了当前区块链相关信息、StateDb访问、调用栈信息、当前区块链配置、参数信息、VM配置信息、解释器信息等:

// filedir:go-ethereum-1.10.2corevmevm.go  L105// EVM is the Ethereum Virtual Machine base object and provides// the necessary tools to run a contract on the given state with// the provided context. It should be noted that any error// generated through any of the calls should be considered a// revert-state-and-consume-all-gas operation, no checks on// specific errors should ever be performed. The interpreter makes// sure that any errors generated are to be considered faulty code.//// The EVM should never be reused and is not thread safe.type EVM struct {  // Context provides auxiliary blockchain related information  Context BlockContext  TxContext  // StateDB gives access to the underlying state  StateDB StateDB  // Depth is the current call stack  depth int
// chainConfig contains information about the current chain chainConfig *params.ChainConfig // chain rules contains the chain rules for the current epoch chainRules params.Rules // virtual machine configuration options used to initialise the // evm. vmConfig Config // global (to this context) ethereum virtual machine // used throughout the execution of the tx. interpreters []Interpreter interpreter Interpreter // abort is used to abort the EVM calling operations // NOTE: must be set atomically abort int32 // callGasTemp holds the gas available for the current call. This is needed because the // available gas is calculated in gasCall* according to the 63/64 rule and later // applied in opCall*. callGasTemp uint64}

BlockContext提供了EVM辅助信息,一旦提供不应该再次进行修改,这里的CanTransfer用于检查账户是否有足够的ether进行转账,transfer用于从一个账户给另一个账户转账,GetHash用于返回入参n对应的Hash值:

// BlockContext provides the EVM with auxiliary information. Once provided// it shouldn't be modified.type BlockContext struct {  // CanTransfer returns whether the account contains  // sufficient ether to transfer the value  CanTransfer CanTransferFunc  // Transfer transfers ether from one account to the other  Transfer TransferFunc  // GetHash returns the hash corresponding to n  GetHash GetHashFunc
// Block information Coinbase common.Address // Provides information for COINBASE GasLimit uint64 // Provides information for GASLIMIT BlockNumber *big.Int // Provides information for NUMBER Time *big.Int // Provides information for TIME Difficulty *big.Int // Provides information for DIFFICULTY}

TxContext用于提供EVM的交易信息,所有字段在交易过程中可以被修改:

// TxContext provides the EVM with information about a transaction.// All fields can change between transactions.type TxContext struct {  // Message information  Origin   common.Address // Provides information for ORIGIN  GasPrice *big.Int       // Provides information for GASPRICE}
EVM对象

NewEVM用于返回一个EVM对象,该对象不应该具有安全风险同时只应该被使用一次:

// filedir:go-ethereum-1.10.2corevmevm.go L143// NewEVM returns a new EVM. The returned EVM is not thread safe and should// only ever be used *once*.func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, vmConfig Config) *EVM {    evm := &EVM{        Context:      blockCtx,        TxContext:    txCtx,        StateDB:      statedb,        vmConfig:     vmConfig,        chainConfig:  chainConfig,        chainRules:   chainConfig.Rules(blockCtx.BlockNumber),        interpreters: make([]Interpreter, 0, 1),    }
if chainConfig.IsEWASM(blockCtx.BlockNumber) { // to be implemented by EVM-C and Wagon PRs. // if vmConfig.EWASMInterpreter != "" { // extIntOpts := strings.Split(vmConfig.EWASMInterpreter, ":") // path := extIntOpts[0] // options := []string{} // if len(extIntOpts) > 1 { // options = extIntOpts[1..] // } // evm.interpreters = append(evm.interpreters, NewEVMVCInterpreter(evm, vmConfig, options)) // } else { // evm.interpreters = append(evm.interpreters, NewEWASMInterpreter(evm, vmConfig)) // } panic("No supported ewasm interpreter yet.") }
// vmConfig.EVMInterpreter will be used by EVM-C, it won't be checked here // as we always want to have the built-in EVM as the failover option. evm.interpreters = append(evm.interpreters, NewEVMInterpreter(evm, vmConfig)) evm.interpreter = evm.interpreters[0]
return evm}
合约创建

通过上面的交易剖析部分的分析我们了解到当交易的接受地址为空地址时则会调用evm.Create方法来创建合约,这里我们对其进行简易跟踪分析:

// filedir:go-ethereum-1.10.2corevmevm.go L500// Create creates a new contract using code as deployment code.func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {  contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))  return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr)}

从上述代码中可以看到这里首先调用crypto.CreateAddress函数根据调用者地址以及当前Nonce值来生成一个合约地址,之后再调用evm.create来创建合约,在这里首先会检查合约创建时的递归调用次数是否大于最大递归调用次数,之后检查合约创建者是否有足够的ether,之后获取并更新Nonce值,之后检查当前合约的地址是否已经存在,随机通过evm.StateDB.CreateAccount(address)创建合约账户,并将交易中的以太币数值(value)转入到新建的账户里:

// create creates a new contract using code as deployment code.func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {  // Depth check execution. Fail if we're trying to execute above the  // limit.  if evm.depth > int(params.CallCreateDepth) {    return nil, common.Address{}, gas, ErrDepth  }  if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {    return nil, common.Address{}, gas, ErrInsufficientBalance  }  nonce := evm.StateDB.GetNonce(caller.Address())  evm.StateDB.SetNonce(caller.Address(), nonce+1)  // We add this to the access list _before_ taking a snapshot. Even if the creation fails,  // the access-list change should not be rolled back  if evm.chainRules.IsBerlin {    evm.StateDB.AddAddressToAccessList(address)  }  // Ensure there's no existing contract already at the designated address  contractHash := evm.StateDB.GetCodeHash(address)  if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {    return nil, common.Address{}, 0, ErrContractAddressCollision  }  // Create a new account on the state  snapshot := evm.StateDB.Snapshot()  evm.StateDB.CreateAccount(address)  if evm.chainRules.IsEIP158 {    evm.StateDB.SetNonce(address, 1)  }  evm.Context.Transfer(evm.StateDB, caller.Address(), address, value)
// Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. contract := NewContract(caller, AccountRef(address), value, gas) contract.SetCodeOptionalHash(&address, codeAndHash)
if evm.vmConfig.NoRecursion && evm.depth > 0 { return nil, address, gas, nil }
if evm.vmConfig.Debug && evm.depth == 0 { evm.vmConfig.Tracer.CaptureStart(evm, caller.Address(), address, true, codeAndHash.code, gas, value) } start := time.Now()
ret, err := run(evm, contract, nil, false)
// check whether the max code size has been exceeded maxCodeSizeExceeded := evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize // if the contract creation ran successfully and no errors were returned // calculate the gas required to store the code. If the code could not // be stored due to not enough gas set an error and let it be handled // by the error checking condition below. if err == nil && !maxCodeSizeExceeded { createDataGas := uint64(len(ret)) * params.CreateDataGas if contract.UseGas(createDataGas) { evm.StateDB.SetCode(address, ret) } else { err = ErrCodeStoreOutOfGas } }
// When an error was returned by the EVM or when setting the creation code // above we revert to the snapshot and consume any gas remaining. Additionally // when we're in homestead this also counts for code storage gas errors. if maxCodeSizeExceeded || (err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas)) { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { contract.UseGas(contract.Gas) } } // Assign err if contract code size exceeds the max while the err is still empty. if maxCodeSizeExceeded && err == nil { err = ErrMaxCodeSizeExceeded } if evm.vmConfig.Debug && evm.depth == 0 { evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err) } return ret, address, contract.Gas, err
}

之后调用NewContract来初始化一个新的合约执行环境对象,之后检查当前以太坊虚拟机的配置是否被配置为不可递归模式,如果EVM不可递归且当前合约正在递归过程中则直接返回,之后检查是否开启模式,以及当前的递归深度是否为0,如果是则跟踪执行流程,之后调用run函数来执行合约的代码:

  // Initialise a new contract and set the code that is to be used by the EVM.  // The contract is a scoped environment for this execution context only.  contract := NewContract(caller, AccountRef(address), value, gas)  contract.SetCodeOptionalHash(&address, codeAndHash)
if evm.vmConfig.NoRecursion && evm.depth > 0 { return nil, address, gas, nil }
if evm.vmConfig.Debug && evm.depth == 0 { evm.vmConfig.Tracer.CaptureStart(evm, caller.Address(), address, true, codeAndHash.code, gas, value) } start := time.Now()
ret, err := run(evm, contract, nil, false)

run通过一个for循环从当前EVM对象中选择一个运行的解释器来运行当前合约并返回其结果:

// run runs the given contract and takes care of running precompiles with a fallback to the byte code interpreter.func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, error) {  for _, interpreter := range evm.interpreters {    if interpreter.CanRun(contract.Code) {      if evm.interpreter != interpreter {        // Ensure that the interpreter pointer is set back        // to its current value upon return.        defer func(i Interpreter) {          evm.interpreter = i        }(evm.interpreter)        evm.interpreter = interpreter      }      return interpreter.Run(contract, input, readOnly)    }  }  return nil, errors.New("no compatible interpreter")}

Run函数如下所示:

// filedir: go-ethereum-1.10.2corevminterpreter.go  L134// Run loops and evaluates the contract's code with the given input data and returns// the return byte-slice and an error if one occurred.//// It's important to note that any errors returned by the interpreter should be// considered a revert-and-consume-all-gas operation except for// ErrExecutionReverted which means revert-and-keep-gas-left.func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
// Increment the call depth which is restricted to 1024 in.evm.depth++ defer func() { in.evm.depth-- }()
// Make sure the readOnly is only set if we aren't in readOnly yet. // This makes also sure that the readOnly flag isn't removed for child calls. if readOnly && !in.readOnly { in.readOnly = true defer func() { in.readOnly = false }() }
// Reset the previous call's return data. It's unimportant to preserve the old buffer // as every returning call will return new data anyway. in.returnData = nil
// Don't bother with the execution if there's no code. if len(contract.Code) == 0 { return nil, nil }
var ( op OpCode // current opcode mem = NewMemory() // bound memory stack = newstack() // local stack callContext = &ScopeContext{ Memory: mem, Stack: stack, Contract: contract, } // For optimisation reason we're using uint64 as the program counter. // It's theoretically possible to go above 2^64. The YP defines the PC // to be uint256. Practically much less so feasible. pc = uint64(0) // program counter cost uint64 // copies used by tracer pcCopy uint64 // needed for the deferred Tracer gasCopy uint64 // for Tracer to log gas remaining before execution logged bool // deferred Tracer should ignore already logged steps res []byte // result of the opcode execution function ) // Don't move this deferrred function, it's placed before the capturestate-deferred method, // so that it get's executed _after_: the capturestate needs the stacks before // they are returned to the pools defer func() { returnStack(stack) }() contract.Input = input
if in.cfg.Debug { defer func() { if err != nil { if !logged { in.cfg.Tracer.CaptureState(in.evm, pcCopy, op, gasCopy, cost, callContext, in.returnData, in.evm.depth, err) } else { in.cfg.Tracer.CaptureFault(in.evm, pcCopy, op, gasCopy, cost, callContext, in.evm.depth, err) } } }() } // The Interpreter main run loop (contextual). This loop runs until either an // explicit STOP, RETURN or SELFDESTRUCT is executed, an error occurred during // the execution of one of the operations or until the done flag is set by the // parent context. steps := 0 for { steps++ if steps%1000 == 0 && atomic.LoadInt32(&in.evm.abort) != 0 { break } if in.cfg.Debug { // Capture pre-execution values for tracing. logged, pcCopy, gasCopy = false, pc, contract.Gas }
// Get the operation from the jump table and validate the stack to ensure there are // enough stack items available to perform the operation. op = contract.GetOp(pc) operation := in.cfg.JumpTable[op] if operation == nil { return nil, &ErrInvalidOpCode{opcode: op} } // Validate stack if sLen := stack.len(); sLen < operation.minStack { return nil, &ErrStackUnderflow{stackLen: sLen, required: operation.minStack} } else if sLen > operation.maxStack { return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack} } // If the operation is valid, enforce and write restrictions if in.readOnly && in.evm.chainRules.IsByzantium { // If the interpreter is operating in readonly mode, make sure no // state-modifying operation is performed. The 3rd stack item // for a call operation is the value. Transferring value from one // account to the others means the state is modified and should also // return with an error. if operation.writes || (op == CALL && stack.Back(2).Sign() != 0) { return nil, ErrWriteProtection } } // Static portion of gas cost = operation.constantGas // For tracing if !contract.UseGas(operation.constantGas) { return nil, ErrOutOfGas }
var memorySize uint64 // calculate the new memory size and expand the memory to fit // the operation // Memory check needs to be done prior to evaluating the dynamic gas portion, // to detect calculation overflows if operation.memorySize != nil { memSize, overflow := operation.memorySize(stack) if overflow { return nil, ErrGasUintOverflow } // memory is expanded in words of 32 bytes. Gas // is also calculated in words. if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow { return nil, ErrGasUintOverflow } } // Dynamic portion of gas // consume the gas and return an error if not enough gas is available. // cost is explicitly set so that the capture state defer method can get the proper cost if operation.dynamicGas != nil { var dynamicCost uint64 dynamicCost, err = operation.dynamicGas(in.evm, contract, stack, mem, memorySize) cost += dynamicCost // total cost, for debug tracing if err != nil || !contract.UseGas(dynamicCost) { return nil, ErrOutOfGas } } if memorySize > 0 { mem.Resize(memorySize) }
if in.cfg.Debug { in.cfg.Tracer.CaptureState(in.evm, pc, op, gasCopy, cost, callContext, in.returnData, in.evm.depth, err) logged = true }
// execute the operation res, err = operation.execute(&pc, in, callContext) // if the operation clears the return data (e.g. it has returning data) // set the last return to the result of the operation. if operation.returns { in.returnData = common.CopyBytes(res) }
switch { case err != nil: return nil, err case operation.reverts: return res, ErrExecutionReverted case operation.halts: return res, nil case !operation.jumps: pc++ } } return nil, nil}

之后检查合约代码长度是否超过最大的限制,如果未超过,则调用StateDB.SetCode将合约代码存储到以太坊状态数据库的合约账户中,最后返回合约字节码以及合约的地址以及合约所耗费的gas费用:

  // check whether the max code size has been exceeded  maxCodeSizeExceeded := evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize  // if the contract creation ran successfully and no errors were returned  // calculate the gas required to store the code. If the code could not  // be stored due to not enough gas set an error and let it be handled  // by the error checking condition below.  if err == nil && !maxCodeSizeExceeded {    createDataGas := uint64(len(ret)) * params.CreateDataGas    if contract.UseGas(createDataGas) {      evm.StateDB.SetCode(address, ret)    } else {      err = ErrCodeStoreOutOfGas    }  }
// When an error was returned by the EVM or when setting the creation code // above we revert to the snapshot and consume any gas remaining. Additionally // when we're in homestead this also counts for code storage gas errors. if maxCodeSizeExceeded || (err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas)) { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { contract.UseGas(contract.Gas) } } // Assign err if contract code size exceeds the max while the err is still empty. if maxCodeSizeExceeded && err == nil { err = ErrMaxCodeSizeExceeded } if evm.vmConfig.Debug && evm.depth == 0 { evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err) } return ret, address, contract.Gas, err
}


未完,请续看下篇~

原文始发于微信公众号(七芒星实验室):以太坊虚拟机(上篇)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年4月17日13:32:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   以太坊虚拟机(上篇)https://cn-sec.com/archives/1674404.html

发表评论

匿名网友 填写信息