`SELFDESTRUCT` 的务实性销毁

  • vbuterin
  • 发布于 2025-02-28 12:26
  • 阅读 15

文章讨论了以太坊中SELFDESTRUCT操作码的弊端,认为它破坏了重要的不变性,如状态对象的数量、合约代码的不变性以及账户余额的自主性。文章提出了两种解决方案:完全移除SELFDESTRUCT,或修改其行为以消除其破坏性影响,同时考虑了现有合约的使用情况和潜在影响。

这篇文章描述了一些原因,说明了为什么 SELFDESTRUCT 操作码对以太坊生态系统弊大于利,因此应该以某种方式使其失效或移除。 为了处理使用 SELFDESTRUCT 的现有合约,我提出了一些方法,以尽量小的中断来消除 SELFDESTRUCT 的有害方面。

历史:SELFDESTRUCT 不是必须的

SELFDESTRUCT (最初称为 SUICIDE) 在以太坊历史的早期就被引入了; 事实上,在 2013 年 12 月的这份 以太坊协议的预发布“规范” 中就已经存在。 当时,对于长期状态大小管理几乎没有进行严谨的思考。 然而,当时(在我的[Vitalik 的]脑海中)普遍认为,为了防止状态无限地被无用的垃圾填满,我们需要让任何可以被创建的对象都能够被销毁。 外部拥有的账户(EOA),按照当时的思路,会在余额降至零时自动销毁,合约可以在代码中加入自毁条款,以便在不再需要时删除自己。 Gas 退款会鼓励他们这样做。

2014 年 1 月,Andrew Miller 指出了一个事后看来显而易见的问题:在 2013 年 12 月的规范中,EOA 容易受到重放攻击。 如果我有 100 个币,我给你发送 10 个,你可以简单地在链上重新发布这个交易 10 次,以清空我的所有余额。 这个问题很快用 nonce 解决了。 然而,nonce 的添加消除了 EOA 被删除的所有希望:nonce 无法重置为零。

2015 年,有人提出了一些方案 来绕过这个问题,并可能允许将所有 ETH 发送出去的账户被安全地删除。 然而,到那时,很明显几乎没有合约开发者真正使用自毁功能:搞清楚何时自毁太难了,而且回报太少。

到 2019-21 年,我们已经清楚地意识到,我们需要某种其他形式的状态管理,无论是租金还是“过期”的长期未触及的树的部分(又名“部分无状态性”)。 但是,如果我们_有_这样的方案,并且它能工作,那么我们就不再需要关心赋予合约自愿删除自己的能力了。

SELFDESTRUCT 是唯一打破重要不变性的操作码

除了不是很有用之外,SELFDESTRUCT 操作码也并非无害。 它打破了一些重要的不变性,如果能够拥有这些不变性会很好,但我们因为这一个操作码而无法拥有。

SELFDESTRUCT 是唯一导致单个区块中改变的状态对象数量不受限制的操作码

所有其他操作码都作用于账户中的单个值,或存储树中的单个键,因此可以更改的固定大小对象数量是有限制的(通常,每个操作码调用一个对象)。 然而,SELFDESTRUCT 会删除整个存储树。

在当前的树结构中,这是可以忍受的。 然而,它需要以一种特殊的方式设计缓存,并增加复杂性来处理 SELFDESTRUCT 删除许多存储槽的可能性,然后在同一地址创建一个合约,并在后续交易中读取相同的存储槽。 此外,它使得未来迁移到不同的状态存储格式变得困难。

SELFDESTRUCT 阻止的两个例子是:

  • 任何类型的“单层”方案(我们实际上只有一个树,或者一个 hashmap,而不是一个树中的每个合约)
  • 一种方案,其中存储槽可以存储在状态树中“靠近”某个_其他_地址的位置(这对于 witness 大小优化可能很有用,例如,对于 ERC20 转账或 Uniswap 交易)

请注意,这不仅仅是理论上的; 关于状态存储的根本性变化的讨论(二叉树、Verkle 树……)现在正在进行中,如果状态存储_可以_近似于单个键/值存储,并且对一个区块中可以更改的键的数量有较低的限制,这将大大扩展我们可以选择的选项。

SELFDESTRUCT 是唯一可以导致合约代码更改的操作码

拥有以下不变性是有价值的:一旦一段代码存在于特定地址,就可以保证该代码将永远存在于该地址。 这使得构建依赖于调用已部署合约的应用程序变得更容易。

