区块执行:BlockChain、StateDB 与 EVM
前置知识
- ›第 1-2 篇:架构与启动流程
- ›以太坊状态模型(账户、存储、状态 trie)
- ›Merkle Patricia Trie 基础知识
区块执行:BlockChain、StateDB 与 EVM
这里才是以太坊真正运转的地方。此前介绍的一切——CLI 框架、Node 容器、服务注入——都是为了支撑同一件事:执行区块,推进世界状态。本文将完整追踪一个区块从到达到状态提交的全过程,重点覆盖三大子系统:负责追踪规范链的 BlockChain 管理器、在内存中持有世界状态的 StateDB,以及执行合约字节码的 EVM。
BlockChain:规范链管理器
BlockChain 结构体是链状态的核心协调者。它负责管理规范链、处理区块插入与重组,并维护最近访问数据的 LRU 缓存:
type BlockChain struct {
chainConfig *params.ChainConfig
cfg *BlockChainConfig
db ethdb.Database
// ... caches, trie database, snapshot tree, etc.
}
BlockChainConfig 结构体(注意不要与 params.ChainConfig 混淆)控制运行时行为,包括 trie 缓存限制、快照设置、归档模式、状态裁剪策略以及 VM 配置。这正是 eth.New() 根据 CLI 参数和协议默认值组装出来的配置对象。
几个缓存常量揭示了性能优先级的考量:
const (
bodyCacheLimit = 256
blockCacheLimit = 256
receiptsCacheLimit = 32
txLookupCacheLimit = 1024
)
交易查找缓存最大(1024 条),原因在于 eth_getTransactionByHash 是调用频率最高的 RPC 方法之一。
classDiagram
class BlockChain {
-chainConfig *ChainConfig
-cfg *BlockChainConfig
-db ethdb.Database
-triedb *triedb.Database
-snaps *snapshot.Tree
-bodyCache LRU
-blockCache LRU
-txLookupCache LRU
+InsertChain(blocks) error
+CurrentBlock() *Header
+StateAt(root) (*StateDB, error)
+GetBlock(hash, number) *Block
}
class BlockChainConfig {
+TrieCleanLimit int
+TrieDirtyLimit int
+StateScheme string
+ArchiveMode bool
+SnapshotLimit int
+VmConfig vm.Config
+triedbConfig() *triedb.Config
}
BlockChain --> BlockChainConfig
triedbConfig() 方法是区块链层与存储层之间的关键桥梁——它将 BlockChainConfig 的参数转换为对应的 triedb.Config,并在基于哈希和基于路径的存储方案之间做出选择。我们将在第 4 篇中深入探讨这一机制。
共识引擎:验证与执行的分离
consensus.Engine 接口将区块验证与区块执行清晰地分开。这一分离至关重要——验证负责检查区块是否可能合法(难度值正确、封印有效、叔块引用合规),而执行则将区块应用于状态,产生下一个世界状态。
classDiagram
class Engine {
<<interface>>
+Author(header) address, error
+VerifyHeader(chain, header) error
+VerifyHeaders(chain, headers) chan, chan
+VerifyUncles(chain, block) error
+Prepare(chain, header) error
+Finalize(chain, header, state, body)
+FinalizeAndAssemble(chain, header, state, body, receipts) block, error
+Seal(chain, block, results, stop) error
}
class Beacon {
-ethone Engine
}
class Clique {
-config *CliqueConfig
}
class EthashFaker {
}
Beacon ..|> Engine
Clique ..|> Engine
EthashFaker ..|> Engine
Beacon --> Clique : wraps
Beacon --> EthashFaker : wraps
合并(Merge)之后,所有共识引擎都由 beacon.New() 包装。Beacon 引擎负责处理权益证明相关的逻辑,同时将合并前的逻辑委托给内层引擎。由于 Geth 现在只支持合并后的网络(在 CreateConsensusEngine 中强制执行),TerminalTotalDifficulty 必须始终被设置。
状态执行流水线
执行流水线是区块转化为状态变更的核心路径。整个过程从 StateProcessor.Process() 开始,它会遍历区块中的每一笔交易:
flowchart TD
A["StateProcessor.Process(block, statedb)"] --> B["Apply pre-execution system calls<br/>(beacon root, parent hash)"]
B --> C["For each transaction:"]
C --> D["TransactionToMessage()"]
D --> E["ApplyTransactionWithEVM()"]
E --> F["State Transition:<br/>intrinsic gas → EVM execution → refunds"]
F --> G["Generate receipt"]
G --> C
C --> H["postExecution()<br/>(withdrawals, requests)"]
H --> I["Finalize via consensus engine"]
I --> J["Return receipts, logs, gas used"]
函数首先执行系统调用——处理 beacon 区块根(EIP-4788)和父区块哈希(EIP-2935,适用于 Prague 和 Verkle)。随后遍历所有交易,通过 ApplyTransactionWithEVM 逐一处理。整个区块共享同一个 EVM 实例,在交易之间只替换 TxContext,不重新创建 EVM。
ExecutionResult 记录每笔交易的执行结果:
type ExecutionResult struct {
UsedGas uint64
MaxUsedGas uint64
Err error
ReturnData []byte
}
提示: 调试 EVM 执行问题时,要注意
ExecutionResult.Err并不是"出了什么问题"意义上的 Go 错误——它是协议层面的执行结果。如果返回ErrExecutionReverted且ReturnData不为空,说明合约是主动 revert 的,ReturnData中包含 revert 的原因数据。
IntrinsicGas 函数在 EVM 开始执行之前计算基础 gas 成本,考虑因素包括:创建合约还是调用合约、calldata 中零字节与非零字节的成本、访问列表条目,以及授权列表条目。EIP-2028 降低 calldata 成本和 EIP-3860 限制 initcode 大小,都是在这里得到执行的。
StateDB:内存中的世界状态
StateDB 是以太坊世界状态的内存缓存层。它位于 EVM 与持久化 trie 之间,提供高速读写能力,同时追踪所有修改以支持回滚:
type StateDB struct {
db Database
prefetcher *triePrefetcher
reader Reader
trie Trie
originalRoot common.Hash
stateObjects map[common.Address]*stateObject
stateObjectsDestruct map[common.Address]*stateObject
mutations map[common.Address]*mutation
refund uint64
// journal for revert snapshots, logs, etc.
}
几个关键的设计决策值得关注:
-
惰性 trie 解析 ——
trie字段采用"首次访问时才解析"的策略,即在真正读取状态之前,不会加载实际的 Merkle trie 根节点。 -
对象追踪 ——
stateObjects持有正在被修改的账户对象;stateObjectsDestruct追踪已销毁的账户;mutations在交易边界记录账户级别的变更。 -
基于 journal 的快照 —— StateDB 通过 journal 模式创建回滚点。当 EVM 执行到
REVERT操作码时,journal 会向前回放,撤销当前调用帧内的所有状态变更。 -
预取机制 ——
triePrefetcher在后台 goroutine 中提前加载 trie 节点,减少执行过程中因等待磁盘读取而产生的阻塞。
classDiagram
class StateDB {
-db Database
-trie Trie
-stateObjects map~Address → stateObject~
-mutations map~Address → mutation~
-refund uint64
+GetBalance(addr) uint256
+SetState(addr, key, value)
+CreateAccount(addr)
+Snapshot() int
+RevertToSnapshot(id)
+Commit(block, collectLeaf) (Hash, error)
}
class stateObject {
-address Address
-data types.StateAccount
-code []byte
-dirtyStorage Storage
-originStorage Storage
}
StateDB o-- stateObject
EVM 架构:基于跳转表的解释器
EVM 是以太坊计算模型的核心。Geth 将其实现为一个跳转表驱动的解释器——一个紧凑的主循环,每次取出一个操作码,在包含 256 个条目的跳转表中查找对应的处理函数,然后执行。
EVM 结构体携带了所有执行上下文:
type EVM struct {
Context BlockContext // Block-level: coinbase, gas limit, block number, time
TxContext // Tx-level: origin, gas price, blob hashes
StateDB StateDB // World state access
table *JumpTable // Fork-specific opcode handlers
depth int // Call stack depth
chainConfig *params.ChainConfig
chainRules params.Rules
Config Config // VM config: tracer, extra EIPs
}
BlockContext 与 TxContext 的拆分是有意为之的。区块级上下文(coinbase、时间戳、区块号)在同一区块内的所有交易中保持不变,而交易级上下文(发送方地址、gas 价格)则随每笔交易切换。这样一来,NewEVM 每个区块只需调用一次,在交易之间通过 SetTxContext 替换上下文即可。
Run() 方法是解释器的主循环:
flowchart TD
START["Run(contract, input, readOnly)"] --> CHECK["Code empty?"]
CHECK -->|yes| RET_NIL["Return nil, nil"]
CHECK -->|no| INIT["Create Memory, Stack, ScopeContext"]
INIT --> LOOP["Main Loop"]
LOOP --> FETCH["op = contract.GetOp(pc)"]
FETCH --> LOOKUP["operation = jumpTable[op]"]
LOOKUP --> VALIDATE["Check stack bounds"]
VALIDATE --> GAS["Deduct constantGas"]
GAS --> DYNGAS["Calculate & deduct dynamicGas"]
DYNGAS --> EXEC["Execute operation.execute()"]
EXEC --> NEXT["Advance pc"]
NEXT --> LOOP
EXEC -->|STOP/RETURN/REVERT| EXIT["Return result"]
跳转表中的每个条目都是一个 operation 结构体:
type operation struct {
execute executionFunc
constantGas uint64
dynamicGas gasFunc
minStack int
maxStack int
memorySize memorySizeFunc
undefined bool
}
这一设计将每个操作码的所有行为——执行函数、gas 成本、栈要求、内存大小计算——全部收纳在同一个结构体中。新增一个操作码,只需在表中添加一条记录即可。
分叉分层:新 EIP 如何添加操作码
各分叉专属的指令集通过分层叠加的方式构建。每个新分叉复制上一分叉的指令表,再对特定条目进行覆盖。jump_table.go 中的链条从 Frontier 出发,一直延伸到 Amsterdam:
var (
frontierInstructionSet = newFrontierInstructionSet()
homesteadInstructionSet = newHomesteadInstructionSet()
// ... each one builds on the previous
pragueInstructionSet = newPragueInstructionSet()
osakaInstructionSet = newOsakaInstructionSet()
amsterdamInstructionSet = newAmsterdamInstructionSet()
)
以 newAmsterdamInstructionSet() 为例,它复制 Osaka 的指令集,并启用 EIP-7843(SLOTNUM 操作码)和 EIP-8024:
func newAmsterdamInstructionSet() JumpTable {
instructionSet := newOsakaInstructionSet()
enable7843(&instructionSet)
enable8024(&instructionSet)
return validate(instructionSet)
}
NewEVM() 被调用时,会根据 chainRules 选择正确的指令集。chainRules 是根据 ChainConfig 在当前区块号和时间戳下推导出的一组布尔标志,选择逻辑使用逆时间顺序的 switch 语句——最新的分叉最先被检查。
提示: 要追踪某个 EIP 的具体实现,可以直接搜索对应的
enable函数(例如enable4844)。这些函数会原地修改跳转表,为每个受影响的操作码设置execute、constantGas、dynamicGas以及栈参数。
这条执行流水线——BlockChain → StateProcessor → EVM → StateDB——就是 Geth 的核心所在。但所有这些状态在提交之后,究竟存储到了哪里?这正是第 4 篇要回答的问题:我们将追踪数据如何从内存中的 StateDB,经由 Merkle Patricia Trie,最终落盘到键值存储中。