◢ Lesson 2 ⏱ 105 min

从账本机器到世界计算机 · ETH

🎬 开场 · 一个新的问题

2013 年冬天。Bitcoin 跑了五年,证明了账本可以去中心化。 Vitalik Buterin 在他公寓里问了下一个问题:

这是 Ethereum 的元命题。Vitalik 写白皮书 · Gavin Wood 把它落成黄皮书。今天我们要解构的,就是黄皮书里那台「世界计算机」。

“Ethereum is a transaction-based state machine.” ——《Ethereum Yellow Paper》


今天的检查表

第一课用「银行职能 8 项」组装出 BTC。这一课用 6 个零件组装出一台世界计算机

item 1 · 身份🆔账户系统
item 2 · 存储📦持久状态
item 3 · 程序🤖智能合约
item 4 · 计算🧮EVM 虚拟机
item 5 · 计费Gas / GasPrice
item 6 · 共识🔗PoW → PoS

每讲完一节,右上角浮窗自动勾掉对应一项。六项全亮,世界计算机就组装完成。


前章 · 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 的三条限制(故意的):

要跑这种程序,三个底层难题躲不掉:

  1. 🧮 谁来执行复杂程序 → EVM
  2. 📦 程序状态存哪 → World State
  3. 谁来付计算的钱 → Gas

本课接下来就是一项一项解决这三个问题。


模块 A · 账户模型 vs UTXO

A.1 · 一个生活类比

🧾 现金钱包 · 钱是一叠面额各异的钞票。付 5 块要么递一张 5 元、要么递 10 元拿回找零。

🏦 银行账户 · 钱是一个数字。账号是门牌号,余额就是号码下的数。转账时一边 −x、另一边 +x。

BTC 用前者(UTXO ≈ 链上的钞票)·ETH 用后者(每个地址下挂一个状态)。

A.2 · 为什么 ETH 选了”银行账户”

银行账户比钞票多了两件事,直接决定能不能跑程序

  1. 固定门牌号——账号一辈子是它,钱永远住在那。钞票没有”位置”,是流动的。
  2. 能记任意东西——账号下不只是余额,还能开新栏位记任何字段。钞票只能是钞票。

核心: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

nonceuint64
balanceuint256
storageRootbytes32
codeHashbytes32

nonce

发送过的 tx 数(EOA)/ 创建过的合约数(Contract)

balance

账户余额(单位:wei)

storageRoot

该账户存储 MPT 的根哈希 · 指向另一棵树

codeHash

合约字节码的哈希 · EOA 时为空哈希

EOAcodeHash = ∅ · storageRoot = ∅ · 由私钥签名控制
ContractcodeHash 指向字节码 · storageRoot 指向 K/V 树 · 由代码控制
字段为什么需要它
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

u130 coin→ Alice
u220 coin→ Alice
Alice 余额(推导)Σ unspent = 50
Bob 余额(推导)Σ unspent = 0
·

press play

ETH · Account

k/v map

Alice_addr
balance50
nonce0
Bob_addr
balance0
nonce0
余额是 直接读取的字段 · 不需要扫描历史

按 ▶ play 启动 · 或者从上面的步骤列表里直接跳到某一步


模块 B · 世界状态

本节是全课最重的概念跳跃——慢一点没关系。

B.0 · 前置 · 两把工具

数据结构✅ 能力❌ 局限
Merkle Tree(上课讲过)一个根代表整集合 · log(N) 长度证明按位置排列 · 给 key 找不到 · 稀疏 + 海量 key 时增删代价大
Patricia Trie(前缀压缩)按 key 索引 · 公共前缀共享 · 增删只动一条路径节点改了没人察觉

◆ How Two Trees Become MPT

stage 0 · flat list

5 个 key

把它们存进字典——要能按 key 查找。

"do"
"dog"
"door"
"tea"
"ten"

编码细节(branch / extension / leaf · nibble 路径 · RLP)留到后面「深水区 · MPT」展开。


B.1 · 从 BTC 的区块头说起

