---
id: 21
title: "EVM 到底是什么：从分布式账本到分布式状态机"
slug: 0-1-what-is-evm
date: 2026/09/18
summary: 把以太坊当成一本记录转账的账本，会漏掉它最关键的部分：账户存储里可任意读写的变量。依据黄皮书的状态转移函数 Υ(σ, T)，以太坊是一个分布式状态机，EVM 是这套状态转移的执行规范。全网逐位重放又给执行语义带来了一组硬约束。
keywords: EVM,世界状态,状态转移函数,确定性执行,Yellow Paper
heroImage: /images/community-bg.png
---

在链上转 100 个 USDT（ERC-20 代币）不会新增一条“甲付给乙 100 USDT”的条目。实际发生的是合约里 `balanceOf` 映射的取值被改写：调用方减少 100，接收方增加 100；这笔改写最终落在哪些存储槽由合约写法与编译器决定，协议只约束 `balanceOf` 返回什么。同时，一笔事件日志（log）被追加进交易收据，日志是给链下索引器消费的副产品，余额的真相只在存储的当前值里。

账本类比在这里失效。账本记录已经发生的事，读者累加历史得出结论；以太坊保存的是一个会被持续覆盖的“当前世界”，交易只把它推向下一版。要回答“EVM 到底是什么”，先要把这个“当前世界”定义清楚，再谈谁负责改写它。

## 比特币的账本模型够用，因为它的状态结构极其受限

“账本”对比特币是准确的描述。一笔交易的输入引用此前某笔交易的输出，输出在被后续输入花掉之前，保持为未花费交易输出（unspent transaction output，UTXO）。一个 UTXO 只能被花费一次，钱包显示的余额其实是若干 UTXO 的加总。

比特币并非没有状态，UTXO 集合就是状态。但这个状态的结构非常受限。它是一堆只能整体创建、整体花费的“硬币”，没有通用的键值存储，脚本也不保存跨交易存在的变量。于是“账本加未花费输出集合”足以完整描述整个系统：验证一笔交易只需要检查被引用的输出是否存在、条件脚本是否被满足，不必查询任何“合约内部变量”。

以太坊要支持合约持有一个可任意读写的映射，这个映射就不能再当作实现细节。ERC-20 转账是否有效，取决于合约存储中调用者的余额槽当前是几。状态因此从“验证时顺带维护的集合”升级为协议必须作出承诺的一等对象。

## 世界状态：把“此刻的一切”压成 32 字节

以太坊黄皮书把世界状态（world state）定义为一个从地址到账户的映射。每个账户是一个四元组：

- nonce：该地址已发出的交易数，或该合约账户已创建的合约数；
- balance：以 wei 计价的余额；
- storageRoot：该账户存储内容的 Merkle Patricia Trie 根哈希；
- codeHash：该账户代码的 Keccak-256 哈希，代码本身按这个哈希存放在状态数据库里。

这里要区分两样东西。storageRoot 承诺的是账户自己的键值空间，合约作者声明的状态变量最终都落在这里；codeHash 指向的是程序，部署之后不会被某笔交易改写。把“数据”与“代码”用两个字段分别承诺，是后面讨论读写集与冲突检测时反复要用的前提（同系列的 0.5《三类存储别搞混：Memory、Storage 与 Transient Storage》会展开这三类存储各自的归属与寿命）。

所有账户再组织成一棵全局的 Merkle Patricia Trie，其根哈希就是 stateRoot，被写进区块头。也就是说，“此刻整个以太坊长什么样”可以被压缩成 32 个字节，全网只需要对这 32 个字节达成一致。

## 链上保存的是状态根，状态本身留在节点本地

这里有一层常被含糊处理的事实：黄皮书明确写道，世界状态并不存储在区块链上，它由每个节点各自维护，链上只保留 stateRoot 这个承诺。交易与收据会被完整发布，状态不会。

这个分工决定了很多使用上的边界。想知道某个账户此刻的余额，只有两条路：自己重放全部历史交易算出状态，或者向某个节点索要一份可以在 stateRoot 下验证的 Merkle 证明。轻客户端走的是第二条路，它不需要保存全部状态，但需要有人提供证明，而且证明只能说明“在某个 stateRoot 之下这个值是什么”，无法说明这个 stateRoot 是最新的。后者是共识层的职责。

同样由此可以理解归档节点与裁剪节点的差别。历史状态持续累积，保存全部历史状态的节点磁盘成本持续增长；只保留近期状态的节点占用更小、启动更快，代价是无法回答任意历史区块上的状态查询。所谓“链上数据”在工程上要拆成两件事：交易与收据的可用性，状态根的承诺。混在一起谈，后面评估数据可用性与状态膨胀时会失去分辨力。

