区块和交易描述历史;世界状态是按既定历史执行后的当前结果。两者相关,但不是同一份数据。
Ethereum 11.01 · World state
以太坊究竟保存了什么?World State · Account State · Contract Storage
以太坊不是只保存“一串转账记录”。在每个确定的区块之后,它都存在一份可验证的全局快照: 哪些账户存在、各有多少 ETH、执行什么代码、持久化数据是什么。
正式目录主题:世界状态:账户状态与合约状态
0x9c20818aef74…8f083ad4
0xA11c…71cE
0xC0a1…bEef
0x7702…Aa10
nonce、balance、storageRoot、codeHash 足以连接账户身份、原生资产、代码与私有存储。
节点执行局部读写、提交或回滚,再得到一份新的全局承诺;失败交易也可能改变 nonce 与余额。
00 / ORIENTATION
先分清:历史、状态与输出
如果把以太坊只想成一本“谁给谁转了多少钱”的账本,智能合约会显得神秘。 更准确的入口是:以太坊是一台由许多节点共同验证的状态机。
状态机有一个起点,接收严格排序的输入,按协议规则计算结果。对以太坊执行层来说, 起点是父区块之后的世界状态,输入包括区块内有序交易与执行上下文,结果是新的世界状态以及收据、日志、 Gas 消耗等执行输出。Ethereum.org 对区块的说明也强调:节点会重执行执行载荷中的交易, 并核对计算出的全局状态与区块承诺是否一致。[3]
区块与交易
保存有序输入与链的演化路径。通过从创世状态重放合法历史,理论上可以重新得到当前状态。
block Ntx[0] → tx[1] → tx[2]
世界状态
某个确定区块执行完之后,所有存在账户的当前协议状态:余额、nonce、代码承诺与存储承诺。
σN[address] → account收据与日志
交易成功状态、累计 Gas、事件日志等可查询输出。它们很重要,但不属于账户世界状态本身。
receipt.status · logs[]
世界状态是在某个区块边界上,对所有存在账户当前协议状态的确定性快照;
它可以被一个固定长度的 stateRoot 密码学承诺。
01 / WORLD STATE
一张从地址到账户记录的巨大映射
在协议层,可以先把世界状态写成 σ[address] = account。
地址是 20 字节的键;账户记录是值。这个极简模型能解释后面几乎所有“链上数据在哪里”的问题。
地址不等于账户
任意 20 字节值都可以被写成一个地址,但并非每个地址在某一状态中都有账户记录。 “地址存在”通常指它在当前状态里有一条非空账户记录,而不是字符串格式看起来有效。 向一个此前不存在的地址转入非零 ETH,可能让它在状态里出现;一个没有代码、nonce 为零、 balance 也为零的空账户会按协议规则被视为不存在并清理。[9]
同一地址,在不同区块可以有不同状态
查询 latest 得到的是当前链头附近的状态;查询某个历史区块,得到的是该区块边界上的状态。
因此“这个地址有多少 ETH”不是脱离区块高度的永恒事实。严格问题应该是:
在链 ID X、区块 B 所承诺的状态中,这个地址的 balance 是多少?
02 / ACCOUNT RECORD
账户状态只有四个协议字段
无论我们把地址称为“用户账户”还是“合约账户”,当前以太坊账户记录的经典结构都由四项组成:
nonce、balance、storageRoot、codeHash。
[1]
σ[0xC0a1…bEef]
地址是世界状态中的查找键;下面四项才是这个地址在该状态版本中的账户值。
防止重复与维持顺序的计数器。普通 EOA 每发出一笔被执行的交易会推进;合约创建其他合约时也会使用并推进其 nonce。
这个地址拥有的原生 ETH 数量,以 wei 记录。它不包含 ERC-20 余额、NFT 或美元估值。
对该地址私有持久化存储的根承诺。存储可抽象为 256-bit 键到 256-bit 值的映射,默认为空。
账户运行时代码字节的 Keccak-256 哈希。节点据此取得代码;它不是 Solidity 源码、ABI 或源码验证状态。
四个字段分别回答什么?
| 字段 | 最可靠的问题 | 不能直接推出 |
|---|---|---|
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]
Interactive 01
切换三种账户形态
0xC0a1…bEef
合约账户没有用于发起普通交易的私钥。它在被交易或另一个合约调用时执行代码, 代码可以读写该地址自己的 storage,并在规则允许时调用其他账户。
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]
contract Counter {
uint256 public count = 7;
address public owner;
mapping(address => bool) approved;
function increment() external {
count += 1;
}
}
固定大小的状态变量通常从 slot 0 开始按布局规则排列;较小类型可能共享一个 32 字节槽。
mapping 和动态数组大小不可预先确定,因此 Solidity 使用以槽位置和键为输入的 Keccak-256 规则寻找数据位置。
[5]
注意:变量名 count、类型 uint256 与“它位于 slot 0”的解释来自源码和编译布局;
单看原始世界状态只能看到键和值。
Storage、Memory、Calldata 与 Logs 不要混用
跨调用、跨交易保留,属于某个地址,改变后会影响该账户的 storageRoot 与最终世界状态。
只在当前执行上下文中存在,调用结束后不保留;它们帮助计算,但不是账户状态字段。
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 的状态差分
起点:节点读取父区块状态。Alice nonce = 17、balance = 1.250000 ETH;Counter slot 0 = 7。
为什么 revert 后状态根仍可能改变?
revert 回滚的是当前调用执行产生的合约状态写入、转账与日志等可回滚效果;
顶层交易已经消耗的 Gas 仍需支付,发送者 nonce 也已推进。因此 Counter 的 count 可以回到 7,
但 Alice 的 nonce、Alice 的 ETH 余额以及费用接收者余额仍会变化,最终世界状态自然也可能得到新的 state root。
06 / STATE COMMITMENT
stateRoot 不是状态,而是对状态的承诺
全部账户与全部合约槽位远远放不进一个区块头。以太坊把这份巨大状态组织进可验证结构,
最终得到一个 32 字节根值。执行区块中的 state_root 承诺应用该区块全部变化后的全局状态。
[3]
0x9c20818a…3ad4
固定 32 字节任何被承诺数据变化,根都极可能改变
这个根能做什么?
它让执行节点能独立重算并核对区块结果;也让证明系统可以围绕某个根,证明特定账户或存储槽的值确实属于那份状态。 两份完全相同的状态会得到相同的根;哪怕只改一个余额或槽值,沿相关路径重新哈希后,根也会变化。 [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
八个会在后续章节反复踩到的误解
错。地址只是可能的键;在某一状态中没有对应记录时,该账户并不存在。
错。账户状态由网络状态承诺;钱包保管密钥、组织请求并显示查询结果。
错。账户 balance 只记录原生 ETH;代币余额通常存在代币合约 storage。
错。它承诺运行时代码字节;源码、ABI 与验证元数据是额外层。
错。交易、calldata、收据和日志各有自己的数据位置与承诺;memory 只在执行时存在。
错。合约写入会回滚,但顶层发送者 nonce 与实际 Gas 费用仍会结算。
已过时。EIP-7702 允许密钥控制账户保存受协议识别的委托指示符并执行委托代码。
错。它是固定长度承诺;没有节点数据或证明,根本身不能展开成全部状态。
一个实用的诊断顺序
当你看到“余额不对”“调用结果变了”“合约数据读不到”时,按顺序核对: 网络与 chain ID → 区块标签 → 地址 → 读取的是 balance、code 还是 storage → 槽布局或 ABI → 是否经过代理/7702 委托 → 查询的是已确认状态还是 pending 模拟。 多数“链上数据矛盾”其实是查询边界不同。
09 / RECAP & QUIZ
把整课压缩成一条因果链
以太坊的“当前现实”不是某一家公司数据库里的一行真相,而是所有合规执行节点都能依据同一历史重算、
并由区块 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 字节密码学承诺。
官方与标准原文
-
Ethereum.org · Ethereum accounts账户类型、nonce、balance、codeHash、storageRoot 与钱包边界。
-
Ethereum Whitepaper世界状态、账户模型与状态转换的原始设计说明;阅读时应结合后续 EIP。
-
Ethereum.org · Blocks执行载荷、交易重执行,以及区块中 state_root 的作用。
-
Ethereum.org · Merkle Patricia Trie世界状态的确定性、可验证性与根承诺概览;具体结构留待后续课。
-
Solidity docs · Layout of State Variables in Storage状态变量的槽布局、紧密打包、mapping 与动态数组位置规则。
-
Solidity docs · Storage, Memory and the Stack账户持久化 storage 与执行期 memory、stack 的官方边界。
-
EIP-7702 · Set Code for EOAsSetCode 交易、委托指示符、执行语义、nonce 与安全边界。
-
Ethereum Foundation · Pectra Mainnet AnnouncementPectra 主网激活时间及 EIP-7702 对账户可编程性的意义。
-
EIP-161 · State trie clearing空账户与不存在账户的规则,以及交易结束时的清理语义。
-
Ethereum.org · Pectra 7702 guidelines7702 委托的当前开发者指南、交互边界与安全注意事项。
-
Ethereum.org · JSON-RPC API读取余额、nonce、代码、存储与模拟调用的标准接口。
-
Ethereum · Execution Layer Specifications用可读代码描述当前执行层状态与交易处理规则的一手规范仓库。
11.01 · Complete
历史告诉你发生过什么;状态告诉你,以太坊现在是什么。
你现在可以把任意“链上数据”放回正确层次:地址查到账户四字段,合约变量落进私有 storage, 交易把旧状态推进为新状态,stateRoot 则对全部结果作紧凑承诺。