第一课我们看过 BTC 的区块头——核心字段有两个:

就这两个就够 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"

parentHash0xa17b…
merkleRoot0x29d3…
timestamp1715890800
nBits
nonce

BTC 只承诺 交易—— 状态(谁有多少钱)由扫描历史推导出来。

ETH · header (selected)

"world computer"

parentHash0xa17b…
transactionsRoot0x29d3…
stateRoot0x8c47…new
timestamp1715890800
baseFeePerGas28 gwei

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

BLOCK N · HEADERparentHash0xa17b…transactionsRoot0x29d3…stateRoot0x8c47…stateRootSTATE TRIE · MPT0xa/b0xc/da1b2c3d40xa1… Alice0xb2… Bob0xc3… Charlie0xd4… Counteraccount leafACCOUNT · 0XD4… COUNTERnonce1balance0codeHash0xc0de…storageRoot0x4f9a…storageRootSTORAGE TRIE · MPTslot[0]slot[1]slot[2]42true

step 1 · L1 · block

区块头 — 三个核心字段 · stateRoot 是 ETH 新增的状态承诺。

B.4 · 状态怎么演化

黄皮书一行公式:

σ' = Υ(σ, T)
   ↑    ↑   ↑
   ↑  当前 state  这一块的 txs
新 state

◆ State Evolution · σ' = Υ(σ, T)

block by block

state σblock B (其交易 = 转移函数的输入)点 σ 或 B 跳到那一步
B₁B₂B₃B₄σ₀0x4f9a…GENESISσ₁0x8c47…σ₂0xa17b…σ₃0xc31d…σ₄0x29ef…

genesis state · σ₀

创世状态 — 所有账户都还没活动。stateRoot 是空 MPT 的根。

三层关系一句话:区块状态是世界状态在某个时刻的固定值;世界状态是所有账户状态的聚合。

B.5 · MPT 复杂的代价 = 三个回报

MPT 实现复杂、占空间,但换来三件硬核能力:

  1. 轻客户端可验证 · 手机只下载 header 也能 Merkle 证明余额
  2. 历史快照零成本 · stateRoot 直接锁定那一刻全部账户
  3. 分叉切换 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

LIGHT CLIENT · block N headerparentHash0xa17b…transactionsRoot0x29d3…stateRoot0x8c47~ 几百字节 · 不存 MPTFULL NODE · keeps the entire MPTAliceBobCharlieDave

step 1 · setup

轻客户端只存 block header(里面有 stateRoot),不存整棵 MPT。它想查 Alice 的余额,但不能凭空相信任何全节点的回复。

B.5.2 · 子树共享 → 历史快照 + 分叉切换

第 2、3 条回报靠同一个机制——MPT 的子树共享

◆ Hash Propagation · Single Tree, Diff View

step 1/4

stateRoot in block header0x8c47
a/bc/da1b2c3d40x8c470x1f6a0x4b220x7a3cAlicebal 50 · nonce 00xb18dBobbal 0 · nonce 00x5e02Charliebal 100 · nonce 50x9d71Davebal 25 · nonce 2

step 1 · σ_N

当前状态 σ_N · 4 个账户 · 3 层 MPT · 根哈希 0x8c47 就是 block header 里的 stateRoot。

注意 Charlie/Dave 这一支:它们字节不差,所以新版本的 σ_N+1 直接指向 σ_N 里那几个旧节点——没新增存储。这是为什么 ETH 全节点存 9 年历史状态磁盘还能装下的原因;也是为什么分叉时切换状态只是”换个指针”的操作。

B.6 · BTC vs ETH 收束

维度BTCETH
状态本质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 的账户结构(共用一种数据格式):

二者唯一的差别就是 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 · 代码住在哪?

code

一台服务器

装在你的机器或你信任的云上

codecodecodecodecode

每个 ETH 节点

全网每台全节点都同步保存这份字节码

Q2 · 代码自己能持有 / 操作资产吗?

codereads/writesuser_db💰 owned by userscode 自己没钱包 · 资产在外部

