以太坊虚拟机(EVM)工作原理深入剖析
文章前言
以太坊虚拟机(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操作指令
- 跳转指令
- 区块、智能合约相关指令
指令解析
算术运算(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算法家族的后继者,旨在提供更高的安全性和更好的性能。
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方法,随后将交易添加到本地交易池中,这样交易就可以被进一步处理和广播到整个以太坊网络上的其他节点。
func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
return b.eth.txPool.AddLocal(signedTx)
}
交易池处理
AddLocals方法用于批量添加交易并处理相关操作,而AddLocal方法是对AddLocals的封装,用于添加单个本地交易。这些方法确保交易满足验证条件并将它们添加到交易池中以供进一步处理和广播。
// 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.
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]
}
交易验证
对交易进行检查,主要包括对已知的交易进行过滤,检查交易的有效性并将未知的交易添加到交易池中,最后返回一个错误对象数组,表示每个交易的处理结果。
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将需要打包的交易逐个打包进区块,并根据本地和远程交易将其分别提交到矿工,随后矿工之后会将交易队列中的交易打包进区块。
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)开始执行交易:
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()处理交易,它会将给定的交易应用到当前的区块链状态中并将交易和交易收据添加到当前区块的列表中,然后返回交易的日志列表作为结果。
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。
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)
}
创建EVM实例
调用vm.NewEVM()来创建一个EVM,根据链配置和虚拟机配置的不同,选择相应的解释器并将其添加到EVM的解释器列表中,最后返回创建的EVM实例。
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) {
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过滤器。
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)