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 中的零依赖实现 [![License: CC-BY-SA-4.0](https://img.shields.io/badge/License-CC--BY--SA--4.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/) [![Rust 1.85+](https://img.shields.io/badge/rust-1.85%2B-blue.svg)](https://www.rust-lang.org) [![no_std](https://img.shields.io/badge/no__std-supported-green.svg)](https://docs.rust-embedded.org/book/intro/no-std.html) [![zero deps](https://img.shields.io/badge/runtime_dependencies-zero-success.svg)]() ## 状态 | 组件 | 状态 | |---|---| | **第 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, 以太坊, 区块链, 可视化界面, 密码学, 嵌入式开发, 手动系统调用, 网络流量审计, 请求拦截, 通知系统