Ethereum 11.01 · World state

以太坊究竟保存了什么?World State · Account State · Contract Storage

以太坊不是只保存“一串转账记录”。在每个确定的区块之后,它都存在一份可验证的全局快照: 哪些账户存在、各有多少 ETH、执行什么代码、持久化数据是什么。

正式目录主题:世界状态:账户状态与合约状态

  • 约 40 分钟
  • 2 个互动实验
  • 6 道校准题
Execution state · after block N snapshot σN
State commitment 0x9c20818aef74…8f083ad4
Alice · EOA 0xA11c…71cE
nonce18 balance1.249380 ETH storageRootempty codeHashempty code
Counter · Contract 0xC0a1…bEef
nonce1 balance0.400000 ETH storageRoot0x91bf…7ac3 codeHash0x3f2a…9d61
Smart EOA · 7702 0x7702…Aa10
nonce43 balance2.800000 ETH storageRoot0x6a10…2c40 codeHashdelegation
这是教学化的三条账户记录。世界状态可以理解为“地址 → 账户状态”的巨大映射;根哈希只承诺整份状态,并不等于状态本身。
01 / SNAPSHOT 状态回答“现在是什么”

区块和交易描述历史;世界状态是按既定历史执行后的当前结果。两者相关,但不是同一份数据。

02 / RECORD 每个账户只有四个协议字段

nonce、balance、storageRoot、codeHash 足以连接账户身份、原生资产、代码与私有存储。

03 / TRANSITION 交易改变的是一小片状态

节点执行局部读写、提交或回滚,再得到一份新的全局承诺;失败交易也可能改变 nonce 与余额。

00 / ORIENTATION

先分清:历史、状态与输出

如果把以太坊只想成一本“谁给谁转了多少钱”的账本,智能合约会显得神秘。 更准确的入口是:以太坊是一台由许多节点共同验证的状态机

状态机有一个起点,接收严格排序的输入,按协议规则计算结果。对以太坊执行层来说, 起点是父区块之后的世界状态,输入包括区块内有序交易与执行上下文,结果是新的世界状态以及收据、日志、 Gas 消耗等执行输出。Ethereum.org 对区块的说明也强调:节点会重执行执行载荷中的交易, 并核对计算出的全局状态与区块承诺是否一致。[3]

一句话工作定义

世界状态是在某个区块边界上,对所有存在账户当前协议状态的确定性快照; 它可以被一个固定长度的 stateRoot 密码学承诺。

01 / WORLD STATE

一张从地址到账户记录的巨大映射

在协议层,可以先把世界状态写成 σ[address] = account。 地址是 20 字节的键;账户记录是值。这个极简模型能解释后面几乎所有“链上数据在哪里”的问题。

address160-bit lookup key
σ[address]account record or absent
Y(T) →
σ′next world state

地址不等于账户

任意 20 字节值都可以被写成一个地址,但并非每个地址在某一状态中都有账户记录。 “地址存在”通常指它在当前状态里有一条非空账户记录,而不是字符串格式看起来有效。 向一个此前不存在的地址转入非零 ETH,可能让它在状态里出现;一个没有代码、nonce 为零、 balance 也为零的空账户会按协议规则被视为不存在并清理。[9]

同一地址,在不同区块可以有不同状态

查询 latest 得到的是当前链头附近的状态;查询某个历史区块,得到的是该区块边界上的状态。 因此“这个地址有多少 ETH”不是脱离区块高度的永恒事实。严格问题应该是: 在链 ID X、区块 B 所承诺的状态中,这个地址的 balance 是多少?

02 / ACCOUNT RECORD

账户状态只有四个协议字段

无论我们把地址称为“用户账户”还是“合约账户”,当前以太坊账户记录的经典结构都由四项组成: noncebalancestorageRootcodeHash[1]

四个字段分别回答什么?

字段 最可靠的问题 不能直接推出
nonce 下一个普通交易需要使用哪个顺序号?该账户创建合约时的地址推导处于什么计数位置? 不能永远当作“用户一生发了多少次交易”;现代授权与合约创建语义会让细节更丰富。
balance 该地址在此状态版本拥有多少 wei? 不能推出 ERC-20、NFT、质押衍生品、L2 余额或法币价值。
storageRoot 该地址私有持久化 storage 的完整内容被哪个根值承诺? 根值本身不能直接显示变量名、Solidity 类型或每个槽的含义。
codeHash 此账户当前关联的运行时代码字节是否与某份代码完全一致? 不能直接给出源代码、ABI、开发者身份、安全性或升级权限。