## 状态转移函数：以太坊规定的是一段函数

黄皮书用一行公式定义交易级语义，记作：

`σ_{t+1} ≡ Υ(σ_t, T)`

其中 σ 是世界状态，T 是一笔交易，Υ 是状态转移函数。换成更容易读的写法就是 `Y(S, T) → S'`。黄皮书紧接着强调，有效状态变更只能通过交易发生，而无效的状态变更远多于有效变更，例如凭空减少某账户余额而不在其他地方等量增加。

读懂这层意思，可以换一个说法：以太坊协议真正规定的内容，是“给定当前状态和一笔交易，下一版状态是什么”；至于“链上存了什么”，那只是这段函数的输入与输出。区块层面同理，黄皮书用 Π 把同一个函数沿交易序列折叠：

`Π(σ, B) ≡ Υ(Υ(σ, T₀), T₁)…`

状态机这个词到这里才落地：状态是 σ，转移规则是 Υ，区块是转移的批次。

## 账本模型与状态机模型的验证成本不同

把两个模型放在一起比较，差异会落到验证方式上，而不只是数据组织方式。

UTXO 交易的输入是显式声明的：每个输入都写明引用哪笔交易的哪个输出。验证者只需在 UTXO 集合里查这些具体条目，不必理解任何全局结构。这让比特币的交易验证天然可以局部化，互不相干的交易能被独立处理。声明式账户模型（例如 Solana 的交易消息里预先列出账户地址，指令按下标引用）走的是同一条思路。

账户模型里，交易的读写集合没有写在交易里。合约可以读取任意地址的任意存储槽，读到了什么只有执行完才知道。验证者必须持有一份与执行时相同的状态视图，否则结果无从复现。这个差异是后面整条技术路线的起点：读写集推断、乐观并发控制、冲突检测，处理的都是“不知道一笔交易会碰什么”这件事。

以太坊也试过向声明式靠拢。EIP-2930 引入的访问列表（access list）是新交易类型 0x01 的可选字段，交易可以预先声明将访问的地址与存储槽，把它们加入 `accessed_addresses` 与 `accessed_storage_keys` 集合以享受折扣。EIP 正文同时写明，列表之外的访问仍然允许，只是更贵。读写集合因此没有被强制写进交易，上面这条结论不因它改变。

## EVM 在协议里的位置：规定“怎么改状态”的那份规范

以太坊虚拟机（Ethereum Virtual Machine，EVM）是 Υ 的具体执行规范，内容包括一套指令语义、一套 gas 计量规则、一套异常与终止语义。合约代码是一串字节，只有在被交易或其它合约以消息调用（message call）触发时才进入执行。

EVM 不等于以太坊协议。共识、数据可用性、结算各自是另一层系统，扩容地图一文对此有专门拆解。EVM 只回答一件事：面对一段字节码和一份输入，如何确定性地算出状态变更与返回值。它也决定这个计算要花多少钱，而 gas 计价规则本身就是共识的一部分，可以被硬分叉修改，EIP-2929 把首次访问账户与存储槽的定价显著抬高就是例子。

还有一处容易混淆的地方：EVM 的机器模型不只有“栈”。每次执行上下文都有栈（stack）、内存（memory）、程序计数器（program counter，PC）与 gas 计数器，合约的持久变量则通过 SLOAD/SSTORE 落到账户存储里。栈、内存与执行循环是 0.4 的主题，三类存储的边界是 0.5 的主题。

## 全网重放：为什么结果必须逐位一致

以太坊没有中心执行者。每个全节点独立执行同一批交易，各自算出 stateRoot，再通过共识比较这个值。任何执行细节上的分歧都会直接变成共识失败，所以规范必须细到字节。

由此推出几条硬约束。浮点运算没有进入指令集，只有基于 256 位字长整数的算术。墙钟、进程 ID、真实随机数这类外部输入不能出现在共识路径上；合约能读到的时间戳与区块号来自区块头，是被共识固定的值。

计算还必须可计量且必然终止，否则无法在有限区块里安全运行任意代码。gas 是把“停机问题”改写成“花光预算就停”的办法，它的计量常数也因此属于共识参数而不是性能调优项。一笔普通转账的固有成本是 21000 gas，calldata 里每个零字节计 4 gas、非零字节计 16 gas，这些数字由黄皮书的费用表固定。改动它们与改动区块大小属于同一类协议变更，需要硬分叉或验证者信号，不能由某个客户端自行决定。

确定性还有一个容易被忽略的后果：规范不允许存在“未定义行为”。黄皮书写明 DIV 在除数为 0 时返回 0，而不抛异常。这个选择的理由，是让所有客户端在同一份非法输入上得到同一个结果，与语言设计偏好无关。客户端的单元测试与 execution-spec-tests 规范测试套件，就是用来锁住这类边界行为的。多个独立客户端（geth、revm、evmone 等）并存，本身也是确定性的校验手段：一旦某个实现对边缘情形理解不同，链就会分叉，因此分歧必须在测试阶段被发现。

