部署合约前,地基是谁的
以太坊的合约地址有两种来历:外部账户按发送序号算出来的,或者合约里的 CREATE 指令在运行中算出来的。绝大多数情况下算出来的地址是一块生地。但规则书上一直悬着一个边角问题:如果算出来的地址上已经住了一个账户——有代码,或者 nonce 不是零——这场部署该怎么办?2017 年 8 月 15 日,Yoichi Hirai 提交 EIP-689 给出判决:撞上了就失败,当作部署代码自己跑崩处理。提案状态停在 Stagnant(停滞),可它的规则精神早已是共识测试的默认判据。本文讲这场无人区官司的案卷。

两种读法的分歧
分歧的源头在早期黄皮书的表述:即便目标地址已有代码,合约创建也从空代码与初始 nonce 开始执行。这句话的字面意思吓人——旧代码被暂时覆盖,init code 跑完若回滚再靠状态回退把旧代码还原。共识测试集里恰好有一批用例专门构造部署撞地址的场景,各家客户端在两种读法下跑出不同结果:有的直接判失败,有的真走了覆盖加回退的路。689 的规格于是写得斩钉截铁:从任意区块高度起,若创建目标账户的 nonce 非零或代码非空,则创建失败,效果等同 init code 遇到异常停机;无论是创建交易还是 CREATE 指令触发,一视同仁。
为什么协议宁可判死也不覆盖
选择失败语义的理由有三层。工程层:实现覆盖加回退的客户端要为一条几乎永不触发的路径维护代码分支,理由一节说为通过测试而实现永不使用的特性并不划算。语义层:失败语义立下一条干净的不变量——非空代码永远不会被创建流程改写, 审计一个地址时不必担心它的代码在部署事故里换过人。生态层:假想一个能覆盖已有合约代码的创建路径,等于在协议里留一扇谁摸到哈希巧合谁就能换掉别人合约的门,即便这扇门理论上需要 keccak256 碰撞才打得开。与 1352 那种先立规矩不加检查的风格不同,689 是反过来:规则本身即是检查。地址碰撞在密码学上的困难度可以对照哈希函数的一般讨论,若碰撞真的发生,那将是比任何一条链上规则都大的事件。
提案自己交的底
动机一节罕见地自我降噪:本提案对主网历史没有实际意义,只简化测试与推理。它写道,在 2019 年的君士坦丁堡升级之后该规则已被其引用的 EIP-86 所含改动吸收——那个编号在账户抽象史上以给签名与发件人画草图出名,见 签名和序号不必只有一种写法:EIP-86 为账户抽象画的第一张草图——而在更早之前,差异只在 keccak256 哈希碰撞的情形下才可见。换句话说,这是一份把既成事实写清楚、顺便删掉客户端里僵尸分支的整理型提案。兼容性一节只有一句:对主网向后兼容。测试用例一节点名了一个具体用例文件,供客户端自证实现路径。提案没有指定激活区块,因为规格用了一个恒真的条件去覆盖全部区块高度——这是考古级提案里少见的写法,也解释了它为何停在 Stagnant:没有部署日程可言。
地址算得越花,这条规则越值钱
今天的部署工具比 2017 年复杂得多:CREATE2 允许用盐值预先计算地址,工厂合约把同一段代码部署到多条链的同一片地址上,相关核对方法见 合约地址对不上?CREATE2 确定性部署与同地址不同代码。地址越是可以人为挑选,撞进别人地盘的工程想象就越多:同一个盐在两条链上可能一边生地一边熟地;跨链部署脚本若拿错链号,算出的地址在目标链上未必空置。689 的规则在这些场景里充当最后一道地板:撞了就整笔失败,不存在半个合约覆盖另半个合约的中间态。审计报告核对部署路径时,也常把目标地址是否空置列为前置检查项——这一步的理论出处正是这类早期语义澄清。
怎么给这类小提案归档
读 689 式的规则澄清提案,先分类型:它不新增功能、不改参数,只是把两种实现读法里更保守的一种写成条文。判它的价值不看状态词,看它立住的不变量有没有活下来——非空代码不被创建覆盖这条,活过了它的提案本身。给工具开发者的提醒则很实际:任何部署脚本在模拟阶段就该检查目标地址的 nonce 与代码是否为空,把协议层的失败前移到本地报错,比在链上烧掉一笔 Gas 才收到回滚体面得多。
风险提示:本文为协议历史分析,不构成投资建议;执行合约部署前请在测试网完整模拟,并核验目标网络与地址计算参数。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。