文章前言
以太坊虚拟机(Ethereum Virtual Machine,简称EVM)是区块链领域的重要组成部分,它为以太坊网络上的智能合约提供了执行环境,随着区块链技术的发展和智能合约应用的日益普及,了解以太坊虚拟机的工作原理变得尤为重要。本文将深入剖析以太坊虚拟机的工作原理,探讨其在智能合约执行过程中的关键角色和功能,我们将介绍EVM的架构和基本组件,解释其如何解析和执行智能合约的字节码指令,我们还将探讨EVM中的状态管理机制和存储模型,以及如何处理异常情况和安全性考虑
基本介绍
以太坊虚拟机(Ethereum Virtual Machine,简称EVM)是以太坊区块链上的智能合约执行环境,它是以太坊网络的核心组件之一,EVM的设计目标是为智能合约提供一个安全、可靠和可执行的计算环境。
EVM是一个基于堆栈的虚拟机,它使用类似于栈数据结构的内存模型,它具有自己的字节码指令集,这些指令被称为EVM指令,智能合约编写的源代码会被编译成EVM字节码,然后由EVM解释和执行。
核心组件
EVM的架构包括以下几个主要组件:
- 栈(Stack):EVM使用一个栈来存储和操作数据,栈是一个后进先出(LIFO)结构,用于执行指令时的临时数据存储
- 存储(Storage):EVM提供了一个持久化的键值存储,用于智能合约的状态管理,合约可以使用存储来存储和检索数据,这些数据会在区块链上永久保存
- 存储器(Memory):EVM提供了一个临时的内存空间,用于存储临时数据和中间结果,存储器是一个字节数组,用于执行复杂的操作和计算
- 指令集(Instruction Set):EVM具有自己的字节码指令集,包括各种操作,如数学运算、逻辑运算、内存操作、存储操作等,智能合约的字节码由这些指令组成,EVM按照指令的顺序执行
- 异常处理(Exception Handling):EVM能够处理各种异常情况,例如:栈溢出、整数溢出、无效指令等。当异常发生时,EVM会中止合约的执行并回滚到之前的状态
- 没有访问外部环境:EVM是一个封闭的执行环境,它不能直接访问外部的网络、文件系统或其他资源,所有的交互都需要通过以太坊网络进行,例如发送交易或接收消息
EVM指令
EVM(以太坊虚拟机)是以太坊区块链上智能合约的执行环境,它定义了一套指令集,称为EVM指令,这些指令被以太坊网络上的节点用于执行智能合约的操作和计算,EVM执行的是字节码,而由于操作码被限制在一个字节以内,所以EVM指令集最多只能容纳256条指令,目前EVM已经定义了100多条指令,还有100多条指令可供以后扩展,这100多条指令包括算术运算指令,位运算指令,比较指令,密码学计算指令,栈、storage、memory、操作指令,跳转指令,区块、智能合约相关指令等内容
源码分析
指令解析
这里我们对EVM中常见的几种指令进行简单介绍,不做过多的扩展,属实是因为指令太多了(心累.jpeg)
算术运算(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" //符号位扩展
加密运算
加密计算支持SHA3(Secure Hash Algorithm 3),它是密码学中的一种哈希函数,它是美国国家标准与技术研究院(NIST)于2015年发布的标准算法,SHA-3是SHA-2算法家族的后继者,旨在提供更高的安全性和更好的性能,SHA-3基于Keccak算法,它是由比利时密码学家设计的,相对于SHA-2,SHA-3采用了不同的设计原理和算法结构,SHA-3使用了一个称为"海绵结构"的非常灵活的构造方式,可以处理各种不同长度的输入,并产生固定长度的哈希值,SHA-3的应用领域广泛,包括数字签名、消息认证码、随机数生成、密码学协议等,它被广泛应用于密码学安全领域,以确保数据的完整性和安全性
SHA3: "SHA3"
按位运算
下面的代码包含了一些基本的按位运算操作符号,主要包括与、或、异或、左移、右移、取模运算
// 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",
存储执行
EVM的存储指令主要用于栈操作数据,包括:数据元素弹出、加载数据操作、复制栈顶数据到内存、跳转指令等
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"
栈操作类
下面的是压栈、复制、交换操作指令:
// 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",
交易处理
每当用户在钱包客户端或者命令行交互端发起交易请求时,在底层都会调用到sendTx方法,随后将交易添加到本地交易池中,这样交易就可以被进一步处理和广播到整个以太坊网络上的其他节点
// filedir:go-ethereum-1.10.2\eth\api_backend.go L229
func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
return b.eth.txPool.AddLocal(signedTx)
}
下面的AddLocals方法用于批量添加交易并处理相关操作,而AddLocal方法是对AddLocals的封装,用于添加单个本地交易,这些方法确保交易满足验证条件并将它们添加到交易池中以供进一步处理和广播
// filedir:go-ethereum-1.10.2\core\tx_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.2\miner\worker.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.2\miner\worker.go L750
func (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.2\miner\worker.go L736
func (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.2\core\state_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,根据链配置和虚拟机配置的不同,选择相应的解释器并将其添加到EVM的解释器列表中,最后返回创建的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.2\core\state_processor.go L92
func 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.2\core\state_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来完成的
// filedir:go-ethereum-1.10.2\core\state_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
}
由于篇幅过长,下篇再做补充解析~