到 2020 年,我们对如何设计智能合约和区块链协议的理解已经远超 2013-15 年。因此,如果我们在 2021 年从头开始搭建以太坊,我们就不会引入很多早期添加的功能了。然而从一条正在运行的、拥有活跃生态的区块链中移除功能,远比在一个新系统中不添加它们要难得多。
有些 「缺陷功能」 是无害的。有些可以安全而缓慢地移除或改进。还有些已经深深地嵌入到了太多的应用中,以至于根本改不动(例如 EVM 的 256 位字长)。另一方面,也有一些功能要么已经被移除,要么已经被改进,要么即将被移除(例如对状态树格式的改进、用 SSZ 编码规则代替 RLP 等)。
但是还有一些中间情况:有些功能过于复杂,对生态的发展造成了中等程度的伤害,我们可以移除它们,但是需要冒一点风险。如果我们移除这些功能,可能会有少量的应用被破坏。但是不移除的话,它们会继续拖累生态。
就跟别的 「长痛短痛」 抉择情形一样,人们很容易低估短痛带来的长期收益。特别是在我们的情况中,由于解决复杂情况的代码已经写好了,所以感觉保留它们不需要付出任何成本。但实际上有两个重要的成本要考虑:
为协议开发新实现的成本
若要改变功能 B,但 B 会跟没必要存在的复杂功能 A 交互,可能会产生 「交互 bug」
以重新设计状态树为例:若以太坊的状态越是遵循一些简单的恒常性质(invariants),那么替换更高效的双层十六进制 Patricia 树就会越容易。然而在现实情况中,因为 SELFDESTRUCT 操作码可以在单笔事务中不受限制地删除大量存储插槽,这给改良状态树带来了很大的困难。另一个例子是 2300 gas 津贴机制(见下文)使 gas 重新定价变得更复杂。
「合并」(放弃 eth1 的 PoW 链,并将其状态导入 eth2 的 PoS 信标链的事件)可能是我们扯掉一些痛苦绷带的最后机会,这篇文章就是解释这样做的理由。
合并是进行最后一轮不兼容更新的一个非常自然的时间节点,有以下几个理由:
合并后构建的客户端很可能不处理 PoW 链,而是专门验证 PoS 信标链。因此,如果在合并时或合并前去除不必要的复杂功能,客户端最容易从中受益,因为它们根本不需要实现这些功能。(从技术上讲,即使是在合并前建立的客户端也可以设计成只处理最近 1-2 个硬分叉之后的数据,但是 「PoS 信标链作为一条独立的链而不需要处理 PoW 链上过于久远的数据」 的说法更容易让人接受)
以太坊已经发生了很大的改变,社区对这将是 「以太坊的一次重大升级」 达成了共识。特别是 「在分片和合并完成之前会出现快速的进化,但合并之后就会趋于稳定」 的观点也得到了社区的一致认可。
必要的向后不兼容的改变(例如,BLOCKHASH 操作码不再是一个好的随机性来源)已经发生了。
这篇文章将介绍一些可以考虑删除的功能的例子。
功能列表
2300 gas 津贴
这是什么?
当一个合约调用另一个合约时,被调用的合约会得到 2300 gas 用于执行非常有限的操作(足够做一点计算和生成一条日志,但不够写满一个存储槽)
为何引入?
最初是为了让智能合约钱包在收钱时能自动生成一条日志。后来还被用于实现 「守卫」 功能以防止合约收到 ETH。
有何问题?
由于它设置的是固定的 gas 数量,因此只要 gas 价格可以调整,人们就没有办法确定这些 gas 到底能支持什么类型的计算。
它并没有很好地满足设计意图,有两个原因。首先,很多用户仍然在使用外部账户,而外部账户并不会生成日志。其次,SELFDESTRUCT 操作码绕过了津贴机制。从长远来看,通过账户抽象化,外部账户的作用将被弱化,并且 SELFDESTRUCT 操作码可能将被移除,但是在这两件事完成之前,它都只是一个不充分的解决方式。
如何移除?
有两种可能 —— 要么将 2300 改成 0 (不支持子执行(child execution))要么不限制数量(子执行可以从父执行中获得全部的 gas 可用额度)
移除有何副作用?
如果我们移除子执行,那么这将需要在合约调用中添加一个笨拙的二分处置(two-clause mechanic),即 0 gas 解释为 0,任何其他数字解释为 「发送所有的 gas」。它还会破坏反接收守卫功能和日志记录。
如果我们在执行中允许子执行获得全部的 gas,那么通过调用发送 ETH 会变成一个需要信任的操作,恶意合约可能会借此扰乱一些应用。不过,Solidity 文档已经建议大家用 withdrawal 模式代替 transfer,这样就不会有任何风险了。
如何消除顾虑?
让所有的 ETH 转账,无论是来自调用还是 SELFDESTRUCT(如果保留的话),都生成一条日志,这样钱包就不需要生成日志了
增加一条规则,对于提供 0 gas 的调用,可看做是一个 「可以生成日志的 STATICCALL」。这样就复制了在 gas 津贴的执行环境里实际做到的功能。
剩余 Gas 额度可见性
这是什么?
GAS 操作码允许合约查看当前的执行环境中还剩多少 gas 可用。CALL 允许调用者为子上下文提供固定数量的 gas。
为何引入?
反对让 CALL 将父环境中剩余的全部 gas 都交给子环境的最主要原因是避免 「不可信任的调用」:即发送者不信任接受者的调用。一个简单的例子是发送 ETH 给参与方的金融机制。另一个例子是 M-of-N 外部价格信息的输入机制(oracle),通过调用一些合约,在获得所有合约回复后取中位数作为输出。
有何问题?
其实绝大多数不可信任调用的用例都可以通过其他方式绕过去。对于转账,Solidity 文档已经建议大家用 withdrawal 模式代替 transfer。M-of-N 外部价格信息的输入机制可以很容易地通过为每一个外部输入单独创建一笔交易实现。
这会让 gas 重定价变得很难做,当操作码的 gas 消耗量发生变化,固定 gas 数量的调用可能会不够用。
如何移除?
让 CALL 可以自动将父环境的所有可用 gas 额度都交给子环境。GAS 操作码只需简单地返回交易的初始 gas 数量。
移除有何副作用?
我们知道的 「不可信任调用的合法用例」 主要是第三方赞助调用(译者注:即元交易)。第三方发布一笔事务,事务中包含你希望的调用,当调用发生后,可以自动地向你扣费(你会公布授权他们这样做的签名)。这对用户没有任何 ETH 的智能合约钱包、混币者的隐私保护以及其他一些用例都很有用。我们需要一个有限 gas 数量的调用以确保最终的支付语句真正被调用,而不会因为 gas 不足而被回退。
如何消除顾虑?
矿工可以直接充当中介,如果交易最终没有付钱给他们,他们就可以直接丢弃事务。参见 Phil Daian 的工作,他创建了一个由第三方机器人构成的生态,矿工可以自动产生 「安全」 的批量交易。
在协议内增加一个明确的 「第三方付款人」 的交易类型。参见 EIP 2711 的例子。
还请注意,如果我们想要走得更远,我们还需要调整 63/64 规则使得如果子调用失败,父调用也彻底失败(所以连 1/64 都不剩)。这可能会破坏更多的用例(「如果子调用失败就仅执行一个简单的操作」),但它将确保当 gas 消耗量发生变化时只会引起一种类型的行为变化(原本成功的交易现在会失败)。
SELFDESTRUCT
请看这篇文章。
Gas 退款
这是什么?
调用 SELFDESTRUCT 销毁一个合约,或者将一个存储槽设置为零,会退回 15000-25000 gas。退款会在事务执行的最后触发,并抵扣发送者需要支付的费用。
为何引入?
激励应用开发者践行 「良好的状态卫生」,清除不再需要的存储插槽和合约。
有何问题?
在实践中,几乎没有人真正践行良好的状态卫生。这是因为激励不够高,不值得为此增加代码的复杂度甚至带来安全风险。
退费机制使得 GasToken 兴起。GasToken 有利于将低费率时期的 gas 调配到高费率时期使用,但是它不利于网络,特别是加重了状态规模的膨胀,并使低效的 gas 使用方法阻塞了区块链。
它加剧了区块大小的波动,使一个区块实际上的理论最大 gas 消耗量几乎是字面意义上区块 Gas 上限的两倍。这并不致命,但仍然不可取,特别是考虑到,在 EIP-1559 实施后,退款机制可以使网络的实际 Gas 使用量长期维持高水平,阻碍 1559 机制的运行。
如何移除?
只要把退款功能从协议中完全删除。
移除有何副作用?
我们可以相当确信,没有任何应用会因此无法使用,因为退款只在执行结束后触发,所以取消退款并不会改变任何执行的可用 gas 数量。
GasToken 将变得毫无用处
在 gas 价格反常时,应用失去了降低费用的能力。好在这个功能目前最主要的用户是 defi 的套利机器人,而套利机器人之间的 gas 价格竞争是一种零和活动,不过还不清楚移除这个它们用于竞争的武器会造成什么全局性的不利影响。
如何消除顾虑?
Gastoken 在他们的网站上已经警告过,未来的协议变更可能会使 GasToken 无效,所以用户不会觉得惊讶
我们可以提前公布变更时间
其他候选功能(推测)
相比上面列举的,我对移除以下功能会带来多少价值缺乏信心,不过还是值得列出一个清单。
RIPEMD160 预编译:这是一个非标准的哈希函数,很少有项目使用(除了与比特币交互的应用)。我们可以用链上部署的合约进行替换,对于真正需要高效验证的项目,可以直接使用 ZK-SNARK。
动态跳转:使用变量作为跳转目标会使代码的分析和操作变得更加困难(例如,无法简单地替换操作码序列,或者预置一些代码)。去掉动态跳转,只允许相对偏移的静态跳转,并且为子程序提供一些专用的指针方案(指针不作为整型暴露)可以解决这个问题。然而,这将是一个底层的改变,可能会破坏许多自定义的合约,所以其收益 / 成本比似乎不如这个列表中的其他项目。
MODEXP 预编译:对于大整数计算来说,这显然是一个错误的 「基本元件」,并且其 gas 消耗的计算方案也相当复杂。更好的选择是:(i) 用预编译的 ADD、MUL 和 MOD 作为替代的基本原语,并用这些预编译的指令编写用于替代 MODEXP 的实现,或者 (ii) 将 EVM384 扩展到更多的长度(256,384,512,768,1024 … 8192)