3

务实地取消 SELFDESTRUCT

 3 years ago
source link: https://ethfans.org/posts/pragmatic-destruction-of-SELFDESTRUCT
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

务实地取消 SELFDESTRUCT

曾汨   |   19. Mar, 2021   |   250 次阅读

本文将介绍 SELFDESTRUCT 对以太坊生态弊大于利的一些理由,正是因为这些理由,我们应该以某种方式移除 SELFDESTRUCT 。鉴于有些合约已经使用了 SELFDESTRUCT ,我提出了一些只需要付出最小的代价就能消除 SELFDESTRUCT 危害的方法。

一段历史: SELFDESTRUCT 已经没有必要了

SELFDESTRUCT (最初叫作 SUICIDE )早在以太坊的极早期便已引入。实际上,它在 2013 年 12 月发布的以太坊协议 “规范” 预告中就已经出现了。那时候,几乎没人仔细考虑过状态规模管理的长远问题。但是,有个想法我大概还有些印象,为了防止没用的垃圾状态不受限制地膨胀,我们需要让任何创建出来的对象都可以被销毁。具体的思路是,当外部账户(Externally-owned accounts, EOAs)的余额为零时触发自毁,而合约在没用后可以调用代码里的一行自毁语句触发自毁。还有一个 gas 退款机制用于激励大家销毁没用的状态。

2014 年 1 月,Andrew Miller 指出了一个非常严重的问题:在 2013 年 12 月的规范设计中,EOA 很容易被重放攻击。如果我有 100 个币,我通过一笔交易发给你 10 个币,你可以简单地在链上重放这笔交易十次,从而转走我的全部余额。这个问题很快就修复了,为此我们增加了 nonce 字段。然而,nonce 字段的引入让删除 EOA 的愿望彻底破灭了:nonce 是不能被重置为零的(译者注:因为以太坊的状态树是根据账户地址计算的一种前缀树,(如果仍然允许 EOA 的 nonce 回退为 0)一旦该账户被再次使用,nonce 又要从零开始,就会被重放攻击)。

2015 年,有人提出了一些方案试图绕过这个问题,使余额为零的账户可以被安全地删除(译者注:nonce 重置时与区块高度关联,而非从零开始)。然而,当时很明显,几乎没有合约开发者真正使用自毁功能:因为要弄清楚什么时候自毁太难了,而奖励也太少了。

到 2019-21 年,事情已经变得很明显了,我们需要的是其他形式的状态管理,比如租金机制或者是长期未动的状态 “到期作废(expiring)” (即 “部分无状态(partial statelessness)”)。而如果我们采用这两个方案中的任何一个,只要它是有效的,那么合约是否有能力主动删除自己就一点儿也不重要了。

SELFDESTRUCT 是唯一一个破坏重要恒常性质(invariant)的操作码

SELFDESTRUCT 不仅没什么用,还会产生危害。它破坏了一些重要的恒常性质,这些性质本来是很好的,但是仅仅因为这一个操作码,我们就失去了这些性质。

SELFDESTRUCT 是唯一一个能在单个区块中变更无限个状态对象的操作码

其他所有的操作码都只能操作账户中的单个值或者存储树上的单个 key,所以它们能变更多少固定大小的对象是有限制的(通常,调用一个操作码只能变更一个对象)。但是,SELFDESTRUCT 可以删除整棵存储树。

在目前的状态树结构中,这是可以容忍的。但是,考虑一种特殊的情况:当调用 SELFDESTRUCT 删除许多存储插槽后,下一个事务又在同一个地址上创建一个合约并访问同一些存储槽。为了处理这种情况,需要额外设计复杂的缓存机制。此外,SELFDESTRUCT 还阻碍了我们变更状态存储格式。

以 SELFDESTRUCT 会阻碍的两类状态存储格式为例:

  • 任意的 “单层” 方案(使用单棵树或者单个 hashmap 来存储所有合约账户的数据,以此代替目前的每个合约账户都有一棵存储树的设计)
  • 存储槽可以存储在一些地址 “附近”,而不是存储在合约里的方案(这可能对优化见证大小(witness size)有用,比如在 ERC20 转账或 Uniswap 交易的场景下)

请注意,这不是在空想,从根本上变更状态存储格式(如采用二进制树、Verkle 树等)的讨论已经开始了,如果状态存储的数据结构能够接近单一的的键/值存储结构,并且单个区块中可以变更的状态数量有一个较低的上限,那将大大扩展我们的选择空间。

SELFDESTRUCT 是唯一一个会导致合约代码变动的操作码

如果在一个特定的地址上存储了一段代码,那么这段代码就会永远保留在链上。这样的恒常性质是有用的,因为在构建应用时不需要担心这些代码会出现变动。

账户抽象化(Account abstraction)非常依赖该恒常性质用以支持库调用。因为代码存在变动的可能,还会导致应用的安全性变得复杂很多:2017 年 Parity 的多签钱包就曾因为其引用的库代码合约被偶然删除而彻底瘫痪。