如果我们希望抽象账户能够调用库,账户抽象在很大程度上依赖于这种不变性。 但是其他应用程序的安全性也因代码更改的可能性而大大复杂化:Parity 多重签名钱包在 2017 年因 库合约代码被意外删除 而崩溃。

唯一打破代码不变性不变性的操作码(事实上,也是导致 Parity 多重签名钱包崩溃的原因)是… SELFDESTRUCT

SELFDESTRUCT 是唯一可以在未经其他账户同意的情况下更改其余额的操作码

SELFDESTRUCT 有一个内置的“发送”功能,并且此发送不执行合约代码,因此绕过了防接收保护和日志记录。 这有破坏智能合约钱包的风险,破坏其他 潜在有用的技巧,并且通常是合约开发者和审计师需要考虑的另一个边缘情况。

当前 SELFDESTRUCT 的用途

如今,有两种重要的应用程序使用 SELFDESTRUCT

  1. GasToken:在 gasprice 低时通过创建合约来消耗 gas,在 gasprice 高时通过调用 SELFDESTRUCT 并索取退款来恢复 gas(对于接近零大小的合约,约为创建成本的 60%)。
  2. 故意使用 SELFDESTRUCT 来动态更改代码:这可以用作 dapp 或 DAO 以及其他类似用例的“升级”模式。

(1)可以安全地破坏。GasToken 开发者自己警告说,“GasToken 的开发者极有可能提倡更改网络,从而使 GasToken 无法使用、无法赎回、不可替代和/或毫无价值”。 从删除 selfdestruct 退款中唯一会发生的事情是,某些操作的成本将增加 2 倍。

从长远来看,(2)是不需要的; 还有其他广泛可用的模式允许动态代码更改。 最容易实现的是 DELEGATECALL 转发器,合约立即使用从存储槽中获取的代码地址执行 DELEGATECALL-to-self; 更改存储槽将升级代码。 然而,在短期内,存在一些使用(2)的应用程序。

提案 1:完全移除 SELFDESTRUCT

我们选择某个区块 FLAG_BLOCK(例如,一个自然的选择是与合并相同的区块),在该区块 SELFDESTRUCT 将完全停止工作。 如果在此区块之后或之上,EVM 执行遇到操作码 0xff,它将简单地以异常退出,就像 EVM 执行遇到任何无效操作码的字节一样。

为了给用户增加警告,我们可以添加的一个技巧是渐近增加的 gas 价格:如果 block.number + 10**6 >= FLAG_BLOCK,则 SELFDESTRUCT 的 gas 成本增加到 10**10 // (FLAG_BLOCK - block.number)

提案 2:使 SELFDESTRUCT 失效

我们还可以更改操作码的行为以保留功能,但消除树被破坏的事实,并添加合约可以将其自身标记为不可自毁的功能,从而保证不会更改代码。

提议的临时新 SELFDESTRUCT 行为:

  • 当合约调用 SELFDESTRUCT 时,它不会被删除。 相反,它的代码被清零,并且它的 nonce 增加 2**40。 不提供退款。
  • 合约中的 ETH 通过调用(使用 0 gas 或父级中的所有 gas)转移到目的地。
  • 如果目标地址具有空代码,则可以创建合约。
  • 地址为 A 的合约中的 SSTORESLOAD 调用使用账户 A_offset = (A + A.nonce // 2**40) % 2**160 的存储树

请注意,从 EIP-2929 的角度来看,如果该账户尚未在已访问集中,则需要“访问”A_offset 并收取额外的 2600 gas。

另一种方法是调整将存储键转换为树键的哈希函数,使用 sha3(storage_key + contract_nonce // 2**40) 而不是仅仅使用 sha3(storage_key)。 请注意,无论如何,都需要进行一些类似地重做,以促进合约级别的 扩展密钥空间无状态性

合约可以将 0xA8 指定为其代码中的第一个字节; EVM 会将其识别为 no-op,但使用它来打开一个完全禁用 SELFDESTRUCT 功能的标志(注意:这相当于 SET_INDESTRUCTIBLE 提案)。

这两种解决方案也可以结合使用:立即失效加上更长期的完全移除。 或者,操作码可能永远不会被_完全_移除; 相反,它最终可以被重命名为 CLEAR,并且只有一个功能,相当于调用目标发送等于合约当前余额的 ETH 金额。

  • 原文链接: hackmd.io/@vbuterin/self...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
vbuterin
vbuterin
江湖只有他的大名,没有他的介绍。