不行 · 代码只是逻辑

函数操作"别人"的数据 · 代码本身没有钱包、没有地址、不"拥有"任何东西

0xC011… · contract accountcode()💰 balance · 142 ETH代码 = 账户 · 自己持有资产

能 · 代码就是一个账户

有自己的地址 / 余额 / storage · Uniswap 合约自己持有几十亿美元的池子

Q3 · 部署之后还能改吗?

bytecode0x60806040…

想改就改

改完代码 redeploy 就行 · 用户察觉不到

bytecode0x60806040…🔒

不可改 · 连你自己也不行

codeHash 钉在那个地址,链上不允许重写

Q4 · 你跑路了之后呢?

服务一关就死

没人付电费 · 程序就没了 · 数据可能丢

codecodecode

还在跑

所有 ETH 节点都有一份 · 你消失 ≠ 它消失

Q5 · 谁能调用?

你授权过的用户

需要账号 · API key · 防火墙白名单

任何人

只要发一笔合法 tx · 不需要任何许可

合在一起看:合约**自己就是一个账户**(持有资产、能发起动作)、**独立于部署者继续运转**、**自带不可变规则**、**任何人都能验证 / 调用**——这就是黄皮书称它为 autonomous object(自主对象)的来由。"代码即账户"是这一切的基础。

这就是黄皮书 Appendix A 的原话:“A smart contract is an autonomous object with an account state and EVM code.” ——一个自主对象

C.4 · 生命周期 · 部署 → 调用

那你怎么”用”一个智能合约?说白了只有两件事:先让它住到链上,然后向它发消息。两件事都是发一笔 tx——区别只在这笔 tx 长什么样:

那个”新地址”不是随机抽的,是确定性派生出来的:

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)

Alice_addrEOA
nonce0
balance10 ether
codeHash

← 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 个今天还在跑的真实策略

◆ DeFi Composability · Real Lego Stacks

building blocks → scenarios

◢ the lego pieces · 单看每一块都很简单

AaveLending · Flash Loan

存抵押资产、借出别的资产 · 也提供 flash loan(无抵押、必须同笔 tx 还)

UniswapDEX · AMM

最大的链上 DEX · 用 AMM 公式让任意两种 ERC-20 直接兑换

CurveDEX · Stable Swap

专做稳定币 / 同类资产兑换的 DEX · 滑点极低

LidoLiquid Staking

把 ETH 质押给验证人,给你一张可流通的 stETH 收据

EigenLayerRestaking

让已质押的 ETH 再"复用"一次安全性 · restaking 鼻祖

ConvexYield Aggregator

自动复利 LP 奖励 · "策略层",骑在 Curve / Balancer 上

单独看每一个都只做一件小事。真正的魔法是它们能在一笔 tx里互相调用—— 像乐高一样拼出原本不存在的策略。下面三个是 2025 年仍在用的真实案例。

scenario A

Leveraged stETH · 循环借贷放大质押收益

Lido + Aave + Curve · 把 3% 的 stake APR 放大到 6-9%

amplifier

3% APR → 7-9% APR

User (EOA)
Lido
Aave
Curve
Aave

◢ call chain · executed so far

step 1User (EOA)Lido

submit{value: 10 ETH}()

step 2User (EOA)Aave

deposit(stETH, 10)

step 3User (EOA)Aave

borrow(WETH, 8)

step 4User (EOA)Curve

exchange(WETH → stETH)

step 5User (EOA)Aave

deposit(stETH, 8) · loop

按 ▶ play 跑一遍 · 5

◢ current step

← 等你点 play · 整条调用链会一步步亮起

0/5
观察 ↘ 结果:3% 的 stETH 原生收益被借贷杠杆放大到 ≈ 7-9%。代价:ETH 价格脱钩或借贷利率反转会触发清算。
底层 ↘ 这三个场景没有"中央协调者"—— 没有一家公司、没有一个 API 网关。只是 智能合约会自己调智能合约 这一条性质,加上账户模型的统一抽象,就长出了整个 DeFi 生态。

