🎬 开场 · 一个新的问题
2013 年冬天。Bitcoin 跑了五年,证明了账本可以去中心化。 Vitalik Buterin 在他公寓里问了下一个问题:
这是 Ethereum 的元命题。Vitalik 写白皮书 · Gavin Wood 把它落成黄皮书。今天我们要解构的,就是黄皮书里那台「世界计算机」。
“Ethereum is a transaction-based state machine.” ——《Ethereum Yellow Paper》
今天的检查表
第一课用「银行职能 8 项」组装出 BTC。这一课用 6 个零件组装出一台世界计算机:
每讲完一节,右上角浮窗自动勾掉对应一项。六项全亮,世界计算机就组装完成。
前章 · BTC 的边界
一段被拒绝的提案
Vitalik 起初没想另起一条链。2013 年他在 Bitcoin Magazine 做编辑,参与过 colored coins(给 satoshi 打”颜色”代表别的资产)和 Mastercoin(在 BTC tx 里夹带元数据)——都是在 BTC Script 微薄能力上”堆砌”。他越做越确信:BTC 需要一个图灵完备的新脚本。
💡 图灵完备 · 能写循环、能写条件分支、能模拟其它任何程序。BTC Script 不是——没循环、有限步骤,这是有意的安全选择。
他向 Bitcoin 社区提交提案。Bitcoin Core 的回应:
回答在工程上没毛病——简洁的脚本是 BTC 至今没出过协议级灾难的核心原因之一。但 Vitalik 由此决定从头造一条新链。几个月后白皮书出版,半年后 Gavin Wood 写出黄皮书。
两种哲学从此分流——BTC “少即是多”,ETH “协议是平台”。两条路都走通了。
BTC Script 差在哪
第一课讲过 BTC Script 的三条限制(故意的):
- 无循环 · 只能跑有限步
- 无持久存储 · 每个 UTXO 的 script 是一次性
- 无外部数据 · 看不到时间、看不到其它 tx
要跑这种程序,三个底层难题躲不掉:
- 🧮 谁来执行复杂程序 → EVM
- 📦 程序状态存哪 → World State
- ⛽ 谁来付计算的钱 → Gas
本课接下来就是一项一项解决这三个问题。
模块 A · 账户模型 vs UTXO
A.1 · 一个生活类比
🧾 现金钱包 · 钱是一叠面额各异的钞票。付 5 块要么递一张 5 元、要么递 10 元拿回找零。
🏦 银行账户 · 钱是一个数字。账号是门牌号,余额就是号码下的数。转账时一边 −x、另一边 +x。
BTC 用前者(UTXO ≈ 链上的钞票)·ETH 用后者(每个地址下挂一个状态)。
A.2 · 为什么 ETH 选了”银行账户”
银行账户比钞票多了两件事,直接决定能不能跑程序:
- 固定门牌号——账号一辈子是它,钱永远住在那。钞票没有”位置”,是流动的。
- 能记任意东西——账号下不只是余额,还能开新栏位记任何字段。钞票只能是钞票。
核心:UTXO 是”钱的状态”,账户是”任意状态”。要跑程序,必须有任意状态。
A.3 · 账户最简版(2 字段)
account = {
address: 0xA17b...ef9 ← 门牌号
balance: 50 ether ← 余额
}
到这一步 ETH 已经能转账。全网维护 mapping[ address → account ],按门牌号 O(1) 查到余额。
💡 ether 与 wei · ETH 的原生货币叫 ether(简称 ETH)。协议层不存浮点数,所有余额用整数——最小单位叫 wei,1 ether = 10¹⁸ wei。
A.4 · 加上”程序住进来”需要的字段
◆ Anatomy of an Account
Account State
keyed by 20-byte address
nonce
发送过的 tx 数(EOA)/ 创建过的合约数(Contract)
balance
账户余额(单位:wei)
storageRoot
该账户存储 MPT 的根哈希 · 指向另一棵树
codeHash
合约字节码的哈希 · EOA 时为空哈希
| 字段 | 为什么需要它 |
|---|---|
nonce | 这账户发过几笔 tx · 防重放 + 派生新合约地址 |
balance | 余额——和 BTC 唯一同名的字段 |
codeHash | 我是合约吗?代码是什么?(EOA 时为空) |
storageRoot | 合约自己存的数据在哪?(EOA 时为空) |
ETH 上两种账户共用这一种结构——EOA 的 codeHash 为空,合约不为空。仅此而已。后面 DeFi 的”乐高积木”,根扎在这种统一抽象里。
A.5 · 互动 · 同一笔转账,两种模型
◆ Same Transfer · Two Models
step 0 / 5
BTC · UTXO
flat set
press play
ETH · Account
k/v map
按 ▶ play 启动 · 或者从上面的步骤列表里直接跳到某一步
- UTXO 侧 · 余额是派生量(unspent 之和)——账本里只有”动作”
- Account 侧 · 余额是字段——账本里直接是”当前状态”
模块 B · 世界状态
本节是全课最重的概念跳跃——慢一点没关系。
B.0 · 前置 · 两把工具
| 数据结构 | ✅ 能力 | ❌ 局限 |
|---|---|---|
| Merkle Tree(上课讲过) | 一个根代表整集合 · log(N) 长度证明 | 按位置排列 · 给 key 找不到 · 稀疏 + 海量 key 时增删代价大 |
| Patricia Trie(前缀压缩) | 按 key 索引 · 公共前缀共享 · 增删只动一条路径 | 节点改了没人察觉 |
◆ How Two Trees Become MPT
stage 0 · flat list
5 个 key
把它们存进字典——要能按 key 查找。
编码细节(branch / extension / leaf · nibble 路径 · RLP)留到后面「深水区 · MPT」展开。
B.1 · 从 BTC 的区块头说起
第一课我们看过 BTC 的区块头——核心字段有两个:
parentHash· 指向上一块的哈希merkleRoot· 本块所有 tx 的 Merkle 根
就这两个就够 BTC 用了——因为 BTC 只做转账,它的”状态”完全可以从交易历史推导出来。想知道 Alice 的余额?扫所有 tx 加减一遍即可。
但这对 ETH 不够。合约要在链上持久持有状态——一次调用就要立刻读出当前余额、当前 storage、当前 nonce。靠扫完整历史去推导?永远跑不动。
所以 ETH 在 BTC 的设计上多承诺了一件事——当前世界的整个状态。这就是 stateRoot。
B.2 · ETH 加了一个字段 · stateRoot
◆ Block Header · BTC → ETH
ETH 在 BTC 的基础上多加了一个 stateRoot · 这是本节的全部主题
BTC · header (selected)
"ledger"
BTC 只承诺 交易—— 状态(谁有多少钱)由扫描历史推导出来。
ETH · header (selected)
"world computer"
ETH 多承诺一个 当前世界状态—— 32 字节锁定所有账户。
stateRoot 是什么?就是 B.0 那种 MPT 的根哈希——这棵 MPT 装着所有账户的所有状态。32 字节的根哈希通过 MPT 的递归引用,唯一确定了链上每一个账户的每一个字段。
这是 ETH 和 BTC 在数据承诺层面的唯一的本质区别。后面所有的状态机制——轻客户端、历史快照、子树共享——都从这一个字段长出来。
B.3 · stateRoot 长在哪 · 三层嵌套
那个 stateRoot 指向的 MPT,是三层嵌套结构:
◆ Three Trees · Drilling Into ETH State
step 1/5
step 1 · L1 · block
区块头 — 三个核心字段 · stateRoot 是 ETH 新增的状态承诺。
B.4 · 状态怎么演化
黄皮书一行公式:
σ' = Υ(σ, T)
↑ ↑ ↑
↑ 当前 state 这一块的 txs
新 state
◆ State Evolution · σ' = Υ(σ, T)
block by block
genesis state · σ₀
创世状态 — 所有账户都还没活动。stateRoot 是空 MPT 的根。
三层关系一句话:区块状态是世界状态在某个时刻的固定值;世界状态是所有账户状态的聚合。
B.5 · MPT 复杂的代价 = 三个回报
MPT 实现复杂、占空间,但换来三件硬核能力:
- 轻客户端可验证 · 手机只下载 header 也能 Merkle 证明余额
- 历史快照零成本 · stateRoot 直接锁定那一刻全部账户
- 分叉切换 O(1) · 切 stateRoot 指针就行
B.5.1 · 轻客户端怎么”不信任地”验证一个账户余额
问题:手机这种轻客户端没法下载整棵 MPT(ETH 状态有几百 GB)。它只能存 block header——里面有 stateRoot 这一个 32 字节哈希。然后向某个全节点问”Alice 的余额是多少”。但它凭什么相信全节点不撒谎?
协议:全节点回应时不仅给余额,还附上一条”Merkle 证明”——从 Alice 的叶子到根这条路径上每一层”兄弟节点”的哈希。
验证:轻客户端用收到的证明自己往上算哈希——叶子哈希 ⊕ 兄弟 → 父节点哈希 → 再 ⊕ 兄弟 → 爷爷哈希 → … → 根哈希。算出来的根哈希如果和它早就拥有的 stateRoot 一致,就证明那个余额没被伪造。
关键洞察:轻客户端不需要信任任何节点——它信任的是哈希函数。任何节点想骗它,都得在密码学上伪造一条能匹配 stateRoot 的路径,那相当于找 keccak256 的碰撞——不可能。
◆ Light Client · Trustless Merkle Proof
step 1/5
step 1 · setup
轻客户端只存 block header(里面有 stateRoot),不存整棵 MPT。它想查 Alice 的余额,但不能凭空相信任何全节点的回复。
B.5.2 · 子树共享 → 历史快照 + 分叉切换
第 2、3 条回报靠同一个机制——MPT 的子树共享:
◆ Hash Propagation · Single Tree, Diff View
step 1/4
step 1 · σ_N
当前状态 σ_N · 4 个账户 · 3 层 MPT · 根哈希 0x8c47 就是 block header 里的 stateRoot。
注意 Charlie/Dave 这一支:它们字节不差,所以新版本的 σ_N+1 直接指向 σ_N 里那几个旧节点——没新增存储。这是为什么 ETH 全节点存 9 年历史状态磁盘还能装下的原因;也是为什么分叉时切换状态只是”换个指针”的操作。
B.6 · BTC vs ETH 收束
| 维度 | BTC | ETH |
|---|---|---|
| 状态本质 | UTXO 集合 | 账户 mapping |
| 按 key 查 | ❌ 扫全表 | ✅ O(log N) |
| 历史快照 | ❌ 要重放 | ✅ 索引 stateRoot |
| 区块头承诺 | tx Merkle 根 | state / storage / tx / receipt 四棵树 |
📦 持久存储 打勾。下一节谈”在这个存储层上跑的程序”——智能合约。
模块 C · 智能合约
有了身份和存储,现在引入 ETH 的核心创新——智能合约。但先把”它到底是什么”讲清楚。
C.1 · 什么是”智能合约”
1994 年 · 自动售货机
“智能合约”这个词不是 Ethereum 发明的。早在 1994 年,密码学家 Nick Szabo 就提出了这个概念,并给出最简单的例子——
🥤 自动售货机
你投入硬币 → 它吐出可乐。没有售货员、没有合同纸、没有法院。
这台机器自己执行了一份合同——“给定 X 元、交付 Y 商品”。在 Szabo 眼里,这就是世界上最早的 smart contract。
Ethereum 把这个思路推到了极致:在一台全球共享、所有人可验证的计算机上,部署一段会自动执行的代码——这就是链上的智能合约。
它和”普通合约”差在哪?
“智能”二字的真正含义,要看它和普通合约(一张纸)的差别:
| 维度 | 📜 普通合约 (paper) | 🤖 智能合约 (code) |
|---|---|---|
| 形式 | 文字 | 字节码 |
| 谁来解读 | 人来读 · 可能有歧义 | EVM 来执行 · 字节级一致 |
| 谁来强制执行 | 法院 / 律师 / 仲裁 | 协议自身 · 每个节点都跑 |
| 能违约吗 | 可以抵赖、拖延、不执行 | 没法违约——代码必然执行 |
| 要信任谁 | 对方 + 法院 + 律师 | 只信协议(密码学保证) |
| 要付什么 | 律师费、仲裁费、强制执行费 | gas |
核心:“智能”指的是合约能自己执行自己——不需要任何外部权威机构来仲裁或强制。
Nick Szabo 1994 年讲的还只是个概念。直到 2015 年 Ethereum 主网上线,这个想法才有了能跑得起来的载体。
C.2 · 它在 Ethereum 上长什么样
智能合约在 Ethereum 上不是一个独立的东西——它就是一种特殊的账户。
回想 Module A 的账户结构(共用一种数据格式):
- EOA:
codeHash = ∅· 没代码,由私钥控制 - 合约账户:
codeHash指向不可变字节码 · 由那段代码控制
二者唯一的差别就是 codeHash 这一个字段是不是空。换句话说:
C.3 · 凭什么这种代码是”自主对象”?
C.2 说合约就是个”特殊账户”——但它的代码 究竟和普通服务器上的代码有什么本质区别?为什么我们要给它起个新名字叫”smart contract”、“autonomous object”?
下面这个对比把同一段 Counter 代码扔到两个世界里跑,看会发生什么:
◆ Why this code is "smart" · 4 dimensions of contrast
same code · two worlds
◢ the code (identical in both worlds)
// 同一段 Counter 代码 · 两种世界
function increment() {
counter += 1;
}📱 普通代码
on your server
🤖 智能合约
on ethereum
Q1 · 代码住在哪?
一台服务器
装在你的机器或你信任的云上
每个 ETH 节点
全网每台全节点都同步保存这份字节码
Q2 · 代码自己能持有 / 操作资产吗?
不行 · 代码只是逻辑
函数操作"别人"的数据 · 代码本身没有钱包、没有地址、不"拥有"任何东西
能 · 代码就是一个账户
有自己的地址 / 余额 / storage · Uniswap 合约自己持有几十亿美元的池子
Q3 · 部署之后还能改吗?
想改就改
改完代码 redeploy 就行 · 用户察觉不到
不可改 · 连你自己也不行
codeHash 钉在那个地址,链上不允许重写
Q4 · 你跑路了之后呢?
服务一关就死
没人付电费 · 程序就没了 · 数据可能丢
还在跑
所有 ETH 节点都有一份 · 你消失 ≠ 它消失
Q5 · 谁能调用?
你授权过的用户
需要账号 · API key · 防火墙白名单
任何人
只要发一笔合法 tx · 不需要任何许可
这就是黄皮书 Appendix A 的原话:“A smart contract is an autonomous object with an account state and EVM code.” ——一个自主对象。
C.4 · 生命周期 · 部署 → 调用
那你怎么”用”一个智能合约?说白了只有两件事:先让它住到链上,然后向它发消息。两件事都是发一笔 tx——区别只在这笔 tx 长什么样:
- 把代码搬上链 →
create:一笔特殊交易,to字段留空,EVM 看到没收件人就理解为”建房子”,把字节码写到一个新地址 - 跟已经在链上的合约说话 →
call:一笔普通交易,to = 合约地址,data = calldata(指明哪个函数 + 参数)
那个”新地址”不是随机抽的,是确定性派生出来的:
contractAddr = keccak256(rlp([deployer_addr, deployer_nonce]))[12:]
部署者 + nonce 一定,地址就一定——意味着部署前就能算出地址。所以你可以把这地址提前写进别的合约里,或者在多条链上用同一个部署者 + nonce 部署,拿到同一个地址。proxy 升级、CREATE2 工厂都基于这点,先眼熟一下。
C.5 · 互动 · 部署一个 Counter
光说不练假把式——把刚才那两件事在浏览器里走一遍:先 create 一个 Counter,再 call 它的 increment(),盯着地址、nonce、storage 怎么变。
◆ Deploy & Call · A Counter on Chain
autonomous object in action
◢ source
// Counter.sol
contract Counter {
uint256 public n; // ← lives in storage[0]
function increment() public {
n = n + 1;
}
}◢ actions
◢ world state (zoomed)
← press deploy() · 一个新地址会被推导出来:
addr = keccak256(rlp([Alice_addr, alice.nonce]))[12:]
注意 Counter 的
nonce始终是 1——合约账户的 nonce 记录”它自己创建过几个子合约”,不是被调用次数。
C.6 · 可组合性 · 合约调合约
到这里”一个合约”已经讲完了——一段不可变代码 + 自己的私有存储 + 公开可调。但 ETH 真正的爆发力,从来不是单个合约能做什么,而是——
这条性质叫 composability(可组合性)——智能合约之间能像乐高一样拼接。它是 ETH 上整个 DeFi 生态的根:没有公司去对接、没有 API key、没有 SLA 谈判——任意一个合约都默认可以调用其它任意合约。
几块乐高 → 一座积木城
下面先把 6 块在主网上最常被组合调用的”乐高积木”放出来——每一块都只做一件简单的事(借贷、兑换、质押、再质押)。然后再把它们拼成 3 个今天还在跑的真实策略:
- A · stETH 循环杠杆 — 把 3% 的质押收益放大到 7-9%
- B · 闪电贷套利 — 0 抵押借走 100 万美元、5 步内还清、净赚价差
- C · Liquid Restaking 收益叠罗汉 — 一份 ETH 同时跑 4 层收益
◆ DeFi Composability · Real Lego Stacks
building blocks → scenarios
◢ the lego pieces · 单看每一块都很简单
存抵押资产、借出别的资产 · 也提供 flash loan(无抵押、必须同笔 tx 还)
最大的链上 DEX · 用 AMM 公式让任意两种 ERC-20 直接兑换
专做稳定币 / 同类资产兑换的 DEX · 滑点极低
把 ETH 质押给验证人,给你一张可流通的 stETH 收据
让已质押的 ETH 再"复用"一次安全性 · restaking 鼻祖
自动复利 LP 奖励 · "策略层",骑在 Curve / Balancer 上
单独看每一个都只做一件小事。真正的魔法是它们能在一笔 tx里互相调用—— 像乐高一样拼出原本不存在的策略。下面三个是 2025 年仍在用的真实案例。
scenario A
Leveraged stETH · 循环借贷放大质押收益
Lido + Aave + Curve · 把 3% 的 stake APR 放大到 6-9%
amplifier
3% APR → 7-9% APR
◢ call chain · executed so far
submit{value: 10 ETH}()
deposit(stETH, 10)
borrow(WETH, 8)
exchange(WETH → stETH)
deposit(stETH, 8) · loop
按 ▶ play 跑一遍 · 5 步
◢ current step
← 等你点 play · 整条调用链会一步步亮起
⚠️ 上面的 calldata 都是示意级的(真实参数会更复杂、还要带路径 / 滑点 / deadline),但调用顺序和资金流向是按主网真实策略画的。
🤖 智能合约 打勾。但这些代码到底怎么执行?答案是 EVM。
模块 D · EVM
合约的代码到底怎么跑起来?这一节只讲大局——code、EVM、出块这三者怎么联动。
D.1 · 三件事的关系 · 一组动画
下面这个动画走完三个阶段:①部署 ②调用 ③出块。看完应该对”合约代码躺在每个节点上、每个节点都有一台 EVM、每出一块都跑一次”这件事有清晰的画面感。
◆ Code · EVM · 出块 · 三者怎么联动
phase 1/3
phase 1 · deploy
部署 · 把代码搬上链
部署本质上就是一笔特殊的 tx——它带 bytecode、`to` 字段为空。走和普通 tx 一模一样的流水线:先进 mempool → 等出块者打包进区块 → 全网广播 → 每个节点的 EVM 跑一次 create 指令、把同一份代码写到同一个新地址。最终每个节点都本地保存这份合约代码。
一句话收束——EVM 是把”链上代码”和”链上状态”接起来的那台机器,每出一个区块就跑一轮。
至于 EVM 内部细节(栈式架构、字节码 opcode、各种内存区)—— 等 Lesson 3 我们真正写 Solidity 时再展开。现在你只需要知道:每个节点都有一台 EVM,每出一个块它就把这块里的 tx 跑一遍。
🧮 计算引擎 打勾——所有节点共同执行、字节级一致、按 gas 计费。下一节谈”按 gas 计费”是什么意思。
模块 E · Gas
合约可以无限循环吗?执行资源谁出?这是经济学问题——公地悲剧。
E.1 · 为什么要有 Gas
ETH 是个共享的世界计算机——你发一笔 tx,全网每个全节点都得跑一遍、存一遍。CPU、内存、磁盘、带宽都是有限的共享资源。
如果免费会怎样?我写一个
while(true) { storage[i++] = 1 }部署上去——全网节点陪我跑到死,硬盘被我塞满。全网瘫痪 · 成本 0 元。
所以必须定价。但怎么定?两个生动的比喻——
比喻 1 · 高速公路收费站
公路是公共资源、容量有限。免费 → 所有人都开上来 → 全堵死。 所以按公里收费,并且不同车按吨位收:
- 小轿车(简单转账)· 占道少 · 收费低
- 大货车(合约部署 / 复杂 swap)· 占道久 · 收费高
- 超载(死循环)· 直接拦下不让上
EVM 里每条指令都有自己的”过路费”——
ADD像小轿车(3 gas),SSTORE写一格状态像 30 吨大货(22,100 gas),因为节点要把它写进硬盘永久存着。
比喻 2 · 打车计价表(最贴切)
你上车前跟司机说:“我最多付 ¥200”(GasLimit)。车开起来计价表跳字——
- 路顺 · 实际 ¥120 · 多收的 ¥80 退给你
- 路超远 · 跳到 ¥200 还没到 · 司机就地把你赶下车——但你已经坐过的那段路还得付
这就是 OOG(Out of Gas) 为什么扣钱:节点真的为你执行了那些指令、消耗了 CPU。否则攻击者只要算准”刚好失败”就能白嫖全网算力。
而 GasPrice 就像你愿意给司机加多少小费——出价高的 tx 排在前面、优先被打包进块。
E.2 · 现实数字 · 一笔操作到底多少钱
抽象的 gas 换算成钱:总费用 = gasUsed × gasPrice。按 1 ETH ≈ $3,000、1 gwei = 10⁻⁹ ETH 估算——
| 操作 | gasUsed | 平峰 (30 gwei) | 拥堵 (200 gwei) |
|---|---|---|---|
| ETH 转账 | 21,000 | ~$1.9 | ~$12.6 |
| ERC20 转账 | ~65,000 | ~$5.8 | ~$39 |
| Uniswap swap | ~150,000 | ~$13.5 | ~$90 |
| NFT mint | ~200,000 | ~$18 | ~$120 |
| 部署一个合约 | 1M-3M | ~$90-270 | ~$600-1,800 |
同一笔 swap · 平峰 $13 · 拥堵 $90——这就是为什么大家盯着 gas tracker 等”夜深人静”再操作。2021 NFT 高潮 gasPrice 飙到 1000+ gwei · 一笔 mint 烧 $500 是常事。
E.3 · Gas / GasPrice / GasLimit
| 概念 | 含义 | 类比 |
|---|---|---|
| Gas | tx 消耗的计算量(无量纲)· ADD=3 · SLOAD=2100 · SSTORE 首写=22100 · base=21000 | 计价表跳了多少格 |
| GasPrice | 你愿付每单位 gas 多少 ETH(gwei = 10⁻⁹ ETH)· 出块者按出价排序 | 每格单价 + 给司机的小费 |
| GasLimit | tx 的 gas 上限 · 实际 ≤ limit 退还 · 实际 > limit → OOG · gas 仍被扣 | 你给司机的预算上限 |
OOG 时 gas 也扣——因为节点真的执行了那些指令。否则攻击者会反复”刚好失败”白嫖。
E.4 · 互动 · 把循环跑到爆
◆ Gas · Loop Until You Run Dry
✓ would succeed
◢ contract
function loop(uint256 N) public {
uint256 sum;
for (uint i = 0; i < N; i++) {
sum += i;
}
}◢ gas accounting
◢ fuel gauge
✓ tx 成功
实际付费 = 46,000 gas × 30 gwei = 0.001380 ETH
剩余 34,000 gas 会原路退还
⛽ 资源计费 打勾。
E.5 · 经济学副产品
- 拥堵时 gasPrice 抬高,简单 tx 自动让位(小轿车不舍得花大货的钱挤高峰路)
- EIP-1559:每块 base fee(被销毁)+ priority fee(出块者小费)·随拥堵动态浮动
- 燃烧:base fee 永久销毁——链上活动越多 ETH 越通缩
模块 F · 交易执行流程
从用户视角,一笔 tx 看起来就是”点个按钮,几秒后链上多了一条记录”。但在链上节点的视角,每一笔 tx 都要走完一条5 段流水线。
F.1 · 流水线全景
◆ Transaction · From Mempool to Receipt
5 stages
◢ incoming tx
F.2 · 每一步防一类攻击
| Step | 防什么 |
|---|---|
| ① 验签 | 防伪造 |
| ② 预检 | 防重放(nonce 错就拒)+ 防白用(余额不够就拒) |
| ③ 快照 | 防部分执行(revert 时能回到原点) |
| ④ 执行 | 防失控(gas 见底就停) |
| ⑤ 结算 | 防免费(OOG 也扣钱)+ 收据上链留痕 |
F.3 · Receipt(收据)
💡 log / event · 合约里用
emit Transfer(from, to, amount)发出事件——不进 storage(太贵),写入 receipt 的logs数组。前端、浏览器靠监听 log 知道链上发生了什么。
◆ Anatomy of a Receipt
Receipt
keyed by tx index in block
status
0/1 · tx 是否成功 · false 也写收据
gasUsed
本笔 tx 实际消耗的 gas
cumulativeGasUsed
本块内累计 gas(不含本笔之后的)
logs
合约 emit 出的事件
logsBloom
2048-bit 布隆过滤器 · 让 light client 高效订阅
contractAddress
只有 create tx 才填这个字段
- 可证明发生过什么 · receipt 树根写进 header
- 前端订阅事件 · 不用扫整个状态,只看 logs
- 失败也留痕 · status=false 的 tx 也产收据——所以钱包能告诉你 “tx failed, gas was charged”
下面是两个深水区——黄皮书形式化 / MPT 细节,给想深入的同学。
深水区 · 黄皮书的形式化
黄皮书把整个 ETH 协议写成形式化数学定义。骨架就几条。
YP.1 · 一行核心公式
σ_t+1 ≡ Υ(σ_t, T)
σ : Address → ⟨ nonce, balance, storageRoot, codeHash ⟩
整本黄皮书都在把 Υ 展开成字节级规则。Υ 又分两段:
Π(σ, B) = Ω( Λ( σ, B.txs ), B )
└── exec txs ──┘ └ block rewards
YP.2 · 一个区块里的 4 棵树
◆ Anatomy of an Ethereum Block · Four Trees
state · storage · txs · receipts
◢ Block Header (selected)
3 个 32-byte hash就锁定了整块的状态、交易、收据
◢ Four Trees (each is an MPT)
mapping[address → AccountState]
所有账户的余额、nonce、合约代码哈希、存储指针
mapping[bytes32 → bytes32]
某个合约自己的 K/V 状态——挂在 stateTrie 的某个叶子下
mapping[index → Tx]
本块所有交易的承诺·让任何 tx 都能被 Merkle 证明
mapping[index → Receipt]
本块所有 tx 的执行结果·让事件日志可被 light client 订阅
4 棵树是 ETH 的默克尔承诺骨架——一个字节被改、根哈希变、共识层立即拒收。
YP.3 · RLP · 协议层序列化
不用 JSON、不用 protobuf——ETH 用自己的 RLP(递归 + 长度前缀 + 紧凑)。
◆ RLP · Recursive Length Prefix
字节级 · 紧凑 · 递归
RLP 无处不在:tx 在 P2P 上传输、account 在 state trie 里、合约地址 = keccak256(rlp([from, nonce]))[12:]。
YP.4 · 黄皮书的价值
真正贡献不是发明,而是让 ETH 成为可被多客户端独立实现并互验的协议。
geth / besu / nethermind / reth / erigon…跑同一个网络,结果必须字节级一致。这种”协议规约 + 多实现”的体系是 ETH 工程层的核心保护。
深水区 · MPT 数据结构
MPT.1 · Nibble 是最小单位
MPT 不按字节走,按 nibble(半字节 / 4-bit / 0-15)——每层分支最多 16 个孩子,正好对齐到一个 hex 字符。
ETH 地址(20 字节)作为 key 进 state trie 时,先展开成 40 个 nibble,从根逐层匹配。
MPT.2 · 三种节点类型
◆ Three Node Types of MPT
Leaf
终点[ encodedPath, value ]
一条路径走到底——存放最终值
- ·encodedPath · 从这个点到该叶子剩余的 nibble 序列
- ·value · 实际数据(账户 RLP / storage 值 / tx RLP)
- ·前缀字节标记 leaf(高位 = 2 表示 leaf)
Extension
共享段[ encodedPath, hash(child) ]
一段公共前缀单链——压缩 Trie 的核心
- ·encodedPath · 这一段共享的 nibble 序列
- ·指向下一个节点(通常是 Branch)
- ·前缀字节高位 = 0 表示 extension
Branch
十六叉口[ v0, v1, …, v15, value ]
17 个元素 · 一个分叉点
- ·v0..v15 · 每个 nibble 值对应一条出边的子节点哈希
- ·value · 该位置就是 key 末尾时的存值
- ·没有 path 字段——位置本身就是 nibble
小测试:树里只存一个 key→value?根节点是 Leaf——一条路径走到底,无分叉无共享。
MPT.3 · 走一遍查找路径
◆ Nibble Trace · Walking an Address Through MPT
hop 0 / 6
◢ address as nibbles (only first 8 shown · real addr = 40 nibbles)
◢ traversal
- 01EXTconsumes "a"从 root 开始,第一个 Extension 共享 nibble "a"——大量地址都以 0xa.. 开头
- 02BRAconsumes "1"到达 Branch 节点 · 17 个出口 · 按下一个 nibble "1" 选 children[1]
- 03EXTconsumes "7b"又是一段共享前缀 "7b" · 继续往下
- 04BRAconsumes "4"又一个分叉口 · 按 "4" 选 children[4]
- 05EXTconsumes "de"最后一段共享段 "de" · 接近叶子了
- 06LEAFconsumes "9"Leaf 节点 · encodedPath 包含剩余 nibble "9" + 终止标记 · value = RLP(account)
读取一个账户 ≈ O(40 nibble)。和”扫整个状态”不在一个量级。
MPT.4 · 为什么这么复杂值得
- 可证明查任意账户 · stateRoot + 节点路径 = 余额可被独立验证
- 跨区块共享子树 · 没变的部分原地复用,磁盘只随改动量增长
- 轻节点 · 只下载 header,按需请求 MPT proof
数据结构的选择,决定协议能力的上限。
🎬 收尾 · 世界计算机组装完成
共识层我们没单独讲——ETH 的 PoW→PoS 和 BTC 在结构上同源。它依然是支撑这一整套的底座。
六个零件全打勾:
- 🆔 账户 · EOA / 合约共用结构,只差
codeHash - 📦 存储 · World State + 合约自己的 storage MPT
- 🤖 合约 · 特殊账户 + 不可变代码 + 自有存储
- 🧮 EVM · 256-bit 栈机,六块内存区
- ⛽ Gas · 把公共资源消耗变成可定价市场
- 🔗 共识 · 沿用 BTC 同源机制 · PoW → PoS
一个收束
回到最开始的问题:账本可以去中心化,那”逻辑”呢?
与第一课的对照
| 维度 | BTC | ETH |
|---|---|---|
| 隐喻 | 账本机器 | 世界计算机 |
| 状态 | UTXO 集合 | World State (MPT) |
| 钱的模型 | 离散 UTXO | 账户余额 |
| 可编程 | BTC Script · 受限 | Smart Contract · 通用 |
| 数据承诺 | tx Merkle root | 四棵树 |
| 检查表 | 银行 8 项 | 世界计算机 6 项 |
通向下节课
世界计算机已经组装好。下节课引入它的高级语言——Solidity,从语法到上线一个 ERC-20 代币。