返回论坛

EVM 深入探析(四):从区块头到合约存储的技术溯源

查找币 学术研究 安全研究 Web3安全 区块链安全

查找币安全研究院

钱包恢复评估 | 链上取证分析 | Web3 事件响应
以合法授权、证据保全、隐私保护和可复核流程为前提,不要求用户在线提交完整私钥或助记词。

查看研究院 研究报告中心
**查找币安全团队技术研究** --- ## 引言 在“EVM 深入探讨”系列的第三部分中,我们深入剖析了合约存储的运作机制。本期,我们将视角提升至更宏观的层面——探究单个合约的存储如何融入以太坊链的“世界状态”。我们将系统性地梳理以太坊链的架构、核心数据结构,以及 Go Ethereum(Geth)客户端的内部实现逻辑。 本文的技术路径从以太坊区块头开始,逆向追溯至特定合约的存储区域,并最终聚焦于 Geth 中 SSTORE 与 SLOAD 操作码的实现细节。这一过程将帮助我们构建对 EVM 执行环境的完整认知。 --- ## 一、以太坊区块架构 以太坊的区块结构看似复杂,实则遵循严谨的层次化设计。下图清晰展示了以太坊区块的组成部分: ``` 区块 N ├── 区块头 (Header) │ ├── Prev Hash │ ├── Nonce │ ├── Timestamp │ ├── Uncles Hash │ ├── Beneficiary │ ├── LogsBloom │ ├── Difficulty │ ├── Extra Data │ ├── Block Num │ ├── Gas Limit │ ├── Gas Used │ ├── Mix Hash │ ├── State Root ← 核心追踪目标 │ ├── Transaction Root │ └── Receipt Root ├── 交易列表 (Transactions) └── 叔块列表 (Uncles) ``` ### 1.1 区块头关键字段解析 区块头包含以太坊区块的核心元数据,各字段含义如下: | 字段 | 描述 | |------|------| | **Prev Hash** | 父区块的 Keccak 256 哈希值 | | **Nonce** | 满足工作量证明(PoW)的随机数 | | **Timestamp** | 区块写入时的 UNIX 时间戳 | | **Uncles Hash** | 叔块列表的 Keccak 哈希 | | **Beneficiary** | 矿工费接收地址 | | **LogsBloom** | 从交易回执中提取的 Bloom 过滤器 | | **Difficulty** | 当前区块的挖矿难度 | | **Extra Data** | 矿工自定义的 32 字节数据 | | **Block Num** | 区块高度 | | **Gas Limit** | 区块允许消耗的最大 Gas 量 | | **Gas Used** | 区块内交易实际消耗的 Gas 总量 | | **Mix Hash** | 与 Nonce 配合验证 PoW 的哈希值 | | **State Root** | 执行所有交易后的状态树根哈希 | | **Transaction Root** | 交易树的根哈希 | | **Receipt Root** | 回执树的根哈希 | 在 Geth 客户端中,区块头对应的数据结构定义在 `core/types/block.go` 的 `Header` 结构体中,其字段与上表完全对应。 ### 1.2 State Root:核心追踪入口 State Root 本质上是一个 Merkle Patricia Trie(默克尔帕特里夏树)的根哈希。该树存储了以太坊网络上所有账户的键值对,其中: - **Key**:以太坊地址的 Keccak 256 哈希值 - **Value**:RLP 编码的以太坊账户对象 State Root 的关键特性在于:任何底层数据的变更都会导致根哈希发生改变,从而确保状态的一致性与可验证性。 --- ## 二、以太坊账户模型 以太坊账户是共识层对地址的抽象表示,每个账户由以下 4 个字段组成: | 字段 | 描述 | |------|------| | **Nonce** | 交易计数器(外部账户)或合约创建计数器(合约账户) | | **Balance** | 账户余额(单位:Wei) | | **StorageRoot** | 合约存储树的根哈希(仅合约账户有效) | | **CodeHash** | 合约代码的 Keccak 256 哈希(仅合约账户有效) | 在 Geth 中,账户对象由 `stateObject` 结构体表示,该结构体封装了账户的完整状态信息。 ### StorageRoot:合约存储的入口 StorageRoot 指向一个独立的 Merkle Patricia Trie,该 Trie 存储了合约的持久化存储数据。其键值对映射关系为: - **Key**:存储插槽的 Keccak 256 哈希值 - **Value**:存储值的 RLP 编码 这意味着合约的每个存储插槽都通过哈希映射到 Trie 中的唯一位置,而 StorageRoot 则是这棵树的根哈希。 --- ## 三、Geth 中的状态管理 ### 3.1 StateDB 接口 Geth 的核心状态管理通过 `StateDB` 接口实现,该接口提供了对账户状态和存储的完整操作。主要方法包括: - `GetBalance(addr) *big.Int`:获取账户余额 - `GetNonce(addr) uint64`:获取账户 Nonce - `GetState(addr, hash) common.Hash`:获取指定存储插槽的值 - `SetState(addr, hash, value)`:设置指定存储插槽的值 ### 3.2 状态分层架构 Geth 采用三层存储架构来管理状态变更: ``` ┌─────────────────┐ │ dirtyStorage │ ← 当前交易内的临时修改(内存) ├─────────────────┤ │ pendingStorage │ ← 当前区块内的待提交修改 ├─────────────────┤ │ originStorage │ ← 已持久化的底层存储(数据库) └─────────────────┘ ``` - **dirtyStorage**:存储当前交易执行过程中尚未提交的修改 - **pendingStorage**:存储当前区块内已提交但尚未写入数据库的修改 - **originStorage**:从底层数据库读取的已持久化数据 这种分层设计确保了交易隔离性和区块原子性:同一区块内的交易可以共享 pendingStorage,但 dirtyStorage 在每笔交易开始时重置。 --- ## 四、SSTORE 与 SLOAD 操作码实现 ### 4.1 SLOAD 执行流程 SLOAD 操作码用于从合约存储中读取指定插槽的值,其核心逻辑如下: 1. **输入**:从栈顶弹出存储插槽的索引(32 字节) 2. **计算**:对索引进行 Keccak 256 哈希,得到存储键 3. **查询顺序**: - 首先检查 `dirtyStorage`,返回最新修改值 - 若未命中,检查 `pendingStorage`,返回区块级修改值 - 若仍未命中,检查 `originStorage`,从数据库读取原始值 4. **输出**:将查询结果压入栈顶 ### 4.2 SSTORE 执行流程 SSTORE 操作码用于向合约存储中写入指定插槽的值,其核心逻辑如下: 1. **输入**:从栈顶依次弹出存储插槽索引和新值 2. **计算**:对索引进行 Keccak 256 哈希,得到存储键 3. **写入**:将键值对写入 `dirtyStorage` 4. **Gas 计算**:根据存储状态变化计算 Gas 消耗(包括冷存储访问、值变更、清理返还等) ### 4.3 存储状态转换 Geth 通过 `StorageState` 枚举跟踪每个存储插槽的状态: - **StorageUnchanged**:未修改 - **StorageAssigned**:已分配新值 - **StorageDeleted**:已删除(设置为零值) 这种状态跟踪机制使得 Gas 计算更加精确,并能有效处理存储退款(Gas Refund)。 --- ## 五、完整技术链路总结 从区块头到合约存储的完整技术链路如下: ``` 区块头 → State Root → Merkle Patricia Trie → 账户对象 → StorageRoot → 存储 Trie → 存储插槽 ``` 在 Geth 执行层面: ``` SLOAD/SSTORE → StateDB.GetState/SetState → stateObject → dirtyStorage → pendingStorage → originStorage → LevelDB ``` --- ## 结语 本文深入剖析了以太坊从区块头到合约存储的完整技术链路,涵盖了 Merkle Patricia Trie、账户模型、状态分层管理以及 SSTORE/SLOAD 操作码的 Geth 实现细节。理解这一架构对于智能合约安全审计、Gas 优化以及区块链底层开发均具有重要的实践意义。 下一篇文章中,我们将探讨 CALL 与 DELEGATECALL 操作码的技术细节及其在合约交互中的应用。 --- **本文由查找币安全团队整理发布**
在论坛中查看和回复