⚠️ 上面的 calldata 都是示意级的(真实参数会更复杂、还要带路径 / 滑点 / deadline),但调用顺序和资金流向是按主网真实策略画的。

🤖 智能合约 打勾。但这些代码到底怎么执行?答案是 EVM。


模块 D · EVM

合约的代码到底怎么跑起来?这一节只讲大局——code、EVM、出块这三者怎么联动。

D.1 · 三件事的关系 · 一组动画

下面这个动画走完三个阶段:①部署 ②调用 ③出块。看完应该对”合约代码躺在每个节点上、每个节点都有一台 EVM、每出一块都跑一次”这件事有清晰的画面感。

◆ Code · EVM · 出块 · 三者怎么联动

phase 1/3

ACTORNETWORK · 4 NODES EACH RUNNING ITS OWN EVMBLOCKCHAIN👤Developermempoolmempool · deploy txNODE 1EVMcode @ 0xC011…state σ_NNODE 2EVMcode @ 0xC011…state σ_NNODE 3EVMcode @ 0xC011…state σ_NNODE 4EVMcode @ 0xC011…state σ_NblockB₁blockB₂blockB₃new blockB_deploycontains deploy txdeploy(code) txcodecodecodecode

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 · 高速公路收费站

公路是公共资源、容量有限。免费 → 所有人都开上来 → 全堵死。 所以按公里收费,并且不同车按吨位收

EVM 里每条指令都有自己的”过路费”——ADD 像小轿车(3 gas),SSTORE 写一格状态像 30 吨大货(22,100 gas),因为节点要把它写进硬盘永久存着。

比喻 2 · 打车计价表(最贴切)

你上车前跟司机说:“我最多付 ¥200”(GasLimit)。车开起来计价表跳字——

这就是 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

概念含义类比
Gastx 消耗的计算量(无量纲)· ADD=3 · SLOAD=2100 · SSTORE 首写=22100 · base=21000计价表跳了多少格
GasPrice你愿付每单位 gas 多少 ETH(gwei = 10⁻⁹ ETH)· 出块者按出价排序每格单价 + 给司机的小费
GasLimittx 的 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;
  }
}
500
03000
80,000
21k200k

◢ gas accounting

tx base21,000
loop body (×500 iter × 50 gas)25,000
needed46,000

◢ fuel gauge

46,000 / 80,000

✓ tx 成功

实际付费 = 46,000 gas × 30 gwei = 0.001380 ETH

剩余 34,000 gas 会原路退还

观察 ↘ 把 iterations 拉到 1200 以上、把 gas limit 留在低位——你会看到红色 OOG 警告。这就是死循环的解药:写得起、跑不起。

⛽ 资源计费 打勾。

E.5 · 经济学副产品


模块 F · 交易执行流程

从用户视角,一笔 tx 看起来就是”点个按钮,几秒后链上多了一条记录”。但在链上节点的视角,每一笔 tx 都要走完一条5 段流水线

F.1 · 流水线全景

◆ Transaction · From Mempool to Receipt

5 stages

◢ incoming tx

from0xA17b…ef9
to0xC011…ab92
value0 ETH
dataincrement()
gasLimit200,000
step 1
验签
step 2
预检
step 3
快照
step 4
执行
step 5
结算
按 ▶ play 看这笔 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

statustrue
gasUsed142,530
cumulativeGasUsed8,210,304
logs[ Transfer(Alice, Bob, 100), Approval(...) ]
logsBloom0x00…(256 bytes)
contractAddressnull

status

0/1 · tx 是否成功 · false 也写收据

gasUsed

本笔 tx 实际消耗的 gas

cumulativeGasUsed

本块内累计 gas(不含本笔之后的)

logs

合约 emit 出的事件

logsBloom

2048-bit 布隆过滤器 · 让 light client 高效订阅

contractAddress

只有 create tx 才填这个字段

