2

OpenEthereum 客户端 “柏林” 升级后出错始末

 3 years ago
source link: https://ethfans.org/posts/openethereum-client-berlin-fork-issue-and-its-reasons
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.

OpenEthereum 客户端 “柏林” 升级后出错始末

曾汨   |   19. Apr, 2021   |   197 次阅读

Alex Stokes@ralexstokes

你可能已经听说了,@OpenEthereum 客户端的一个错误导致了一些支撑以太坊网络的重要服务宕机。

我们来琢磨一下那笔造成事故的交易。

首先,我想感谢所有快速反应到事故并解决了问题的工程师:https://twitter.com/OpenEthereumOrg/status/1382719444833726470?s=20

另外,我没有自己跟踪所有的细节,下文中的重要事实都由用户 eb 在 Eth R&D discord 服务器里提出。

先从那笔触发了错误的交易开始:https://etherscan.io/tx/0x7006f38fa2e6654fae1a781aefc5885fe0cb8f778b1add10636eaf7e34279247

这是一笔合约调用交易,从 KuCoin 交易所发出,向许多地址分发 ETH。该笔交易的 call data 的 ABI 编码错误,最终导致了链分裂。你可以在 Etherscan 上看看这笔交易的 “Input Data”。

1/ 在合约中调用 sendEths 时,需要提供两个参数:一个是关于目标地址的不定长数组(dynamically sized array);一个是关于转账数额(以 wei 为单位)的不定长数组;两者相搭配才知道要转移多少钱给哪个地址。

2/ 我们可以解析 call data 来看看到底哪里出了错:第一行(在 Etherscan 上标记为 “[0]”)表示地址列表从字节 64(行 “[2]”)开始。第二行表示转移数额的列表从字节 416(行 “[13]”)开始。

3/ 因此,大体上,我们是希望成对成对地、从上往下、向某个地址发送一定数量的 ETH —— 看起来很直接嘛。

4/ 然而,当我们开始遍历这个列表,我们先跳转到 call data 的正确字节,而 Solidity ABI 声明了数据的第一个字是整个不定长数组的长度。

5/ 这就是最终 bug 的根源:因为 call dada 中的值是 “0x10”(注意,这可是 16 进制!),但是 call data 只给出了 10 个 地址-数值对。对这个 call data 的正确 ABI 编码(填在行 [2] 和行 [13] 的)应该是 “0xA” —— 不是 “0x10”!

6/ 你可能已经猜到了那时候会发生什么事,我们可以通过执行情况跟踪器(execution trace)来看看:https://etherscan.io/vmtrace?txhash=0x7006f38fa2e6654fae1a781aefc5885fe0cb8f778b1add10636eaf7e34279247&type=parity

7/ 合约成功地遍历了前 10 个地址。本来合约应该在此时停止执行,但根据 call data 的声明,还有很多个地址!那就继续执行吧。

但是,根据 call data 的结构,“第 11 个地址” 是用于编码列表长度的 0x10,所以合约就尝试发送 0 ETH 到地址 0x10

8/ 此外,似乎,当合约尝试读取并不存在的 call data 时,会返回 0 ETH —— 你可以想象成合约在这里跑出了一个错误,但它却继续发送 0 ETH 到它从 call data 中读取的另外 6 个 “地址”。

此时,你可能会注意到,0x10 有可能是我们所谓的 “特殊地址” 之一,它完全在 EVM 预编译合约的范围内(所谓 “预编译合约”,就是一类特殊合约,在 EVM 之外有最优的实现,但是编译起来与大多数合约一样)。

而我们也并不期望预编译合约 0x10 能够返回 ETH 。如此,它就成了一个 ETH 黑洞。但是,这也并不必然造成任何问题。到底是什么导致了整个客户端崩溃?

原因在于,0x10 实际上是一个由 EIP-2537 断言的预编译合约,是为 BLS 配对密码学程序而设的,但这个 EIP 还未部署到主网上。所以虽然你能够跟这个地址互动,但主网上的这个地址里没有任何合约,不会有任何进一步的动作。

此外,我们还需要一个事实来解释这次分裂,你可能也猜到了,就是 “柏林” 硬分叉(也正是这次硬分叉使这个问题浮出水面):它改变了 EVM 中 Gas 消耗量的计量方法。

在 EIP-2929 实施后,如果你在一笔交易中对同一个存储槽多次执行状态存储操作,第一次执行会消耗更多 Gas,后续执行的消耗会更少。这种重定价理论上能更准确地反映当前的客户端访问存储项的成本 ……

而且,要知道,在所有客户端的执行中,这些数据通常都换存在更便宜的硬件层中。

现在我们终于找到了 OpenEthereum 在区块 #12244294 处发生的 Bug:该客户端包含了 所有 已实现的预编译,作为 EIP-2929 访问清单的一部分。(译者注:此处应为 “EIP-2930”)

因为 EIP-2537 在大部分客户端中都已经实现就绪了(而且一度有人提议要把它包含在 “柏林” 升级里面!),OpenEthereum 对所有访问了 0x10 的交易都给了 gas 折扣。

但网络的绝大部分活跃客户端都不是这样实现 EIP-2929 的,它们只会给访问了已激活预编译合约的交易提供 gas 折扣 —— 而 EIP-2537 属于还未激活的预编译合约!所以,OpenEthereum 客户端对该交易消耗了多少 Gas 的计算与网络中其他客户端发生了分歧。

所幸,@mhswende 很快找出了该 bug,而 @sorpaas 出力修复了该 bug:https://github.com/openethereum/openethereum/pull/364

还有很多东西可说,我也预期会有比我更能观察到全貌人来撰写更好的时候报告。

我能说的只是,这个 bug 彰显了硬分叉的内在风险,以及持续致力于建设更有弹性的基础设施的重要性。

依赖于 OpenEthereum 客户端的单客户端系统在今天停机了一段时间,因为客户端无法在问题区块出现后与网络保持同步。Etherscan 自身也因此停机。

庆幸的是,这个 bug 没有严重到导致重大的链分叉,但这样的可能性并不是不存在。我们可以利用多客户端实现来提升抗性 —— 多客户端本身就是我们以太坊生态的一大长处 —— 并推动您的基础设施提供商也这样做。

我们已经看到,2021 年的普及速度已经前所未有地快,而且前景非常光明。我们要从这个事故中吸取教训,一起打造更好的以太坊。


原文链接: https://twitter.com/ralexstokes/status/1382750001026146304
作者: Alex Stokes
翻译: 阿剑


你可能还会喜欢:

“柏林” 升级内容概览

搞懂 “柏林” 之后的合约 Gas 开销

以太坊柏林(Berlin)升级公告

Icon wechat

微信扫一扫
分享至朋友圈


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK