mt logoMyToken
总市值:
0%
恐慌指数:
0%
币种:--
交易所 --
ETH Gas:--
EN
USD
APP
Ap Store QR Code

Scan Download

Vitalik:为什么说SELFDESTRUCT对以太坊的生态弊大于利

收藏
分享

原文链接:

https://hackmd.io/@HWeNw8hNRimMm2m2GH56Cw/selfdestruct

作者: Vitalik

翻译&校对: 戡乱 & 阿剑

原标题:《Vitalik:务实地取消SELFDESTRUCT》

本文将介绍 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)。
  • 可以在代码为空的地址(比如被清空过的)上创建合约。
  • 在合约里调用 SSTORE 和 SLOAD 操作地址 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 。

(完)

免责声明:本文版权归原作者所有,不代表MyToken(www.mytokencap.com)观点和立场;如有关于内容、版权等问题,请与我们联系。