EIP-2937 详解:SET_INDESTRUCTIBLE 操作码与以太坊合约不可销毁机制
【免费下载链接】EIPs The Ethereum Improvement Proposal repository 项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
导读
EIP-2937(SET_INDESTRUCTIBLE opcode)是 2020 年 9 月由 Vitalik Buterin 提出的以太坊 Core 类标准提案,其核心目标是引入一个新的 EVM 操作码 SET_INDESTRUCTIBLE (0xA8),让合约能够主动声明自己"不可销毁",从而在协议层面禁止该合约后续执行 SELFDESTRUCT (0xFF)。该提案最初是 EIP-2938(Account Abstraction,账户抽象)的重要配套方案——用于保证账户抽象交易目标合约代码的永久存在,防止通过 SELFDESTRUCT 销毁合约导致 nonce 重放等安全问题;同时也适用于任何希望向用户承诺"代码将永久原样存在"的库合约场景。阅读本文后,你将理解该提案的完整技术规范(新增全局变量、操作码语义、gas 成本)、它为何选择全局变量而非局部变量(DELEGATECALL 问题)、它与 EIP-2938 账户抽象方案的耦合关系,以及它在以太坊 SELFDESTRUCT 演化史(EIP-6 → EIP-6049 → EIP-6780)中所处的位置与最终走向。
说明:该提案当前状态为 Stagnant(停滞),未被纳入任何主网分叉。本文基于 EIPS/eip-2937.md 原文及仓库内相关 EIP(EIPS/eip-2938.md、EIPS/eip-6780.md、EIPS/eip-6049.md、EIPS/eip-2681.md、EIPS/eip-6.md)整理,文中实现细节均为提案层面的设计描述,而非已上线的主网行为。
背景:SELFDESTRUCT 的存废之争
要理解 SET_INDESTRUCTIBLE,必须先理解它试图约束的 SELFDESTRUCT (0xFF) 操作码。
在以太坊早期,销毁合约的操作码名为 SUICIDE。EIP-6(Renaming SUICIDE opcode,Final 状态)将其在编程语言层更名为 SELFDESTRUCT,作为 SUICIDE 的别名,理由是"suicide"一词对患有抑郁症的开发者群体不够友好——这也侧面说明该操作码在当时的常用程度。
SELFDESTRUCT 的功能是:将合约账户内的全部余额转给指定受益人(beneficiary),并将该合约的代码与存储从状态中彻底删除。这在历史上被广泛用于:
- 合约自我终结后的资金回收;
- 结合 CREATE2 在同一地址重新部署新代码(升级模式);
- 通过以自身为受益人销毁合约来"燃烧" ETH。
然而,随着以太坊状态模型演进,SELFDESTRUCT 逐渐被视为一个负担:
针对"如何处置 SELFDESTRUCT",社区提出了多种路线:彻底禁用、条件禁用(EIP-6780 的"仅限创建同交易内生效"方案)、以及本文主角——由合约自愿声明不可销毁的 EIP-2937 方案。
提案概览(Simple Summary 与 Abstract)
EIP-2937 的表述非常简洁:
Add a SET_INDESTRUCTIBLE (0xA8) opcode that prevents the contract from calling SELFDESTRUCT (0xFF).
即新增一个操作码 SET_INDESTRUCTIBLE(字节码 0xA8),凡是执行过该操作码的合约,之后不允许再调用 SELFDESTRUCT。原文档的 Abstract 部分为空,其全部技术内容集中在 Specification 与 Rationale 中。
提案的目标使用场景是:希望作为"库合约"(library)提供给用户、并向用户保证"代码将永久原样存在、永不被修改或删除"的合约,可以把 SET_INDESTRUCTIBLE 作为合约代码的第一个字节。这样,任何依赖该库的调用方都可以通过检查首字节来确认代码的永久性承诺。
EIP-2937 与"直接禁用 SELFDESTRUCT"类提案(如后来的 EIP-6780)的关键区别在于:它不改变任何既有合约的行为。已经部署的合约从未执行过 SET_INDESTRUCTIBLE,其调用 SELFDESTRUCT 的能力完全不受影响,因此向后兼容性压力远小于全局禁用方案。
技术规范(Specification)
EIP-2937 的规范由两部分组成:一个交易级全局变量 + 一个新操作码。两者共同构成"不可销毁"语义。
交易级全局变量:globals.indestructible
提案引入一个交易级(transaction-wide)全局变量:
globals.indestructible: Set[Address]
- 初始化:每条交易开始时初始化为空集 ∅;
- 生命周期:与既有 selfdestructs 集合(交易内已执行 SELFDESTRUCT 的地址集合)相同的运作方式,即跨整个交易内的所有执行上下文(含内部 CALL)可见;
- 类型:Set[Address],保存的是地址(address),而非合约对象或代码哈希。
这一设计直接借鉴了 EVM 中已有的 selfdestructs 集合模式——在黄皮书(Yellow Paper)及各类客户端实现(如 go-ethereum 的 state 模块)中,交易执行期间会维护一个已自毁地址集合,用于处理同交易内的二次调用等边界情况。globals.indestructible 只是把这一成熟模式复用到了"不可销毁"语义上。
新操作码:SET_INDESTRUCTIBLE (0xA8)
提案规范原文:
Add a SET_INDESTRUCTIBLE opcode at 0xA8, with gas cost G_base, that adds the current callee to the globals.indestructible set. If in the current execution context the callee is in globals.indestructible, the SELFDESTRUCT opcode throws an exception.
逐条拆解:
| 项目 | 值 | 说明 | | – | – | – | | 操作码字节 | 0xA8 | 位于 0xA0 系列未占用区域,与既有操作码不冲突 | | Gas 成本 | G_base | 与 ADDRESS、ORIGIN 等基础操作码相同的固定基础 gas 成本,非常廉价 | | 栈行为 | 无入栈/出栈操作 | 它是一个标记型操作码,不消费也不产生栈元素 | | 效果 | 将当前 callee 加入 globals.indestructible 集合 | callee 即当前正在执行代码的合约地址(被调用方) | | 约束效果 | 集合内的地址调用 SELFDESTRUCT (0xFF) 时抛异常 | 执行帧 revert/异常终止,而非静默忽略 |
语义要点:
推荐的部署姿势:首字节标记
提案明确指出预期用法:合约代码的第一个字节(byte 0)为 0xA8(即 SET_INDESTRUCTIBLE)。这样,合约部署后第一件事就是执行该操作码,将自己加入不可销毁集合。
这一约定带来的可验证性价值在于:任何外部观察者只需读取合约代码的第一个字节(例如通过 EXTCODECOPY 复制 1 字节代码),即可验证该合约是否做出了"永久存在"的承诺,而无需理解合约其余逻辑。这也是 EIP-2938 账户抽象方案将其作为"验证目标合约"的充分条件的原因(详见下文)。
与 EIP-2938 账户抽象(AA)的耦合
EIP-2937 并非孤立提案,它是 EIP-2938(Account Abstraction,账户抽象) 的配套基础设施,两者同一天(2020-09-04)创建、同一作者(Vitalik Buterin)参与。
EIP-2938 引入了一种新的 EIP-2718 交易类型(AA_TX_TYPE = 2),允许合约作为顶层账户发起交易。其规范中存在"Replay Protection(重放保护)"一节,明确给出了两个必须二选一的方案(EIPS/eip-2938.md#L109-L123):
方案一:Require SET_INDESTRUCTIBLE(采用 EIP-2937)
Require that contracts targeted by AA transactions begin with [EIP-2937]'s SET_INDESTRUCTIBLE opcode. AA transactions targeting contracts that do not begin with SET_INDESTRUCTIBLE are invalid, and cannot be included in blocks.
即:AA 交易的目标合约必须以 SET_INDESTRUCTIBLE 操作码开头,否则该 AA 交易无效、不能被包含进区块。AA_PREFIX 需相应修改以包含该操作码。理由正如 EIP-2938 第 212 行所述:必须实现上述两种方案之一,以保证 nonce 无法被重用——因为账户抽象交易没有传统 ECDSA 签名,一旦目标合约可以通过 SELFDESTRUCT 销毁后重建,nonce 就会归零,攻击者可能重放交易。而要求合约"不可销毁"(或其 nonce 在 SELFDESTRUCT 后保留)是堵住这一漏洞的前提。
方案二:Preserve Nonce on SELFDESTRUCT(保留 nonce)
另一种思路是在 SELFDESTRUCT 执行后保留合约 nonce,而不是将其归零,从而防止销毁-重建-重放。该方案与 EIP-2681(Limit account nonce to 2^64-1) 讨论的 nonce 语义一脉相承。
此外,EIP-2938 还提到一个更深层的交互(EIPS/eip-2938.md#L166):如果"不可销毁合约 EIP"被采纳,单租户 AA 可以放宽交易验证阶段的限制——允许对首字节为 SET_INDESTRUCTIBLE 的合约执行 EXTCODESIZE、EXTCODEHASH、EXTCODECOPY、CALLCODE、DELEGATECALL 等代码读取类操作,因为"这些被读取的数据不可能被更改",是一个安全的例外。这说明"首字节标记"不仅是承诺,还为 AA 的执行环境约束(AA 验证阶段禁止读取可能变化的外部状态)提供了可证明安全的豁免依据。
设计论证(Rationale):为何不用别的方案
提案 Rationale 部分明确对比了两种替代方案:
替代方案一:彻底禁用 SELFDESTRUCT
Simply banning SELFDESTRUCT outright. This would be ideal, but has larger backwards compatibility issues.
直接全局禁用 SELFDESTRUCT 在语义上最干净,但向后兼容问题巨大——大量既有合约(尤其是依赖 CREATE2 重部署升级、以及以自身为受益人销毁来燃烧 ETH 的合约)会直接失效。EIP-2937 的选择是只约束自愿声明者:不销毁既有合约的任何能力,把选择权交给合约作者。
替代方案二:使用局部变量
Using a local variable instead of a global variable. This is problematic because it would be broken by DELEGATECALL.
如果用一个局部变量(例如仅在当前执行帧内有效的标记)来代替交易级全局集合,会存在致命缺陷:DELEGATECALL 会破坏该语义。原因在于 DELEGATECALL 的特殊性——它借用调用方的存储与地址上下文执行被调用方的代码。若采用局部变量方案:
- 合约 A 通过 DELEGATECALL 执行合约 B 的代码,B 的代码中执行了 SET_INDESTRUCTIBLE 的等效逻辑;
- 此时"当前上下文"到底是 A 还是 B?DELEGATECALL 下 ADDRESS 返回的是 A(调用方),而代码来自 B;
- 一旦标记与"实际存储代码的合约"(B)脱钩,就可能出现"B 的代码被标记不可销毁、但 B 仍可通过某种上下文调用 SELFDESTRUCT"或相反的错误绑定。
而交易级全局变量 + 以 callee 为标记对象的设计天然规避了这个问题:无论通过 CALL、CALLCODE 还是 DELEGATECALL 进入,callee(当前执行代码的合约地址)始终是确定的——DELEGATECALL 下 callee 是被委托的合约 B,标记也精准落在 B 上。这正是规范中强调"operates the same way as the selfdestructs set"的原因:selfdestructs 集合在黄皮书中同样是交易级全局的,处理的就是跨上下文的一致性。
向后兼容性与安全考量
向后兼容(Backwards Compatibility)
原文档此节内容为 TBD(待定),未给出明确结论。从提案设计本身可以推断(下文为基于规范结构的技术推断,非原文结论):
- 对既有合约零影响:SET_INDESTRUCTIBLE 是新操作码,0xA8 此前未被分配,既有合约字节码中不可能出现该字节(除非巧合的 PUSH 数据),因此不会改变任何已部署合约的行为;
- 仅影响自愿者:只有主动以 0xA8 开头部署的合约才进入"不可销毁"状态,符合"opt-in"原则。
安全考量(Security Considerations)
原文指出两类安全影响:
1. 与状态租金(state rent)方案的兼容性冲突
This breaks forward compatibility with some forms of state rent, which would simply delete contracts that get too old without paying some maintenance fee.
"不可销毁"语义与部分状态租金方案存在前瞻性兼容冲突——这类方案主张:合约若长期未缴纳维护费,协议可直接删除(清算)其账户。不可销毁合约显然与该逻辑矛盾。
但原文同时强调,并非所有状态控制方案都受影响:例如 Ledgerwatch 提出的 ReGenesis 方案(定期将状态"重置"为仅含近期活跃账户的集合)就不受影响——因为 ReGenesis 删除的是"非活跃"账户,而非按合约意愿保留的账户。换言之,SET_INDESTRUCTIBLE 与"按活跃度/租金清算"类方案冲突,但与"定期快照重置"类方案兼容。
2. 未来 SELFDESTRUCT 被移除时的退化行为
If SELFDESTRUCT is ever removed in the future, this EIP would simply become a no-op.
如果未来某天 SELFDESTRUCT 被彻底移除,本 EIP 将自然退化为空操作(no-op):既然没有可禁止的自毁操作,SET_INDESTRUCTIBLE 就只剩下标记语义而无实际约束效果,不会产生任何额外问题。这体现了该设计的前瞻兼容性——它不排斥更激进的后续改革,只是提供了一个"中间态"。
历史演进:从 EIP-2937 到 EIP-6780
将 EIP-2937 放入以太坊 SELFDESTRUCT 治理的时间线中,可以更清晰地理解它的历史定位:
| 时间 | EIP | 状态 | 核心内容 | | – | – | – | – | | 2015-11 | EIP-6 | Final | 将 SUICIDE 更名为 SELFDESTRUCT(接口层别名) | | 2020-09 | EIP-2937 | Stagnant | 新增 SET_INDESTRUCTIBLE (0xA8),合约自愿声明不可销毁 | | 2022-11 | EIP-6049 | Final | 正式弃用(deprecate) SELFDESTRUCT,警告其行为未来可能破坏性变更 | | 2023-03 | EIP-6780 | Final | 将 SELFDESTRUCT 限制为"仅创建同交易内生效",不再删除账户 |
EIP-6049 指出,社区对"SELFDESTRUCT 一定会变"有强烈共识("there is a strong consensus that something will change"),只是具体方案未定。EIP-2937 就是早期探索中的一条路线。
最终被采纳的路线是 EIP-6780(Cancun 升级,已上线):SELFDESTRUCT 仅保留"转账余额"功能,且只有在创建合约的同一交易内才保留完整的账户删除行为;其余情况下不再删除代码与存储。相比 EIP-2937 的"opt-in 自愿标记",EIP-6780 选择了"协议级全局约束",但其动机(Verkle 树不兼容、简化客户端实现)与 EIP-2937 的动机(保护库合约永久存在、服务账户抽象)同源。
值得注意的呼应点:EIP-2937 的动机部分提到"unlike EIPs that disable the SELFDESTRUCT opcode entirely, this EIP does not modify behavior of any existing contracts"——而 EIP-6780 恰好就是因为全局禁用兼容性代价太大,才选择了"保留余额转账、仅限制删除"的折中方案。两条路线在"如何最小化破坏既有合约"的思考上是相通的;区别在于 EIP-2937 把选择权交给合约作者,EIP-6780 由协议统一收窄语义。
总结与现状
EIP-2937 提供了一个优雅而克制的设计:
- 规范极简:1 个交易级全局集合 globals.indestructible + 1 个 G_base 成本的操作码 SET_INDESTRUCTIBLE (0xA8),以 callee 为标记对象,被标记后调用 SELFDESTRUCT 即抛异常;
- opt-in 哲学:只有以 0xA8 为首字节部署的合约才受约束,既有合约行为零改变,向后兼容压力远小于全局禁用方案;
- 为账户抽象而生:它是 EIP-2938 重放保护的两条候选路线之一,且是唯一能同时解决"目标合约代码可验证永久存在"问题的方案;
- 可退化设计:若未来 SELFDESTRUCT 被移除,本 EIP 自动变为 no-op,不构成历史包袱。
该提案最终因 EIP-2938 整体搁置(后者状态为 Withdrawn,理由为"过时需重写")而进入 Stagnant 状态,未被纳入主网。但它所承载的技术思想——"合约自愿声明不可变 + 交易级集合跟踪 + 首字节可验证约定"——在账户抽象、库合约安全性论证、以及 SELFDESTRUCT 治理讨论中仍有重要的参考价值。对于研究 EVM 操作码设计、理解以太坊账户抽象历史、或设计"代码永久性承诺"类机制的开发者而言,EIP-2937 是一份精炼且值得研读的规范样本。如需查阅原文,可参见 EIPS/eip-2937.md;其配套的账户抽象方案见 EIPS/eip-2938.md;SELFDESTRUCT 的最终归宿见 EIPS/eip-6780.md。
【免费下载链接】EIPs The Ethereum Improvement Proposal repository 项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