03 / ACCOUNT TYPES

EOA、合约账户,以及 7702 之后的灰区

传统教学把账户分成两类:由私钥控制、能发起顶层交易的 EOA;没有私钥、收到调用才执行代码的合约账户。 这仍是必要基础,但在 2025 年 Pectra 激活 EIP-7702 后,“EOA 一定没有代码”已不再是安全假设[7][8]

EIP-7702 到底改变了什么?

EIP-7702 新增了一种交易类型,允许一个由密钥授权的账户把 23 字节 0xef0100 || delegateAddress 写成“委托指示符”。当该账户被执行时,EVM 会取得委托目标的代码, 但在授权账户自己的地址、余额与存储上下文中运行。账户的 codeHash 因而不再是空代码哈希, 私钥仍保留更换或清除委托的最终能力。[7]

这不是把完整实现代码复制进 EOA,也不是自动把单签账户变成真正的多签。它更像在账户状态里放入一个受协议识别的代码指针。 对应用开发者的直接后果是:不能再用“code.length == 0”或 “msg.sender == tx.origin”简单证明调用者一定是没有可编程行为的人类账户。 Ethereum.org 的 7702 指南也明确要求开发者按“任何参与者都可能具备合约行为”来设计。[10]

04 / CONTRACT STATE

合约的变量,最终落在 256-bit 槽里

智能合约的“记忆”不是 Solidity 变量名本身,而是一份属于该地址的持久化键值空间。 EVM 将其抽象为 2²⁵⁶ 个可能的槽:每个键和值都是 256 bit。 Solidity 编译器再把高级类型按规则映射到这些槽。[5]

Counter.solsource model
contract Counter {
  uint256 public count = 7;
  address public owner;
  mapping(address => bool) approved;

  function increment() external {
    count += 1;
  }
}
Persistent storageconceptual slots
slot 0count7
slot 1owner · left padded0xA11c…
slot 2mapping anchor · no direct value
keccak(k,2)approved[k]1

固定大小的状态变量通常从 slot 0 开始按布局规则排列;较小类型可能共享一个 32 字节槽。 mapping 和动态数组大小不可预先确定,因此 Solidity 使用以槽位置和键为输入的 Keccak-256 规则寻找数据位置。 [5] 注意:变量名 count、类型 uint256 与“它位于 slot 0”的解释来自源码和编译布局; 单看原始世界状态只能看到键和值。

Storage、Memory、Calldata 与 Logs 不要混用

PERSISTENT Storage

跨调用、跨交易保留,属于某个地址,改变后会影响该账户的 storageRoot 与最终世界状态。

EPHEMERAL Memory / Stack

只在当前执行上下文中存在,调用结束后不保留;它们帮助计算,但不是账户状态字段。

INPUT & OUTPUT Calldata / Logs

Calldata 是调用输入;事件日志写入交易收据体系。两者都不是合约持久化 storage。

“我的 USDC 在钱包里”为什么只是界面语言?

原生 ETH 余额直接在每个账户的 balance 字段里;ERC-20 代币余额通常保存在 代币合约自己的 storage 中,例如概念上的 balances[userAddress] = amount。钱包只是查询相应合约并把结果展示出来。 NFT 所有权也通常是 NFT 合约 storage 里的映射,而不是用户账户多出一个“资产列表”字段。

05 / STATE TRANSITION

Alice 调用 increment(),到底改了什么?

一笔交易通常只触碰巨大世界状态中的很小一部分。最好的理解方法不是背“整条链更新了”, 而是逐项列出读集、写集,以及成功与回滚的边界。

假设 Alice 的 nonce 是 17、余额为 1.250000 ETH;Counter 的 slot 0 是 7。 Alice 发送一笔 value = 0 的交易调用 increment()。 下面教学数字假设实际使用 31,000 gas、有效 gas 价格为 20 gwei,其中 base fee 18 gwei、 priority fee 2 gwei,因此总费用是 0.000620 ETH。

Interactive 02

成功与 revert 的状态差分

illustrative numbers
00读取父状态
01验证并预扣
02执行与日志化写入
03提交或回滚
04结算并承诺

起点:节点读取父区块状态。Alice nonce = 17、balance = 1.250000 ETH;Counter slot 0 = 7。

