LusterSourav/how_to_make_evm_from_scratch_in_rust_by_love
GitHub: LusterSourav/how_to_make_evm_from_scratch_in_rust_by_love
一个使用 Rust 从零构建的零依赖、no_std 以太坊虚拟机实现,逐层还原 EVM 底层算术、哈希、状态树及 Gas 计量机制。
Stars: 1 | Forks: 0
# 从零开始构建 EVM —— Rust 中的零依赖实现
[](https://creativecommons.org/licenses/by-sa/4.0/)
[](https://www.rust-lang.org)
[](https://docs.rust-embedded.org/book/intro/no-std.html)
[]()
## 状态
| 组件 | 状态 |
|---|---|
| **第 0 层 —— 原始算术** (`U256`, `U512`, Knuth 除法) | 已交付 |
| **第 1 层 —— 序列化与 Sponge** (RLP, HP 半字节, Keccak-256) | 已交付 |
| **第 2 层 —— 状态管理** (MPT, 账户模型, 世界状态, journal) | 已交付 |
| **第 3 层 —— 资源计量** (gas, 访问列表, 63/64 规则) | 已交付 |
| **第 4 层 —— 引擎核心** (调度表, jump map, Z-function, opcodes) | 计划中 |
| **第 5 层 —— 合规性与预编译** (Secp256k1, MODEXP, EOF, EIP-1153) | 计划中 |
已交付 = 已实现并测试。 计划中 = 已有文档记录,但尚未构建。
## 这是什么
一个用 Rust 从头构建的以太坊虚拟机。没有 `ethers`,没有 `primitive-types`,没有 `sha3`,没有 `secp256k1`。默认情况下连 `std` 也没有。
`#[no_std]` 约束是刻意为之的。它迫使每一个原始操作都必须经过深思熟虑:panic 处理程序、内存布局、算术溢出 —— 没有任何假设,也没有任何借用。每个组件都是直接针对 [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) 设计实现的。
目标是构建一个符合 Cancun 时代要求的执行引擎,其中每一字节的行为都可以追溯到正式规范。
## 快速开始
```
# 克隆
git clone https://github.com/LusterSourav/how_to_make_evm_from_scratch_in_rust_by_love
cd how_to_make_evm_from_scratch_in_rust_by_love
# 运行测试套件(跨越 6 个 crate 的 300 多项测试)
cargo test --workspace
# 运行示例
cargo run --example basic_state
cargo run --example rlp_encode_decode
# 使用可选的 `runtime` 特性运行(基于 std,用于 host 端测试)
cargo test --workspace --features runtime
```
该库在工作区级别是 **no_std + 零依赖** 的。`runtime` 特性是一个为了文档说明而保留的空操作标志 —— 该工作区可以在稳定的 Rust (1.85+) 上完美编译。如果要在实际的裸机环境(`aarch64-unknown-none`, `riscv64imac-unknown-none-elf` 等)中部署,您将需要带有 `-Z build-std=core,alloc` 的 nightly 工具链。
## Crate 布局
七个职责单一的聚焦 crate。全部满足 `#![no_std]` + `#![deny(unsafe_code)]` + 零外部运行时依赖。
| Crate | 职责 |
|---|---|
| `bare-metal-evm-types` | 256 位 (`U256`) 和 512 位 (`U512`) 无符号整数,所有算术运算、除法 (Knuth D)、字节转换。 |
| `bare-metal-evm-keccak` | 位级 Keccak-256 (24 轮 sponge,正确的以太坊填充)。 |
| `bare-metal-evm-rlp` | 具有严格极简主义的递归长度前缀编码/解码器。 |
| `bare-metal-evm-nibble` | 十六进制前缀 nibble 路径,`u4` 抽象,打包的 nibble 缓冲区。 |
| `bare-metal-evm-trie` | 修改版的 Merkle Patricia Trie:4 种节点类型,十六叉树遍历,内联优化,深度受限的递归。 |
| `bare-metal-evm-state` | 账户模型,`WorldState`(缓存 + journal + 提交),EIP-158 空账户修剪,代码存储。 |
| `bare-metal-evm-gas` | Gas 计量:固有 gas,二次内存扩展,EIP-2929 访问集合,EIP-2200 SSTORE,EIP-150 63/64 规则,EIP-3860 initcode 成本,预编译 gas 成本。 |
工作区根 crate (`bare-metal-evm`) 为了方便起见重新导出了所有内容。
## v0.2.0 —— 世界状态 (MPT + 账户模型)
七个 crate 实现了以太坊的世界状态 (σ):一个以地址为键的 MPT,包含 nonce、balance、code hash 以及每个账户的 storage,并带有用于 checkpoint 和回滚的 journal。
### 此版本包含的 Crates
- `bare-metal-evm-types` — U256, U512
- `bare-metal-evm-gas` — EVM gas 计量
- `bare-metal-evm-keccak` — Keccak-256
- `bare-metal-evm-nibble` — u4 nibbles + HP 编码
- `bare-metal-evm-rlp` — RLP 编码/解码
- `bare-metal-evm-trie` — Merkle Patricia Trie
- `bare-metal-evm-state` — WorldState, Account, Journal
全部满足 `#![no_std]`, `#![deny(unsafe_code)]`,零外部依赖。
### 统计数据
| | |
|---|---|
| 测试 | 488 |
| 行数 (src, 不含 lib.rs) | 10,037 |
| Crates | 8 |
`cargo test --workspace` 和 `cargo clippy -- -D warnings` 均无任何警告。
### 0.1.0 以来的新增内容
- 带有 HP 编码和 inline-node 优化的 MPT
- 带有 cache-then-commit 和 EIP-158 修剪的 WorldState
- Journal/回滚,深度限制为 4096
- 发生 DB 错误时保留 Trie root
- `set_code(addr, code)` 现在会自动为您进行哈希计算
- 类型化的 `NibbleError` 替换了 nibble 操作中的 `Result<(), ()>`
- Keccak-256 XOR 块边界:将 `debug_assert` 改为了 `assert`
- 移除了无用的公开 API (`drain`, `split_first`, `split_at`)
- **新 crate: `bare-metal-evm-gas`** — 完整的 gas 计量(固有成本、内存、访问集合、SSTORE 真值表、CALL 63/64、initcode、预编译成本、EIP-7623)
### 安装
```
[dependencies]
bare-metal-evm-types = "0.2.0"
bare-metal-evm-gas = "0.2.0"
bare-metal-evm-keccak = "0.2.0"
bare-metal-evm-nibble = "0.2.0"
bare-metal-evm-rlp = "0.2.0"
bare-metal-evm-trie = "0.2.0"
bare-metal-evm-state = "0.2.0"
```
### OCI (ghcr.io)
```
docker pull ghcr.io/lustersourav/how_to_make_evm_from_scratch_in_rust_by_love/bare-metal-evm-types:0.2.0
docker pull ghcr.io/lustersourav/how_to_make_evm_from_scratch_in_rust_by_love/bare-metal-evm-gas:0.2.0
# ... 每个 crate 还有 5 个
```
完整的发布说明也位于 [GitHub 发布页面](https://github.com/LusterSourav/how_to_make_evm_from_scratch_in_rust_by_love/releases/tag/v0.2.0)。
## v0.3.0: Gas 计量引擎
一个处理完整 gas 计量 pipeline 的单一 crate。固有 gas、内存扩展、冷热访问跟踪、SSTORE 真值表、CALL 转发、transient storage、预编译成本。涵盖了从合并前到 Pectra 阶段的十三个 EIP。一个外部依赖(bare-metal-evm-types),没有标准库,没有 unsafe 代码。
### Crate
- `bare-metal-evm-gas` — EVM gas 计量
### 覆盖范围
- 固有 gas:基础成本,calldata(零字节和非零字节),访问列表
- 内存:二次扩展,增量收费
- 访问集合:针对地址和 slot 的 EIP-2929 冷热状态跟踪
- SSTORE:EIP-2200 真值表中的所有 12 种情况,包含退款跟踪
- CALL:EIP-150 63/64 转发规则,stipend,新账户成本
- CREATE / CREATE2:EIP-3860 initcode 字成本和 48KB 限制
- Transient storage:EIP-1153 TLOAD 和 TSTORE
- 预编译:所有 18 个预编译的 gas(从 ECRECOVER 到 BLS12-381)
- 退款:交易结束时的 EIP-3529 上限
- Calldata 下限:EIP-7623 强制执行最低 gas
### 统计数据
| | |
|---|---|
| 测试 | 221 |
| 单元测试 | 191 |
| 集成测试 | 30 |
| 属性测试 | 8 |
| EIPs | 13 |
| 预编译 | 18 |
`cargo test --workspace` 和 `cargo clippy -- -D warnings` 均无任何警告。
### 安装
```
[dependencies]
bare-metal-evm-gas = "0.3.0"
```
### OCI (ghcr.io)
```
docker pull ghcr.io/lustersourav/how_to_make_evm_from_scratch_in_rust_by_love/bare-metal-evm-gas:0.3.0
```
# 底层物理机制 (已实现)
## 1. 256 位算术引擎
Rust 原生最高支持到 `u128`。而 EVM 的字长为 256 位。因此,首要任务就是构建语言没有提供给您的类型。
```
pub struct U256(pub [u64; 4]);
```
四个 64 位的 limbs,采用小端序。描述起来很简单,但要想正确实现却极具挑战。这不仅仅是一个数据结构的选择 —— 这是对现代 64 位 CPU 实际处理宽整数的直接映射。
### 加法与减法 —— 手动进位传播
使用 `overflowing_add` 和 `overflowing_sub` 手动跨 limb 边界传播进位和借位。机器保持绝对的确定性,因为每一次溢出都被明确检测并转发 —— 不会留下任何未定义的行为或特定于平台的回绕规则。二进制补码处理所有有符号操作(`SDIV`, `SMOD`, `SAR`),而无需单独的有符号类型。
### 乘法 —— `u128` 编译器提示
每个 `u64 × u64` 的乘积都作为 `u128` 中间值进行计算。这不仅是为了方便 —— 这是对 LLVM 的刻意提示。通过将逻辑表示为 `u64 → u128` 的宽化乘法,您可以引导编译器将其直接映射到 x86_64 上的原生 `MULX` 指令,无需编写任何不安全的内联汇编即可实现汇编级别的性能。
**Schoolbook vs. Karatsuba:** 恰好在 256 位时,schoolbook(蛮力)乘法比 Karatsuba 更快。Karatsuba 将乘法从 O(n²) 减少到 O(n^1.585),但在这种规模下,额外的加减法所带来的开销超过了节省的收益。Karatsuba 在大得多的位宽下才划算 —— 对于 256 位 × 256 位,使用带有 `u128` 部分乘积的 schoolbook 才是正确的选择。
`MULMOD` opcode 需要 512 位的中间乘积。通过在将除数取模之前,将相同的 limb 宽化逻辑扩展到八个 64 位通道来处理这些乘积 —— 同样的代码路径被公开为 `U256::mul_full(self, rhs) -> U512`。
### 除法 —— Knuth 算法 D
除法是事情变得严肃的地方。该实现遵循了 *The Art of Computer Programming, Vol. 2* 中的 **Knuth 算法 D**。它分三个阶段运行:
1. **规范化** —— 将除数左移,直到其最高有效位被置位。这通过确保前导除数 limb 足够大以限制估计误差,从而使商数位估计变得可靠。
2. **估计与修正** —— 根据当前被除数的两个最高有效 limbs 估计一个商数位 `q̂`。该估计最多可能偏大 2,因此通过一个修正循环将其带回正确范围。
3. **去规范化** —— 将余数右移回其原始比例,以撤销第一步中的规范化。
每一步都有一个 `#[assert]`(而不是 `debug_assert!`)来保护边界条件,因此在 release 模式下的回归会显式报错,而不是静默产生错误结果。
## 2. `no_std` 环境 —— 裸机胶水
剥离 `std` 意味着手动提供 Rust 通常免费为您提供的东西。这就是让其他一切成为可能的“胶水”层。
### Lang Items
编译器要求在链接时存在某些符号。如果没有 `std`,您必须自己提供它们:
```
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {} // deterministic halt — no unwinding, no stack trace, no formatting
}
```
对于实际的裸机部署(`aarch64-unknown-none` 等),需要使用 `build-std`,并且 `alloc_error_handler` lang item 由 `alloc` crate 的默认设置提供。`src/lang_items.rs` 文件受 `not(feature = "runtime")` 控制,因此在主机上运行测试时,它不会与 `std` 的 panic 处理程序发生冲突。
### 确定性内存
除非您明确引入一个堆,否则没有 OS 堆。所有缓冲区 —— trie、journal、账户缓存 —— 要么是固定大小的数组,要么是手动管理的 `Vec`。这保证了绝对的确定性:相同的输入在任何主机架构上总是产生相同的结果,不依赖于 OS 的内存布局或 allocator 的行为。
## 3. 加密原语 —— 位级手术
以太坊的安全模型建立在两个加密基础之上:一个哈希函数和一个椭圆曲线。这里的哈希实现没有使用辅助库。
### Keccak-256 —— Sponge 构造
以太坊使用 Keccak-256 —— 而不是 NIST SHA-3 标准。它们在填充上有所不同(`0x01` vs `0x06`),这个细节以前坑过不少人。该实现遵循 **sponge 构造**:
- **吸收阶段**:输入按 rate 大小的块(Keccak-256 为 1088 位)通过 XOR 写入 1600 位的状态中
- **挤出阶段**:输出在每轮置换后被提取出来
置换运行 **24 轮**,每一轮对 5×5 的 64 位 lane 矩阵应用五个步骤映射:
| 步骤 | 职责 |
|------|------|
| **θ (Theta)** | 列奇偶校验混合 —— 将位扩散到所有列 |
| **ρ (Rho)** | 每 lane 的位旋转 —— 偏移量是规范中固定的常量 |
| **π (Pi)** | Lane 置换 —— 重新排列 5×5 矩阵的位置 |
| **χ (Chi)** | 非线性行混合 —— 唯一的非线性步骤,提供安全性 |
| **ι (Iota)** | 轮常量注入 —— 每轮打破对称性 |
每个旋转偏移量和轮常量都是直接从 [Keccak Reference v3.0](https://keccak.team/files/Keccak-reference-3.0.pdf) 中硬编码的。没有捷径,也没有借用任何外部的查找表。
## 4. 状态与序列化
### 递归长度前缀 (RLP)
RLP 是以太坊的通用序列化格式 —— 交易、区块、trie 节点、账户状态,全部都是。编码规则映射到五个前缀范围:
| 范围 | 含义 |
|-------|---------|
| `0x00–0x7f` | 单字节 —— 值即为其自身的编码 |
| `0x80–0xb7` | 短字符串(0–55 字节) —— 长度编码在前缀中 |
| `0xb8–0xbf` | 长字符串 —— “长度的长度”编码在前缀中 |
| `0xc0–0xf7` | 短列表 —— 有效负载总长度在前缀中 |
| `0xf8–0xff` | 长列表 —— “长度的长度”在前缀中 |
**强制执行严格的 RLP 极简主义。** 整数必须是没有前导零字节的大端序 —— `0x0100` 必须被编码为两个字节,而不是三个。此规则在解码时会被检查,如果违规将被拒绝。弄错了会产生不同的 trie root 并破坏共识。
### 修改版的 Merkle Patricia Trie (MPT)
世界状态、账户 storage、交易列表和收据列表都存在于 MPT 中。这是一个十六叉树 trie,结合了 Merkle 完整性(每个节点都由其 RLP 编码的哈希标识)和 Patricia 压缩(共享前缀折叠为 Extension 节点)。
#### 虚拟 `u4` Nibble 系统
Rust 操作的是 `u8` 字节,但 MPT 是十六叉的 —— 每次遍历 4 位键。**虚拟 `u4` 迭代器**将每个 32 字节的哈希拆分为 64 个 nibbles,每次产生一个 nibble。该迭代器驱动着 trie 的每一次插入、查找和删除操作。如果没有它,路径逻辑就会变成散落在代码库中的一堆乱七八糟的位移操作。
#### 四种节点类型
| 节点 | 结构 | 用途 |
|------|-----------|---------|
| **NULL** | — | 空 / 基础情况 |
| **Branch** | 17 项 | 16 个子插槽(每个十六进制 nibble 一个)+ 可选的值 |
| **Extension** | 2 项 | 共享前缀压缩 |
| **Leaf** | 2 项 | 持有值的终端节点 |
#### 节点拆分手术
当两个键共享一个前缀但在特定的 nibble 处出现分歧时,必须通过手术将 Leaf 转换为 Extension + Branch:
1. 计算现有键和新键之间的共享前缀长度
2. 在分歧点创建一个新的 Branch 节点
3. 现有的 Leaf 成为其分歧 nibble 处的 Branch 的子节点
4. 新值成为其分歧 nibble 处的子节点
5. 如果共享前缀不为空,则将 Branch 包装在 Extension 节点中
这是 trie 中结构最复杂的操作 —— 很容易弄错,而且一旦弄错就很难调试。
#### 十六进制前缀 (HP) 编码
HP 编码使用四个前缀标志解决了 nibble 级别上 Leaf 和 Extension 节点之间的歧义:
| 前缀 | 含义 |
|--------|---------|
| `0x00` | Extension,偶数长度路径 |
| `0x1_` | Extension,奇数长度路径(nibble 在低位中) |
| `0x20` | Leaf,偶数长度路径 |
| `0x3_` | Leaf,奇数长度路径(nibble 在低位中) |
#### 内联优化
RLP 编码小于 32 字节的节点将直接嵌入到其父节点中,而不是通过哈希引用存储。一个只有几个字节的节点不需要 32 字节的 Keccak 指针 —— 将其内联可以保持小规模 trie 的紧凑性,并避免不必要的哈希开销。
#### 深度受限的递归
`insert` 和 `remove` 的递归带有一个限制为 `MAX_DEPTH = 64` 的 `depth: usize` 计数器(匹配 32 字节 256 位密钥,即 trie 可以存储的最大密钥)。攻击者控制的深度嵌套的 trie 会产生 `Error::MaxDepth`,而不是发生 stack overflow。
## 5. 世界状态
`WorldState` 是账户模型状态的顶级容器:
- **账户缓存** (`HashMap<[u8;20], Option>`) —— 挂起的写入,`None` 表示删除。
- **Storage 缓存** (`HashMap<([u8;20], U256), U256>`) —— 挂起的 storage 写入,以 `(address, slot)` 为键。
- **代码缓存** (`HashMap<[u8;32], Vec>`) —— 合约字节码,以 Keccak-256 哈希为键。
- **状态 trie** + 每个账户独立的 **storage trie**(MPT 实例)。
- **Journal** —— 记录突变的只追加日志,用于 `checkpoint` / `revert`。
### Commit Pipeline
`commit()` 被分解为五个子函数,每个函数负责一个阶段:
1. `commit_storage` —— 将挂起的 storage 写入刷新到各个账户的 storage trie 中。
2. `commit_prune_deleted_storage` —— 移除已删除账户的 storage trie。
3. `commit_accounts` —— 在写入状态 trie 时应用 EIP-158 空账户修剪。
4. `commit_code` —— 将任何新看到的合约代码持久化到数据库中。
5. `commit_clear_caches` —— 清除内存中的缓存并设置新的 state root。
这使得每个阶段都可以独立测试,并保持主 `commit()` 的可读性。
### Journal 与 Checkpoint
嵌套调用(`CALL`, `DELEGATECALL`, `STATICCALL`)创建的子上下文可能会回滚。当子上下文回滚时,必须撤销其 storage 写入和余额更改 —— 但该子上下文消耗的 gas *不会* 返还给调用者。
这是通过 **journaling 系统** 处理的:
1. 在进入子上下文之前,记录一个 **checkpoint**(当前的 journal 长度)
2. 每次状态突变(storage 写入、余额更改、账户创建)都会向 journal 追加一条记录
3. 在 `REVERT` 时,从当前的末端到 checkpoint 反向重放 journal,撤销每一次突变
4. 成功时,自 checkpoint 以来的 journal 条目将被简单地丢弃
Journal 被限制在 `MAX_JOURNAL_DEPTH = 4096` —— 超过此深度,`checkpoint()` 将返回 `false`,调用者必须执行 commit 或 revert。
# 机器 (计划中)
## 6. 执行引擎
### 机器状态
EVM 是一台栈机器。完整的执行状态 μ 是一个正式的元组:
```
μ = (g, pc, m, i, s, o)
```
| 符号 | 组件 | 描述 |
|--------|-----------|-------------|
| `g` | Gas | 剩余的 gas 预算 |
| `pc` | Program Counter | 当前字节码中的索引 |
| `m` | Memory | 字节可寻址的易失性缓冲区 |
| `i` | Active Words | 以 32 字节字为单位的内存大小(用于 gas 计算) |
| `s` | Stack | LIFO,256 位字,最大深度 1024 |
| `o` | Return Data | 来自上一次子调用的输出缓冲区 |
### 执行环境 —— I 元组
机器状态 μ 描述了在当前执行上下文*内部*正在发生什么。但是 EVM 还需要知道关于*外部世界*的信息 —— 它运行在哪个区块中、谁调用了它、什么地址拥有该代码。该外部上下文通过 **执行环境元组 I** 注入:
| 符号 | 组件 | 描述 |
|--------|-----------|-------------|
| `Ia` | Code owner | 正在执行代码的账户的地址 |
| `Io` | Original transactor | 发起顶层交易的 EOA |
| `Ip` | Effective gas price | 本次交易中每个 gas 单位的 Wei |
| `Id` | Call data | 传递给当前上下文的输入数据 |
| `Is` | Caller | 直接调用者的地址 |
| `Iv` | Value | 随此调用转移的 Wei |
| `Ib` | Bytecode | 正在被执行的代码 |
| `IH` | Block header | 包含 coinbase, number, timestamp, difficulty, gas limit |
| `Ie` | Call depth | 当前的调用栈深度 |
| `Iw` | Write permission | 正常调用时为 `true`,在 `STATICCALL` 内部为 `false` |
`Iw` 标志是在静态上下文中强制实施只读语义的机制。如果 `Iw` 为 `false` 并且当前的 opcode 会修改状态 —— `SSTORE`, `LOG*`, `CREATE`, `SELFDESTRUCT` —— Z-function 将立即触发异常停机。
### Fetch-Decode-Execute 循环
每次迭代:
1. **Fetch** 从当前字节码中获取 `pc` 处的字节
2. **Decode** 将其解码为 opcode
3. **Verify** 检查 gas 可用性和栈前置条件
4. **Execute** 执行操作,更新 μ
5. **Advance** 前进 `pc` —— 除非是跳转指令
### 正式异常停机 —— Z 函数
Yellow Paper 定义了一组严格的条件,在这些条件下执行必须进入**异常停机状态** —— 消耗所有剩余 gas 并丢弃当前上下文中的所有状态突变。这些不是软错误;它们是硬停止。在执行任何指令之前,机器会检查以下所有条件:
| 条件 | 含义 |
|-----------|---------|
| `μ_g < C_cost` | 操作的 gas 不足 |
| `δ_w` 未定义 | 无效或无法识别的 opcode |
| `\|μ_s\| < δ_w` | 栈下溢 —— opcode 输入的项不足 |
| `JUMP`/`JUMPI` 目标 ∉ jump map | 目标不是有效的 `JUMPDEST` |
| `\|μ_s\| - δ_w + α_w > 1024` | 栈溢出 —— 结果将超过 1024 项的限制 |
| 在 `STATICCALL` 中执行修改状态的 opcode | 在只读上下文中尝试写入 |
如果任何条件为真,执行立即停止。不会 commit 任何部分状态。Gas 被消耗殆尽。
## 7. 资源计量
### Gas 微积分 —— 二次内存
**二次内存扩展** 阻止了基于内存的 DoS 攻击。确切的公式为:
```
G_memory(words) = 3 · words + ⌊words² / 512⌋
```
内存不是由 OS 分配的 —— 而是由 VM 计量的。二次项意味着,试图分配数 GB 内存的合约在如愿以偿之前很久就会耗尽其 gas 预算。
### 63/64 规则 (EIP-150)
调用者在将剩余 gas 转发给子调用之前,必须保留其 1/64 的 gas。这确保了父上下文始终有足够的 gas 来执行终结操作 —— 清理、emit log、写入返回数据 —— 即使子级发生回滚并消耗了分配给它的所有 gas。
虽然正式的栈深度限制是 1024 帧,但 63/64 规则施加了更为严格的*实际*限制。在大约 **340 次嵌套调用**之后,每帧转发的 gas 就会降低到低于执行任何有用操作所需的最低值。
### 分层状态访问 (EIP-2929)
一个交易范围的访问列表 —— 包含了触及过的地址和 storage slot 的 `Set`。第一次访问地址或 slot 都是“冷”的:
| 访问类型 | 冷成本 | 热成本 |
|-------------|-----------|-----------|
| 账户访问 | 2600 gas | 100 gas |
| Storage slot (`SLOAD`) | 2100 gas | 100 gas |
## 8. 解释器优化
### 函数指针调度表
dispatch 没有使用针对 256 个可能 opcode 的 `match` 语句,而是使用了**函数指针表**:
```
type Handler = fn(&mut Vm) -> Result<(), Error>;
static DISPATCH: [Handler; 256] = [ /* one entry per opcode byte */ ];
```
直接通过 opcode 字节进行索引,这提供了 O(1) 的调度,并且与分支预测器配合良好。
### 热点 Opcode 内联
`PUSH`, `DUP`, `SWAP` 和 `JUMP` 在真实合约字节码中占据了已执行指令的不成比例的份额。将这些直接内联到执行循环中 —— 在常见情况下完全绕过函数指针表 —— 减少了调用开销,并可以在热路径上产生 **30–40% 的吞吐量提升**。
### Computed Gotos & 尾调用
消除这些指令对中央调度瓶颈的影响,使得分支预测器能够顺应执行模式工作,而不是与其对抗。
## 9. 静态 Jump Map 分析
在执行开始之前,会对字节码进行预扫描以构建一个 **jump map** —— 一个标记每个有效 `JUMPDEST` (`0x5B`) 位置到位集。关键的细节是:作为 `PUSH` 数据出现的 `0x5B` 字节*不是*有效的跳转目标。
```
PUSH2 0x5B 0x5B ← these two 0x5B bytes are PUSH data, not JUMPDEST
JUMPDEST ← this one is a valid jump target
```
jump map **必须在执行开始之前静态生成** —— 在每次跳转时进行运行时检查是不够的。如果没有它,合约可能会跳转到 `PUSH` 数据的中间,并将任意字节序列作为 opcode 执行。
## 10. Secp256k1 —— 有限域算术
几乎被每个以太坊交易使用的 `ECRECOVER` 预编译需要在有限域 GF(p) 上进行真正的椭圆曲线算术运算。该字段的素数是:
```
p = 2²⁵⁶ - 2³² - 977
```
这种特定的形式使得模归约非常高效。曲线方程是 Short Weierstrass 形式:
```
y² = x³ + 7 (mod p)
```
覆盖范围将包括通过扩展欧几里得算法求模逆、点加法/加倍、通过加倍和加法进行标量乘法,以及使用 `y = x^((p+1)/4) mod p` 计算 mod p 的平方根(因为 `p ≡ 3 (mod 4)` 所以有效)。
## 11. Log 处理和 Bloom Filter
合约事件 (`LOG0`–`LOG4`) 产生结构化的 log 条目,并 commit 到交易收据中。**Bloom filter 的构建**允许在不扫描整条链的情况下进行高效的 log 搜索。对于 log 条目中的每个地址和 topic,都会在一个 2048 位的过滤器中准确设置 **3 位**。位的位置是根据 Keccak-256 哈希前三对字节中每一对的低 11 位确定的。
## 12. 现代化 EIP 支持 —— Cancun 及后续版本
### Transient Storage —— EIP-1153
`TSTORE` 和 `TLOAD` 操作在仅存在于交易期间的 storage 上 —— 开始时初始化为零,结束时丢弃。这是 DeFi 中用于重入保护的最干净的解决方案。
### EVM 对象格式 —— EIP-3540 / EIP-3541
EOF 在容器级别将代码与数据分开,具有声明代码段、数据段和类型信息的结构化标头。EIP-3541 拒绝了字节码以 `0xEF` 开头的新合约,为 EOF 容器格式保留了该字节。
## 13. 未来研究 —— 并行执行
**乐观并发控制 (OCC)** —— 乐观地假设没有冲突,并行执行独立的交易。执行完毕后,检查是否存在状态冲突;仅串行地重新执行产生冲突的交易。
**延迟更新** —— 对区块受益人账户的 gas 支付将推迟到区块结束时进行,而不是在每次交易之后应用,从而消除了最常见的误报冲突源之一。
**流水线化 Merkle 化** —— 将交易执行与 Merkle Patricia Trie 的重新哈希重叠进行,将 Merkle 化延迟隐藏在执行延迟之后。
**推测性预热** —— 乐观地在并行中推测性地执行交易,以*预测*将要访问哪些 storage slot 和账户,并在正式执行开始之前将它们加载到热访问集合中。
## 100% 正确性法则
因为这是一个裸机且共识至关重要的实现,所以不存在“基本正确”的情况。有一些特定的不变量必须无条件成立:
- **严格的 RLP 极简主义** —— 整数必须是不带前导零的大端序。哪怕多出一个零字节都会产生不同的 trie root,并破坏与网络上其他节点的共识。
- **Jump map 安全性** —— jump map 必须在执行开始之前静态生成。`PUSH` 数据中的 `0x5B` 字节不是有效的跳转目标。
- **异常停机** —— 必须在每一条指令之前检查所有六个 Z-function 条件。
- **`no_std` lang items** —— 必须为裸机目标提供自定义的 `panic_handler`。
- **Gas 核算** —— 每一个 opcode、每一次内存扩展、每一次状态访问都必须完全按照 Yellow Paper 和相关 EIP 规定的标准收取 gas。
## 实施路线图
一层一层地构建,每一层都只依赖于它下面的层:
- [x] **第 0 层 —— 原始算术**:`U256`/`U512`,使用 `u128` limb 宽化的 schoolbook 乘法(`MULX` 提示),Knuth 算法 D 除法,二进制补码,`no_std` panic 处理程序
- [x] **第 1 层 —— 序列化与 Sponge**:递归 RLP 编码/解码器(五个前缀范围,严格的极简主义),HP nibble 编码(四个前缀标志),按位 Keccak-256(24 轮 sponge 置换,正确的以太坊填充)
- [x] **第 2 层 —— 状态管理**:修改版的 MPT(虚拟 `u4` nibble 迭代器,四种节点类型,节点拆分手术,内联优化,深度受限的递归),账户模型,世界状态 σ,状态 journaling 与 checkpoint/回滚
- [x] **第 3 层 —— 资源计量**:二次内存 gas 公式,EIP-2929 冷热访问集合,用于子调用 gas 转发的 EIP-150 63/64 规则
- [ ] **第 4 层 —— 引擎核心**:函数指针 dispatch 表,静态 jump map 预扫描器,Z-function 异常停机检查,热点 opcode 内联(`PUSH`/`DUP`/`SWAP`/`JUMP`),fetch-decode-execute 循环,log 处理和 bloom filter 构建
- [ ] **第 5 层 —— 合规性与预编译**:所有约 144 个 opcode,`ECRECOVER`(GF(p) 上的 Secp256k1 点算术),`SHA256`,`RIPEMD160`,`IDENTITY`,`MODEXP`,BN256 操作,transient storage(`TSTORE`/`TLOAD`),EOF 容器验证(EIP-3540/3541),类型化交易(EIP-2718)
## 参考文献
- [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) —— 整个项目构建所基于的正式规范
- [FIPS 202](https://csrc.nist.gov/publications/detail/fips/202/final) —— SHA-3 / Keccak sponge 标准(注意填充与以太坊的 Keccak-256 有所不同)
- Knuth, D.E. — *The Art of Computer Programming, Vol. 2: Seminumerical Algorithms* — 算法 D(多精度除法)
- [The Keccak Reference v3.0](https://keccak.team/files/Keccak-reference-3.0.pdf) —— 旋转偏移量、轮常量、sponge 参数
- [EIP-150](https://eips.ethereum.org/EIPS/eip-150) —— 针对重 IO 操作的 gas 成本更改;用于子调用 gas 转发的 63/64 规则
- [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) —— 类型化交易信封
- [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929) —— 访问列表与冷热 storage 定价
- [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153) —— Transient storage (`TLOAD` / `TSTORE`)
- [EIP-3540](https://eips.ethereum.org/EIPS/eip-3540) —— EVM 对象格式 (EOF) v1
- [EIP-3541](https://eips.ethereum.org/EIPS/eip-3541) —— 拒绝以 `0xEF` 开头的新合约
- [EIP-161](https://eips.ethereum.org/EIPS/eip-161) —— 状态 trie 清理;定义了何时将账户视为“空”并符合删除条件
*凭借着耐心、固执以及对位运算的极度偏爱构建而成。*
标签:EVM, Rust, 以太坊, 区块链, 可视化界面, 密码学, 嵌入式开发, 手动系统调用, 网络流量审计, 请求拦截, 通知系统