## 确定性与可预测是两回事

需要区分两个层次的不确定性。执行层是确定的：同样的字节码、同样的输入、同样的状态，必然得到同样的结果。交易能拿到什么结果却不只由执行层决定。同一笔交易放在区块的不同位置，前序状态不同，结果就可能不同。抢跑与三明治的收益来自排序权，执行层本身仍是确定的。

这个区分在评估并行执行方案时很关键。并行改造要守住的是“执行结果与某个串行顺序等价”；至于“结果与交易提交时间无关”，这个目标从来就不成立。

## EVM 不是冻结的规格

把 EVM 当成固定标准会带来兼容性误判。规格随硬分叉演进：PUSH0（EIP-3855，Shanghai）为栈机新增一条推入常量 0 的指令；EIP-2929 重定价了冷热访问；交易层引入了单笔交易 16,777,216 gas 的上限（EIP-7825，随 Fusaka 上线）；EIP-7935 把客户端默认的区块 gas 上限提到 60M。

因此“兼容 EVM”这句话必须带上分叉高度与具体范围才有意义。字节码、预编译合约、JSON-RPC 与工具链各自兼容到什么程度，属于延伸阅读里那篇的范围。

## 这套模型在什么条件下会成为负担

以状态为中心的设计买到了通用计算，代价写在三个地方。

全节点必须长期保存一份随时可读的当前状态，其规模随账户与存储条目增长，持续抬高硬件门槛。

新节点要自行算出 stateRoot，就必须重放历史交易或依赖快照同步，验证成本与历史长度挂钩。

由于每笔交易都可能触碰全局状态的任意位置，经典实现只能逐笔串行执行，这是后面所有并行执行讨论要处理的原始约束。

反过来说，账本类比的失效也不是绝对的。审计、索引、对账这类场景仍然按事件流处理数据，因为收据里的日志本来就是为链下消费设计的。问题只在于不要把这种视角当成协议模型。

## 资料来源

- Ethereum Yellow Paper（Upsilon、Pi、世界状态的存放方式、账户四元组、DIV 语义、gas 费用表）：https://ethereum.github.io/yellowpaper/paper.pdf
- ethereum.org，Ethereum Virtual Machine (EVM)：https://ethereum.org/developers/docs/evm/
- ethereum.org，Understanding the Yellow Paper's EVM Specifications：https://ethereum.org/developers/tutorials/yellow-paper-evm/
- Bitcoin Developer Guide，Transactions（UTXO 只能被花费一次、输入引用具体输出）：https://developer.bitcoin.org/devguide/transactions.html
- Solana Docs，Transactions（交易消息中预先列出账户地址，指令按下标引用）：https://solana.com/docs/core/transactions
- ethereum.org，Nodes and clients（全节点、归档节点与状态裁剪；归档节点才保留全部历史状态）：https://ethereum.org/developers/docs/nodes-and-clients/
- EIP-2929，Gas cost increases for state access opcodes：https://eips.ethereum.org/EIPS/eip-2929
- EIP-2930，Optional access lists（类型 0x01 交易的可选字段；列表之外的访问仍然允许，只是更贵）：https://eips.ethereum.org/EIPS/eip-2930
- EIP-3855，PUSH0 instruction：https://eips.ethereum.org/EIPS/eip-3855
- EIP-7825，Transaction Gas Limit Cap（16,777,216 gas）：https://eips.ethereum.org/EIPS/eip-7825
- EIP-7935，Set default gas limit to 60M：https://eips.ethereum.org/EIPS/eip-7935
- Ethereum Foundation，Fusaka Mainnet Announcement：https://blog.ethereum.org/2025/11/06/fusaka-mainnet-announcement
- go-ethereum，core/vm/instructions.go（opDiv 在除数为 0 时写入 0）：https://github.com/ethereum/go-ethereum/blob/master/core/vm/instructions.go
- ethereum/execution-spec-tests（规范一致性测试套件）：https://github.com/ethereum/execution-spec-tests

## 延伸阅读

- 相关阅读：[《EVM 兼容意味着什么：字节码、预编译、JSON-RPC 与工具链》](/zh/blog/evm-compatibility-explained)
- 相关阅读：[《区块链扩容地图：L1 并行、L2、分片与 DA 各自解决什么》](/zh/blog/blockchain-scaling-map)
- 下一篇：[《为什么单线程 EVM 会卡住 TPS：从历史拥堵到执行模型》](/zh/blog/evm-single-thread-bottleneck)