Alice.nonce
17
18
Alice.balance
1.250000 ETH
1.249380 ETH
Counter.slot[0]
7
8 · committed
Counter.codeHash
0x3f2a…9d61
unchanged
Fee recipient
F
F + 0.000062 ETH
Receipt status1 · success
Base fee burned0.000558 ETH
Post-state root0x9c20…3ad4

为什么 revert 后状态根仍可能改变?

revert 回滚的是当前调用执行产生的合约状态写入、转账与日志等可回滚效果; 顶层交易已经消耗的 Gas 仍需支付,发送者 nonce 也已推进。因此 Counter 的 count 可以回到 7, 但 Alice 的 nonce、Alice 的 ETH 余额以及费用接收者余额仍会变化,最终世界状态自然也可能得到新的 state root。

06 / STATE COMMITMENT

stateRoot 不是状态,而是对状态的承诺

全部账户与全部合约槽位远远放不进一个区块头。以太坊把这份巨大状态组织进可验证结构, 最终得到一个 32 字节根值。执行区块中的 state_root 承诺应用该区块全部变化后的全局状态。 [3]

这个根能做什么?

它让执行节点能独立重算并核对区块结果;也让证明系统可以围绕某个根,证明特定账户或存储槽的值确实属于那份状态。 两份完全相同的状态会得到相同的根;哪怕只改一个余额或槽值,沿相关路径重新哈希后,根也会变化。 [4]

这个根不能单独做什么?

单看根值不能还原全部账户,也不能告诉你“哪个字段变了”;它还需要底层节点数据或一份证明路径。 stateRoot 也不是 block hash、transactions root 或 receipts root: 它们分别承诺不同对象。下一课讲 Merkle Tree 的验证思想,再下一课打开状态树、合约存储树与交易树的具体关系。

07 / READING STATE

钱包和区块浏览器怎样读到这些数据?

用户界面不会直接“打开世界状态”。它通常向执行客户端的 JSON-RPC 接口提出一个具体问题, 并指定区块标签或区块号。理解每个接口读取哪一层,就不容易被界面文案误导。

JSON-RPC 主要读取 关键边界
eth_getBalance 账户记录的原生 balance 不返回 ERC-20 或 NFT;这些要调用相应代币合约。
eth_getTransactionCount 账户 nonce pending 视图还可能考虑本地交易池,不完全等于已入块状态。
eth_getCode 某地址的代码字节 返回代码或 7702 委托指示符,不是 codeHash、源码或 ABI。
eth_getStorageAt 某地址的一个原始 storage 槽 调用者必须知道槽位置和编码,节点不会替你恢复变量名。
eth_getProof 账户与指定存储槽的值及证明 证明要针对一个具体区块状态根验证;不是脱离区块的永久证明。
eth_call 基于选定状态执行本地模拟 能读取并临时执行,但不会把结果提交成网络状态。

区块浏览器会把 RPC、索引数据库、已验证源码、代币列表与价格源组合成更易读的页面。 “Token Holdings”往往是浏览器对多个合约状态的索引结果,不是账户记录中天然存在的第五个字段。 钱包也类似:它是签名与交互界面,不是链上账户本身。[1]

08 / MISCONCEPTIONS

八个会在后续章节反复踩到的误解

01 / ADDRESS “有地址就一定有账户。”

错。地址只是可能的键;在某一状态中没有对应记录时,该账户并不存在。

02 / WALLET “账户就装在钱包 App 里。”

错。账户状态由网络状态承诺;钱包保管密钥、组织请求并显示查询结果。

03 / TOKENS “balance 包含所有代币。”

错。账户 balance 只记录原生 ETH;代币余额通常存在代币合约 storage。

04 / SOURCE “codeHash 就是 Solidity 源码。”

错。它承诺运行时代码字节;源码、ABI 与验证元数据是额外层。

05 / STORAGE “所有链上数据都在 storage。”

错。交易、calldata、收据和日志各有自己的数据位置与承诺;memory 只在执行时存在。

06 / REVERT “失败交易什么都不改变。”

错。合约写入会回滚,但顶层发送者 nonce 与实际 Gas 费用仍会结算。

07 / EOA “有代码就绝不可能是 EOA。”

已过时。EIP-7702 允许密钥控制账户保存受协议识别的委托指示符并执行委托代码。

08 / ROOT “stateRoot 就是全部状态的压缩文件。”

错。它是固定长度承诺;没有节点数据或证明,根本身不能展开成全部状态。

一个实用的诊断顺序