失败也留痕——OOG 也产生 status=false 的 receipt
Bloom 让 DApp 前端能 O(1) 判断"我关心的事件在不在本块"
所有 receipt 进 receipt 树 · 根哈希写进 block header

下面是两个深水区——黄皮书形式化 / 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)

parentHash0xa17b…
stateRoot0x8c47…
transactionsRoot0x29d3…
receiptsRoot0x55e1…
number19,820,317
timestamp1715890800
baseFeePerGas28 gwei
gasUsed14,210,304

3 个 32-byte hash就锁定了整块的状态、交易、收据

◢ Four Trees (each is an MPT)

State Trievia stateRoot

mapping[address → AccountState]

所有账户的余额、nonce、合约代码哈希、存储指针

Storage Trie (per contract)via storageRoot (in account)

mapping[bytes32 → bytes32]

某个合约自己的 K/V 状态——挂在 stateTrie 的某个叶子下

Transactions Trievia transactionsRoot

mapping[index → Tx]

本块所有交易的承诺·让任何 tx 都能被 Merkle 证明

Receipts Trievia receiptsRoot

mapping[index → Receipt]

本块所有 tx 的执行结果·让事件日志可被 light client 订阅

4 棵树是 ETH 的默克尔承诺骨架——一个字节被改、根哈希变、共识层立即拒收。

YP.3 · RLP · 协议层序列化

不用 JSON、不用 protobuf——ETH 用自己的 RLP(递归 + 长度前缀 + 紧凑)。

◆ RLP · Recursive Length Prefix

字节级 · 紧凑 · 递归

input
rlp encoded
rule
"a"
0x61
单字节 < 0x80 · 原样输出
""
0x80
空字符串 · 0x80
"hello"
0x85 68 65 6c 6c 6f
字符串 0-55 字节 · 0x80 + len · 原文
15
0x0f
小整数 (< 128) · 单字节
1024
0x82 04 00
大整数 · big-endian bytes · 前缀 0x80 + len
["cat", "dog"]
0xc8 83 63 61 74 83 64 6f 67
列表 · 0xc0 + payload_len · 各元素递归

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)

a
1
7
b
4
d
e
9

◢ traversal

  1. 01EXTconsumes "a"从 root 开始,第一个 Extension 共享 nibble "a"——大量地址都以 0xa.. 开头
  2. 02BRAconsumes "1"到达 Branch 节点 · 17 个出口 · 按下一个 nibble "1" 选 children[1]
  3. 03EXTconsumes "7b"又是一段共享前缀 "7b" · 继续往下
  4. 04BRAconsumes "4"又一个分叉口 · 按 "4" 选 children[4]
  5. 05EXTconsumes "de"最后一段共享段 "de" · 接近叶子了
  6. 06LEAFconsumes "9"Leaf 节点 · encodedPath 包含剩余 nibble "9" + 终止标记 · value = RLP(account)
观察 ↘ 8 个 nibble,3 个 Extension 段消化掉公共前缀,2 个 Branch 做按位分叉,最后一个 Leaf 收尾。复杂度 ≈ O(key 长度),不是 O(全状态)。

读取一个账户 ≈ O(40 nibble)。和”扫整个状态”不在一个量级。

MPT.4 · 为什么这么复杂值得

数据结构的选择,决定协议能力的上限。


🎬 收尾 · 世界计算机组装完成

共识层我们没单独讲——ETH 的 PoW→PoS 和 BTC 在结构上同源。它依然是支撑这一整套的底座。

六个零件全打勾:

一个收束

回到最开始的问题:账本可以去中心化,那”逻辑”呢?

与第一课的对照

维度BTCETH
隐喻账本机器世界计算机
状态UTXO 集合World State (MPT)
钱的模型离散 UTXO账户余额
可编程BTC Script · 受限Smart Contract · 通用
数据承诺tx Merkle root四棵树
检查表银行 8 项世界计算机 6 项

通向下节课

世界计算机已经组装好。下节课引入它的高级语言——Solidity,从语法到上线一个 ERC-20 代币。