而唯一破坏代码不变性的操作码就是 SELFDESTRUCT (是造成 Parity 多签猝死的罪魁祸首)。

SELFDESTRUCT 是唯一一个可以未经账户同意就能修改账户余额的操作码

SELFDESTRUCT 有一个内置的 “转账” 的功能,其并不走正常的转账流程,因而可以绕过避免合约地址接收 Ether 的守护功能,以及对转账事件的日志记录。这为智能合约钱包埋下了隐患,让一些潜在有用的技巧没法使用,加重了开发者和审计者的心智负担(需要考虑更多的例外条件)。

SELFDESTRUCT 当前的用例

如今 SELFDESTRUCT 有两类重要的应用:

  1. GasToken:当 gas 价格低时通过创建合约用掉 gas,当 gas 价格高时通过调用 SELFDESTRUCT 获得 gas 退款(对于几乎不占用空间的合约来说,可以退回大约 60% 的创建费用)。
  2. 利用 SELFDESTRUCT 实现代码的动态变更:这可用于 dApp 或 DAO 及其他类似用例的 “升级”。

(1)可以被安全地销毁。GasToken 的开发者已经发出了警告 “虽然对以太坊网络的变更会导致 GasToken 无法使用、不可赎回、不能互换以及/或毫无价值,但是 GasToken 的开发者极可能会拥护该变更”。移除 selfdestruct 退款只会导致有些操作的费用变得更贵(2 倍以上)。

从长远来看,(2)是没必要的,还有其他一些被广泛使用的范式可用于支持动态代码变更。最容易实现的是 DELEGATECALL 转发器,合约从一个存储插槽中获取一个代码地址,然后调用对应地址的代码;修改这个存储插槽就能更新代码。不过,从短期来看,有少数应用已经使用了(2)。

提案 1:完全移除 SELFDESTRUCT

从某个区块(用 FLAG_BLOCK 表示,比如取 PoW 链与信标链合并发生的那个区块)开始,完全停用 SELFDESTRUCT 。在这个及之后的区块里,如果 EVM 在执行时遇到 0xff 操作码,只要抛出异常直接退出即可,就像 EVM 执行时遇到不存在的操作码一样。

在完全停用前,为了警示用户避免使用 SELFDESTRUCT ,我们可以渐进式地增加其 gas 费用:如果 block.number + 10**6 >= FLAG_BLOCK ,则 SELFDESTRUCT 的 gas 费用增加到 10**10 // (FLAG_BLOCK - block.number)

提案 2:阉割 SELFDESTRUCT

我们也可以保留这个操作码,但是改变其行为,一方面消除其对状态树的破坏,另一方面增加一个新特性,让合约可以标识为不可自毁(un-self-destructible),从而确保代码不可变。

暂时提议新增的行为包括:

  • 当一个合约调用 SELFDESTRUCT 时,并不会删除合约账户,而是清空代码,并且将 nonce 值增加 2**40 。没有退款。
  • 通过调用(转账)将合约中的 ETH 转移到目标地址(要么由父调用提供所需的全部 gas,要么父调用并不提供任何 gas)。
  • 可以在代码为空的地址(比如被清空过的)上创建合约。
  • 在合约里调用 SSTORESLOAD 操作地址 A 时,实际操作的是 A_offset = (A + A.nonce // 2**40) % 2**160 的存储树。

注意,从 EIP-2929 的角度来看, A_offset 需要 “可达(accessed)”。如果该账户不在可达账户集合中,则需要额外支付 2600 gas 以加入可达集合。

另一种选择是调整将 storage key 转换为 tree key 的哈希函数,用 sha3(storage_key + contract_nonce // 2**40) 代替 sha3(storage_key) 。需要注意的是,无论如何都需要做一些类似的调整,以方便合约级别的无状态 key 空间扩展(expanding-key-space statelessness)。

合约可以在代码中指定 0xA8 作为第一个字节,EVM 会将其识别为无操作,但使用它来开启一个标志,在执行过程中完全禁用 SELFDESTRUCT 的功能(注意:这与 SET_INDESTRUCTIBLE 提案是一样的)。

这两种解决方案也可以结合起来:当前立即阉割,将来完全移除。或者,这个操作码也可以永远不被完全移除,但是最终只保留一个功能,即向目标地址发送合约当前的全部 ETH 余额,我们可以将这个操作码重命名为 CLEAR


原文链接: https://hackmd.io/@HWeNw8hNRimMm2m2GH56Cw/selfdestruct
作者: Vitalik
翻译&校对: 戡乱 & 阿剑


你可能还会喜欢:

值得考虑删除的 EVM 功能

弱无状态性 以及/或者 状态保质期机制:即将到来

GasToken:我为何不再担心 gas 价格飙升

Icon wechat

微信扫一扫
分享至朋友圈


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK