
EIP-8253 深度解析通过一次性 nonce 提升消除零 nonce 存储账户的合约创建碰撞风险【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs本文基于 EIPs 仓库中的 EIP-8253 规范 及其配套资产文件编写。EIP-8253 提出了一种不规则状态迁移irregular state transition在指定分叉区块起始将一小批零 nonce、空代码、非空存储的主网账户的 nonce 统一提升为1从而永久化解 EIP-684 下的CREATE/CREATE2地址碰撞风险且无需像 EIP-7610 那样在每次合约创建时付出永久的运行时存储检查成本。读完本文你将理解这类历史遗留账户形状的成因、目标账户清单的构建与复现方法论、分叉状态迁移的精确定义以及它与 Block-Level Access ListsBAL等前沿提案的交互方式。一、问题背景被 Spurious Dragon 遗留的账户形状1.1 EIP-684 的碰撞防护与漏洞缺口EIP-684Revert creation in case of collision规定当一次合约创建无论是合约创建交易、CREATE还是CREATE2的目标地址已经具有非零 nonce或非零代码长度时创建必须像 init code 首字节是非法操作码一样回滚。该规则追溯适用于所有历史区块目的是保证已部署合约代码的不可变性防止攻击者通过在已有地址上再创建的方式覆盖代码。然而EIP-684 的防护并非完备。它的检查条件只覆盖了 nonce 与代码长度没有覆盖存储。理论上存在一种账户形状可以绕过它地址的 nonce 为0、代码为空、但存储非空。此时 EIP-684 不会拦截创建攻击者理论上可以在这个地址上部署任意代码覆盖或接管该地址原本持有的存储数据。1.2 这种账户是怎么产生的问题的根源在 EIP-161Spurious Dragon分叉块高度2,675,000之前的旧创建语义当时CREATE在 init code 执行之前不会递增新合约账户的 nonce。因此如果一个构造函数的 init code 写了存储槽但最终没有返回任何部署字节码产出的账户就是nonce 0codeHash keccak256()即EmptyCodeHashstorageRoot ! keccak256(RLP())即EmptyRootHashEIP-161 之后两条创建路径CREATE与合约创建交易都会在 init code 运行前先把新合约的 nonce 递增到1因此这个集合是封闭的Spurious Dragon 之后不会再有任何新账户进入这一形状。EIP-8253 要处理的正是这批封闭集合中的历史遗留账户。二、方案对比EIP-7610 的运行时检查 vs EIP-8253 的一次性迁移针对上述缺口社区提出了两条技术路线EIP-7610Revert creation in case of non-empty storage在 EIP-684 的基础上追加一个条件创建目标地址若已有非空存储则创建必须回滚。代价是需要在运行时的每次合约创建路径中增加一次额外的存储检查对部分客户端/数据库布局而言还需要额外的一次磁盘读取这笔成本是永久性的——它将一直为一个 Spurious Dragon 之后不再增长的封闭地址集合买单。EIP-8253则选择釜底抽薪既然目标是封闭的、数量已知的账户集合当前主网仅28 个不如在分叉块起始做一次一次性的共识迁移把这些账户的 nonce 统一从0提升为1。迁移之后这些账户自动满足 EIP-684 的拦截条件nonce ! 0未来任何CREATE/CREATE2指向它们都会回滚——完全不需要运行时存储检查。这一思路与 EIP-7523Empty accounts deprecation异曲同工EIP-7523 通过清理空账户来退役 EIP-161 touch 边角情况在重执行与测试逻辑中的负担EIP-8253 则通过移除零 nonce、无代码、非空存储这一账户形状退役它在后续 EIP 分析与测试用例中持续施加的负担确保未来状态中不存在可以在一个零 nonce、无代码但存储非空的地址上创建合约的理论路径。三、规范详解SpecificationEIP-8253 的规范全文使用 RFC 2119 的关键词MUST / MUST NOT 等表述约束。3.1 主网目标账户清单被迁移的主网账户发布在assets/eip-8253/targeted-accounts.json共28 条记录。每条记录包含address20 字节地址preimageaddressHash该地址的keccak256哈希MPT 实际使用的键balance当前余额十六进制 weicodeHashkeccak256()即EmptyCodeHash0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470storageRoot非空存储根! EmptyRootHashstorage该账户的非零存储槽列表槽 key 及其哈希作为storageRoot ! EmptyRootHash谓词的佐证材料——状态迁移本身不使用这些槽信息currentStorageHash通过eth_getProof从latest状态获取的当前存储哈希发布文件额外附加的字段。以清单中第一个账户为例{ address: 0xf468bcbc4a0bfdb06336e773382c5202e674db71, addressHash: 0x1dff70804724888a5e9a0de818749dd6373791aee04456d72d4ee4d93f7a71d5, balance: 0x186cc6acd4b0000, codeHash: 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, storageRoot: 0xf5110f2debd306f955dec5946fb284a43eba5d8ec108adb26848b2b45385aee6, storage: [ { key: 0x0000000000000000000000000000000000000000000000000000000000000000, keyHash: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 } ], currentStorageHash: 0xf5110f2debd306f955dec5946fb284a43eba5d8ec108adb26848b2b45385aee6 }清单中账户的存储规模差异很大有的只有 1 个存储槽如0xf468bcbc...、0x3311c080...而像0x2c081ed1949d7dd9447f9d96e509befe576d4461这样的账户则有超过 20 个非零存储槽。值得注意的是0xadd92e0650457c5db0c4c08cbf7ca580175d33d2的余额仅为0x11 wei——这类低余额账户之所以能存活至今是因为 EIP-161 的空账户判定只看无代码、零 nonce、零余额而忽略存储所以它们只有在被touch时才会被删除。清单的构建过程与验证方法基于latest的eth_getProof校验记录在assets/eip-8253/methodology.md中Spurious Dragon 分叉块处、EIP-161 删除动作之前的224 条超集记录则发布在assets/eip-8253/zero-nonce-matches.jsonl每行一个 JSON 对象。3.2 状态迁移规则在分叉块起始、任何预执行系统合约调用例如 EIP-2935 的历史区块哈希、EIP-4788 的信标根、EIP-7002 的执行层触发提款、EIP-7251 的合并质押相关调用之前、以及任何交易之前对清单中的每个账户AA的 nonce必须被设置为1A的balance、codeHash、storageRoot不得被修改。该迁移不产生任何交易、收据或日志也不消耗任何 gas。迁移后这些账户仍然保持看起来像合约的形态nonce 1正是标准已部署合约的 nonce 形状只是没有代码——这是一种以太坊本来就可表示的合法状态如果你部署一个写了存储、但只返回 1 字节代码的合约得到的账户就是非空存储、nonce 1、无代码。3.3 非主网链的适用方式从主网在 Spurious Dragon 之后分叉出去的链可以直接复用同一份 28 账户清单其他链需要针对自己的状态生成清单同时列出地址 preimage 与哈希从未在创世或历史上产生过此类账户的链清单为空可以MAY完全忽略本 EIP。3.4 与 EIP-7928Block-Level Access Lists的交互如果分叉块激活了 EIP-7928Block-Level Access ListsBAL则 BAL 必须在block_access_index 0处编码这些变更且排序在任何预执行系统合约调用之前。对每个目标账户A其AccountChanges条目为address Anonce_changes单个NonceChange[0, 1]block_access_index 0new_nonce 1storage_changes、storage_reads、balance_changes、code_changes全部为空。并且要求将 BAL 重新应用到分叉前状态根必须能复现分叉后的状态根。这样区块构建者无需重新执行不规则迁移仅凭索引 0 处的 BAL 条目即可正确计算状态根。在 EIP-7928 的 RLP 编码模式address - field - block_access_index - change下nonce 提升恰好对应每个账户一条NonceChange其他字段均不变编码非常干净。四、目标账户的枚举方法论assets/eip-8253/methodology.md详细记录了清单的构建与验证方法以下是其核心内容。4.1 谓词Predicate目标账户必须同时满足nonce 0codeHash keccak256()EmptyCodeHashstorageRoot ! keccak256(RLP())EmptyRootHash。这类账户只可能由 Spurious Dragon 之前的合约创建CREATE操作码或合约创建交易产生且其 init code 写了存储并返回了零字节部署代码。EIP-161 之后创建路径都会先递增 nonce因此该集合封闭不会增长。4.2 两阶段枚举匹配集合会随时间收缩EIP-161 的空账户谓词无代码、零 nonce、零余额不看存储所以处于该形状且余额为零的账户会在下一次touch时被删除。设S(b)为区块b处的匹配集合则有S(2,675,000) ⊇ S(latest)。枚举分两步边界扫描在 Spurious Dragon 激活块2,675,000处扫描——这是该群体最早处于封闭状态的时刻分叉块本身无需检查因为它已经把可能的合约 nonce 置为 1。输出224 条即zero-nonce-matches.jsonllatest过滤通过eth_getProof(address, [], latest)只保留当前活跃状态仍满足谓词的地址。输出28 条即targeted-accounts.json。依据 EIP-7523每个幸存者都必然具有非零余额。4.3 高层流程整个流程与具体客户端实现无关可概括为从创世重放到 Spurious Dragon 分叉块2,675,000配置为按 preimage原始 20 字节地址与 32 字节槽键记录账户及存储键而非只记录 MPT 所依赖的keccak256哈希。地址 preimage 是构造 BAL nonce 提升条目所必需的槽 preimage 仅作为该账户满足storageRoot ! EmptyRootHash的佐证状态迁移不需要。在分叉块后状态处遍历所有账户保留满足谓词的账户对每个匹配项枚举其非零存储槽同时记录槽键及其哈希。用eth_getProof(address, [], latest)对照当前主网状态过滤仅当活跃响应仍满足谓词时保留该条目。幸存条目构成该 EIP 的规范性清单。值得强调的是仅凭第 3 步——以已发布的zero-nonce-matches.jsonl为输入——就可以廉价地重新计算幸存者集合从任意 RPC 即可运行第 1、2 步只用于复现边界块超集本身。4.4 边界扫描的 Geth 实现细节扫描实现在 Gethv1.13.15的一个 fork 上这是最后一个带 PoW 执行与 Era1 导入的release/1.13版本v1.14无法在没有terminalTotalDifficulty的情况下引导主网前 Spurious Dragon 链。扫描器以Preimages true从创世重放主网至区块 2,675,000快照遍历时可恢复未哈希的地址与存储键并开启归档模式TrieDirtyDisabled true崩溃最多丢失一个区块在边界状态根处通过snapshot.New(NoBuild: false, AsyncBuild: false)构建快照遍历扁平快照键布局SnapshotAccountPrefix a、SnapshotStoragePrefix o对每个匹配谓词的SlimAccount反转地址哈希并通过rawdb.IterateStorageSnapshots逐账户遍历存储、反转每个槽哈希。输出格式每行一个 JSON 对象即 jsonl{ address: 0xabc…, // 20-byte preimage addressHash: 0x4f2c…, // keccak256(address) balance: 0x…, codeHash: 0xc5d2…a470, // EmptyCodeHash storageRoot: 0x71b9…, // ! EmptyRootHash storage: [{ key: 0x…, keyHash: 0x… }, …] }已发布的targeted-accounts.json采用相同模式额外附加了来自eth_getProof的currentStorageHash字段并打包为 JSON 数组。4.5 复现命令端到端复现扫描需要上文提到的 Geth fork分支remove-account-with-state-which-is-not-eoa-Geth-v-1-13-15提交96aa3bbea2e85037de79e6b38e341281646a32bf# 1. 获取 Era1 归档任何现代 geth 发行版或手动下载均可 geth-1.17 --datadir /tmp/era download-era \ --era.server https://data.ethpandaops.io/era1/mainnet/ --block 0-2675000 # 2. 构建扫描分支 go build -o ./build/bin/geth ./cmd/geth # 3. 重放 快照 扫描重放约 9 小时快照生成约 1.5 小时合计约半天 ./build/bin/geth --datadir /tmp/zn snapshot find-zero-nonce-replay \ /tmp/era/geth/chaindata/ancient/chain/era # 4. 过滤 验证存储根 go build -o ./verify-storage-root ./cmd/verify-storage-root cat zero-nonce-matches.jsonl | ./verify.sh targeted-accounts.jsonl jq -s . targeted-accounts.jsonl targeted-accounts.json扫描在每次快照迭代上是确定性的在干净的 datadir 上运行会得到相同的(address, slot_key)对。4.6 验证Verification对主网latest执行两项检查存活过滤eth_getProof(address, [], latest)保留仍满足谓词的 28 个账户即从S(2,675,000)收缩到S(latest)存储根重建对每个幸存者用eth_getProof(address, [our_slot_keys], latest)取回活跃值按(keccak256(slot), rlp(stripLeadingZeros(value)))重建存储 MPT并与主网报告的storageHash比对。全部 28 个幸存者均验证通过证明槽列表完整且与活跃状态一致。五、设计取舍Rationale5.1 不规则状态迁移 vs 运行时检查EIP-7610 将问题状态留在原地并永久地为封闭地址集合付出每次创建的成本EIP-8253 则选择一次性的共识变更来消解这与以太坊此前清理历史状态的模式一致如 EIP-7523。5.2 提升 nonce vs 清空存储清空每个目标账户的存储是被认真考虑过的备选方案但它更侵入每个账户至少要删除 1 个、多则多个存储槽对主网而言这意味着28 个账户上的 129 个存储条目需要被清除而 nonce 提升只需28 次 nonce 变更清空存储需要重新计算storageRoot并应得到空存储根比简单地修改账户 RLP 字段中的 nonce从 0 改为 1算法更复杂。提升 nonce 是能化解 EIP-684 碰撞风险的最小可能变更每个账户仅一次标量更新。迁移后账户仍呈已部署合约形态nonce 1、无代码该形态以太坊本就可表示。5.3 更小的测试面迁移前协议必须处理一种 Spurious Dragon 之后任何路径都无法再产出的账户形状零 nonce、无代码、非空存储。从未来状态中移除这一形状可以退役它在重执行逻辑与 EIP 测试用例中的边角情况负担。5.4 BAL 编码nonce 提升恰好是每个账户一条NonceChangeA的其他字段均不变。因此 BAL 让区块构建者可以在索引 0 处发出这些变更无需重执行不规则迁移即可正确计算状态根。六、向后兼容性这是一项共识层变更需要硬分叉。目标账户代码为空、不可作为合约调用、也没有关联密钥分叉后它们变为nonce 1、无代码、余额与存储保持不变。未来指向这些地址的CREATE/CREATE2将改为在 EIP-684 下回滚因为nonce ! 0而不是在 EIP-7610 的存储检查下回滚——对外可观察的结果完全相同。七、测试用例EIP-8253 定义了 5 组测试用例Nonce 提升A具有空代码、零 nonce、余额b、非零存储S。分叉块后A变为 nonce1余额b、空代码、存储S不变。CREATE 回滚指向A的CREATE/CREATE2在 EIP-684 下回滚nonce ! 0。无本 EIP 时同一操作在仅有 EIP-684 时会成功、或在 EIP-7610 下回滚。该测试作为分叉块的第一笔交易执行。CALL 不变对A的CALL仍不收取新建账户 gas因为A仍在 trie 中调用成功返回且无可观察效果空代码。非目标账户不受影响有代码、非零 nonce 或不在清单中的账户均不被触碰。BAL 正确性若激活 EIP-7928对每个目标账户BAL 只有单条NonceChange[0, 1]其余变更列表为空重放 BAL 可复现后状态根。八、参考实现规范给出的核心实现非常简洁——遍历清单并设置 nonce显式声明不修改其他字段def apply_irregular_state_transition(state, account_list): for entry in account_list: state.set_nonce(entry.address, 1) # balance, codeHash, and storageRoot are intentionally not modified.清单的构建过程谓词、Spurious Dragon 之前区块的重执行、快照扫描、eth_getProof验证在assets/eip-8253/methodology.md中描述。已发布的清单必须与在分叉块高度对主网运行该过程得到的输出字节级一致byte-equal——这是共识正确性的硬性要求。九、安全考量清单正确性无论是遗漏条目还是混入多余条目都会影响共识。清单可通过methodology.md中的流程从规范状态复现在排定分叉前必须进行多客户端与外部验证。无供应量变化余额与存储均被保留仅提升 nonce不改变任何资产的归属或数量。创世账户不可能属于此类这一事实无法通过重执行捕获重执行从创世开始但创世状态由配置文件直接给定但快照扫描会将其捕获。十、配套文件与延伸阅读EIP 规范正文EIPS/eip-8253.md28 条目标账户规范性清单assets/eip-8253/targeted-accounts.json224 条边界块超集assets/eip-8253/zero-nonce-matches.jsonl枚举与验证方法论assets/eip-8253/methodology.md相关提案EIP-684创建碰撞回滚、EIP-161Spurious Dragon 状态清理、EIP-7610非空存储回滚本 EIP 的替代方案、EIP-7523空账户废弃、EIP-7928区块级访问列表BAL 编码交互理解 EIP-8253 的关键在于把它视为一次有界的历史债务清偿用一个事先可完全枚举、可复现、可验证的一次性迁移替换掉一个永久性的运行时检查。对于关心以太坊执行层状态管理、硬分叉状态转换设计以及客户端共识实现的开发者这套封闭集合 不规则迁移 方法论复现的组合拳提供了一个值得借鉴的完整范式。【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考