当你看到“余额不对”“调用结果变了”“合约数据读不到”时,按顺序核对: 网络与 chain ID → 区块标签 → 地址 → 读取的是 balance、code 还是 storage → 槽布局或 ABI → 是否经过代理/7702 委托 → 查询的是已确认状态还是 pending 模拟。 多数“链上数据矛盾”其实是查询边界不同。

09 / RECAP & QUIZ

把整课压缩成一条因果链

01父区块承诺旧世界状态 σ
02地址映射到四字段账户记录
03代码读取余额、nonce 与私有 storage
04交易写入被提交或按边界回滚
05全部变化形成新状态 σ′ 与 stateRoot
最终心智模型

以太坊的“当前现实”不是某一家公司数据库里的一行真相,而是所有合规执行节点都能依据同一历史重算、 并由区块 stateRoot 承诺的世界状态。

六题校准

1. “世界状态”最准确的描述是哪一个?

请选择一个答案。

2. 下列哪一项不是账户记录的四个协议字段之一?

请选择一个答案。

3. Alice 的 USDC 余额通常存在哪里?

请选择一个答案。

4. 顶层交易触发合约 revert 后,哪句话正确?

请选择一个答案。

5. EIP-7702 对传统 EOA 模型最关键的修正是什么?

请选择一个答案。

6. 只拿到一个 stateRoot,可以直接得到什么?

请选择一个答案。

10 / GLOSSARY & SOURCES

术语与一手资料

本课完成第 11 章的第一层:世界状态与账户记录。下一课学习 Merkle Tree 如何高效证明数据完整性; 第三课再进入 State Trie、Storage Trie 与 Transaction Trie 的具体结构。

核心术语

World state
某一执行区块边界上,所有存在账户当前协议状态的确定性映射。
Account state
地址对应的四字段记录:nonce、balance、storageRoot、codeHash。
Address
20 字节查找键;格式有效不代表该地址在某个状态中一定存在账户。
Nonce
用于交易顺序、防重放与合约创建语义的账户计数器。
Balance
账户拥有的原生 ETH 数量,以 wei 计;不含代币合约中的余额。
Storage
属于某地址、跨调用持久化的 256-bit 键值空间。
storageRoot
对某账户完整持久化 storage 内容的根承诺。
codeHash
账户关联运行时代码字节的 Keccak-256 哈希。
EOA
传统上由私钥授权顶层交易的账户;7702 后可带委托指示符与可编程行为。
Contract account
由部署代码控制、收到消息调用时执行的账户,没有用于发起普通交易的私钥。
State transition
依据交易、上下文与协议规则,从旧世界状态计算新世界状态的过程。
stateRoot
执行区块对应用全部交易后世界状态的 32 字节密码学承诺。

官方与标准原文

  1. Ethereum.org · Ethereum accounts账户类型、nonce、balance、codeHash、storageRoot 与钱包边界。
  2. Ethereum Whitepaper世界状态、账户模型与状态转换的原始设计说明;阅读时应结合后续 EIP。
  3. Ethereum.org · Blocks执行载荷、交易重执行,以及区块中 state_root 的作用。
  4. Ethereum.org · Merkle Patricia Trie世界状态的确定性、可验证性与根承诺概览;具体结构留待后续课。
  5. Solidity docs · Layout of State Variables in Storage状态变量的槽布局、紧密打包、mapping 与动态数组位置规则。
  6. Solidity docs · Storage, Memory and the Stack账户持久化 storage 与执行期 memory、stack 的官方边界。
  7. EIP-7702 · Set Code for EOAsSetCode 交易、委托指示符、执行语义、nonce 与安全边界。
  8. Ethereum Foundation · Pectra Mainnet AnnouncementPectra 主网激活时间及 EIP-7702 对账户可编程性的意义。
  9. EIP-161 · State trie clearing空账户与不存在账户的规则,以及交易结束时的清理语义。
  10. Ethereum.org · Pectra 7702 guidelines7702 委托的当前开发者指南、交互边界与安全注意事项。
  11. Ethereum.org · JSON-RPC API读取余额、nonce、代码、存储与模拟调用的标准接口。
  12. Ethereum · Execution Layer Specifications用可读代码描述当前执行层状态与交易处理规则的一手规范仓库。

11.01 · Complete

历史告诉你发生过什么;状态告诉你,以太坊现在是什么。

你现在可以把任意“链上数据”放回正确层次:地址查到账户四字段,合约变量落进私有 storage, 交易把旧状态推进为新状态,stateRoot 则对全部结果作紧凑承诺。

下一课预告 · Merkle Tree 与数据完整性证明