化险为夷:以太坊柏林硬分叉之前存在一个严重的状态问题

ECN以太坊中国

    来源 | Ethereum Blog
    作者 |?Martin Holst Swende & Peter Szilagyi
    Martin Holst Swende 及 Peter Szilagyi 于 2021 年 5 月 18 日发布
    原标题:《化险为夷:以太坊的状态问题》
    本篇博文的目的在于正式揭露以太坊平台在柏林硬分叉之前的一个严重且显而易见的问题。
    State 状态
    我们先从以太坊和及其“状态”的背景开始梳理。
    以太坊的状态由 patricia-merkle trie 组成,一种前缀树。本文不会深入技术细节,简单来说随着状态增长,树的分支会越来越密集。每个加入的账户都是一片新叶子。在树根之间和叶子之间,存在大量的“中间”节点。
    为了查找某个特定账户,或是说这颗巨树中的一片“叶子”,从树根再通过中间节点,需要按序解决 6-9 个哈希才能最终对我们所寻找的数据进行哈希计算。
    简而言之,每执行一次查找账户的 trie 查询,都要执行 8-9 个解析操作。每次解析操作都是一次数据库查询,而每次数据库查询都可能是任意数量的实际磁盘操作。磁盘操作的数量难以预估,但是由于 trie 密钥是加密哈希 (抗冲突),因此密钥是“随机的”,这对任何数据库来说都是最糟糕的情况。
    随着以太坊的发展,一直以来都有必要提高树访问操作的 gas 费用。2016 年 10 月,在经历了“上海攻击”时间之后,以太坊网络在区块高度 2,463,000?进行了Tangerine Whistle?硬分叉,其中包含 EIP 150,大举提升了某些操作的 gas 成本,并引入了大量更改以防御 DoS 攻击。
    另一次对 gas 费用的提升是在 2019 年 12 月的Istanbul 升级中,激活了EIP 1884。
    EIP-1884 针对 gas 费用引入了以下改动:
    ?SLOAD?从?200?提升至?800?gas
    ?BALANCE?从?400?提升至?700?gas (并添加了一个更便宜的?SELFBALANCE)
    ?EXTCODEHASH?从?400?提升至?700?gas
    出现的问题
    2019 年 3 月,Martin Swende 当时在进行一些 EVM 操作码性能的测定。之后的 EIP-1884 就是基于该调查而成的。在 EIP-1884 被激活的前几个月,Broken Metre 发布了这篇论文 (2019/9)。
    两位以太坊安全研究员,Hubert Ritzdorf 和 Matthias Egli,与论文的作者之一 Daniel Perez 将一个漏洞“武器化”,提交给了以太坊的漏洞赏金 (bug bounty) 项目。这是在 2019 年 10 月 4 日。
    建议大家阅读这份他们提交的完整文档,写得很详尽。
    同日,在一个专门用于讨论跨客户端安全性的频道中,来自 Geth、Parity 和 Aleth 的开发者都得知了这份文档。
    这个漏洞的本质在于触发随机的 trie 查询。以下是一个简单的示例:
    
    在其报告中,研究员们通过 eth_call 对同步了主网的节点执行了这个 payload,以下是他们执行过程中的数据,耗费了一千万 gas:
    ? 消耗一千万 gas 的 EXTCODEHASH (400 gas)
    
  • Parity:?~90s
  • Geth:?~70s

    ? 消耗一千万 gas 的 EXTCODEHASH (700 gas)
    
  • Parity :?~50s
  • Geth :?~38s

    显而易见,EIP-1884 的更改确实在降低该攻击的影响上起到了帮助,但还远远不够。
    当时已经临近在大阪的开发者大会。在开发者大会上,这个问题的信息分享给了主网的客户端开发者。我们也和 Hubert、Mathias 以及 Greg Markou (来自 Chainsafe,当时也在进行一些 ETC 的工作) 见面了。ETC 的开发者也收到了这份报告。
    随着 2019 年临近尾声,我们知道这个问题比之前预期的要严重,恶意交易可能将区块时间提升到分钟范围。更糟的是,开发者社区对 EIP-1884 感到不满,因为 EIP-1884 破坏了一些合约流程,而且用户和矿工都非常希望提高区块的 gas limit。
    此外,仅两个月后的 2019 年 12 月,Parity Ethereum 宣布停止运维,而 OpenEthereum 接管了代码库的维护工作。
    随后搭建了一个新的客户端协调频道,Geth、Nethermind、OpenEthereum 和 Besu 的开发者在此继续进行协作。
    解决方案
    我们意识到要解决这个问题,必须要双管齐下。一种方法是通过以太坊协议以某种方式在协议层解决该问题。最好不要破坏合约,并且避免波及“良好”行为,但仍要设法防止攻击。
    第二种方式是通过软件工程来解决,修改客户端中的数据模型和结构。
    协议层的工作
    关于如何处理这些类型的攻击的第一个版本在这里。2020 年 2 月,正式发布为 EIP 2583。其理念是,每当一次 Trie 查找导致未命中时,施加一次罚款。
    但是,Peter 找到了应对方法,即“shielded relay”攻击,可以有效地限制这种惩罚的上限 (约为 800)。
    对未命中查询进行惩罚的问题在于,首先需要进行查找,以确定是否施加惩罚。但是如果剩余的 gas 不足以支付罚款,已执行了未付费的消耗。即使确实会导致抛出异常,也可以将这些状态读取包装到嵌套调用中,允许外部呼叫者继续重复攻击而无需支付 (全部) 罚款。
    因为这个原因,这个 EIP 被放弃了,我们也在寻找更好的替代方案。
    ? Alexey Akhunov 提出了?Oil?的概念,gas 的第二来源,但和 gas 在本质上不一样,因为它对执行层不可见,并可能导致事务全局回滚。
    ? Martin 在 2020 年 5 月也撰写了一个类似的提案 (Gas And Karma)
    在对这些不同机制进行迭代的同时,Vitalik Buterin 提议直接提高 gas 成本,并且保留访问列表。2020 年 8 月,Martin 和 Vitalik 开始完善 EIP-2929 及配套的 EIP-2930。
    EIP-2929 有效地解决了许多之前的问题。
    ? 与无条件提升 gas 成本的 EIP-1884 相比,EIP-2929 只提升了未访问部分的 gas 成本。这导致净成本提升了不足一个百分点。
    ? 加上 EIP-2930,不会对任何合约流程造成破坏
    ? 并且能够在不造成破坏的前提下进一步调整 gas 成本
    2021 年 4 月 15 日,这两个 EIP 都在 Berlin 升级中被激活了。
    开发工作
    Peter 在 2019 年 10 月提出的解决方案是“动态状态快照” (dynamic state snapshots)。
    快照是用于以平面格式存储以太坊状态的辅助数据结构,在 Geth 节点的实时操作期间,可以完全在线构建。快照的好处在于充当状态访问的加速结构:
    ? 无需通过 O(log N) 次磁盘读取 (x?LevelDB 开销) 来访问一个账户或存储插槽,快照可以提供直接的 O(1)访问时间 (x?LevelDB 开销)。
    ? 快照支持每项条目以 O(1) 复杂度迭代账户和存储,这使远程节点能够以比以前便宜得多的方式检索顺序状态数据。
    ? 快照还启用了更多奇特的用例,例如离线修剪状态 Trie 或迁移到其他数据格式。
    快照的缺点在于原始帐户和存储数据实际上是重复的。对于主网来说这意味着将占用额外的 25GB SSD 空间。
    动态快照的概念从 2019 年中就已经发轫,起初的目的主要是推动快照同步的实现。当时 Geth 团队在忙于许多“大项目”。
    ? 离线状态修剪
    ? 动态快照+快照同步
    ? 通过分片化状态实现 LES (Light Ethereum Subprotocol) 状态分发
    然而,最终决定将快照的优先级排到最前,将其他项目暂时搁置。这为后来的 snap/1 同步算法奠定了基础,并于2020年3月合并。
    随着“动态快照”功能的发布,我们有了一些喘息的空间。如果以太坊网络受到攻击,那将是痛苦的,是的,但是至少有可能通知用户启用快照。完整生成快照将花费大量时间,并且当时尚无法同步快照,但是网络至少可以继续运行。
    总结
    2021 年的三月到四月,snap/1 协议在 geth 客户端中实现了,使得通过新的基于快照的算法进行同步成为可能。虽然仍不是默认的同步模式,但这是很重要的一个步骤,使快照不仅可用作攻击防护措施,并且对于用户来说也是一项重要优化。
    在协议方面,柏林升级于 2021 年 4 月完成。
    以下是在我们的 AWS 监控环境中制定的一些基准:
    ? 柏林升级之前,无快照,25M?gas:?14.3s
    ? 柏林升级之前,有快照,25M?gas:?1.5s
    ? 柏林升级之后,无快照,25M?gas:?~3.1s
    ? 柏林升级之后,有快照,25M?gas:?~0.3s
    大致的数据显示柏林升级能够将攻击的效率降低 5 倍,快照能够将其降低 10 倍,总计将影响降低了 50 倍。
    我们估计目前在主网 (15M gas),对于未使用快照的 geth 节点来说,有可能创建执行时间在 2.5-3 秒的区块。对于非快照节点来说,随着状态增长这个数字会持续恶化。
    如果通过 gas 退还来增加区块内的有效 gas 使用量,则可能会进一步加剧为 (最大) 2 倍。随着 EIP 1559 的实施,区块的 gas limit 的弹性会更大,并且在临时爆发中会再增加 2 倍(ELASTICITY_MULTIPLIER)。
    至于实施这种攻击的可行性,攻击者购买一整个区块的成本约为几个 ETH (100 Gwei 时 15M gas 为1.5 ETH)。
    为什么现在公布?
    这个风险其实一直以来都是一个“公开的秘密”,已经不止一次被意外公开披露,并且在核心开发者会议中多次被提及,但并未涉及细节。
    现在既然已经实施了柏林升级,geth 的节点也在默认情况下使用快照同步,因此我们估计现在的威胁性已经非常低了,现在是时候对幕后工作进行全面公开了。
    重要的是,让社区有机会了解一些变更背后的原因,而这些变更会对用户体验造成负面影响,例如 gas 成本增加和限制 gas 返还。
    本文由 Martin Holst Swende 和 Peter Szilagyi 在 2021-04-23 写就。并于 2021-04-26 与基于以太坊的项目进行分享,2021-05-18 公开发布。
    点击“阅读原文”获取文章内部链接!
    原文链接:https://blog.ethereum.org/2021/05/18/eth_state_problems/