这篇文章探讨了以太坊协议的未来,特别是关于消除复杂性和数据膨胀的挑战。通过介绍历史数据的过期管理、状态的过期以及协议特性的清理,文章提出了通过优化存储要求和降低协议复杂性来实现以太坊长期可持续发展的策略。作者详细阐述了背景、面临的问题以及可能的解决方案,并讨论了实施过程中的权衡与挑战。
特别感谢 Justin Drake、Tim Beiko、Matt Garnett、Piper Merriam、Marius van der Wijden 和 Tomasz Stanczak 的反馈和审查
以太坊面临的挑战之一是,默认情况下,任何区块链协议的臃肿和复杂性会随着时间的推移而增长。这种现象发生在两个方面:
为了使以太坊在长期内持续运作,我们需要对这两种趋势产生强有力的反向压力,随着时间的推移减少复杂性和臃肿。同时,我们还需要保留使区块链伟大的关键特性之一:它们的永久性。无论是将 NFT、情书放在交易 calldata 中,还是在链上放一个包含一百万美元的智能合约,你可以在山洞里待十年,再出来时发现它仍在等待你阅读和互动。为了使 dapps 能够放心地完全去中心化并去除升级密钥,它们需要确信其依赖项不会以破坏性的方式升级,特别是 L1 本身。
在这两种需求之间取得平衡,同时在保留连续性的情况下最小化或逆转臃肿、复杂性和衰退,是完全可能的,只要我们肯花心思去做到。活生生的生物都可以做到:虽然大多数生物随着时间的推移而衰老,少数幸运者不会。社会系统甚至可以拥有极端的长期性。在几次情况下,以太坊已经表现出成功:工作量证明已经消失,SELFDESTRUCT
操作码大部分也已消失,信标链节点已经只存储最多六个月的旧数据。以一种更普遍的方式找到以太坊的这条道路,并朝着最终稳定的长期结果迈进,是以太坊长期可扩展性、技术可持续性甚至安全性的最终挑战。
历史过期 是解决什么问题?
截至本文撰写时,一个完全同步的以太坊节点需要大约 1.1TB 的磁盘空间用于执行客户端,再加上几百GB用于共识客户端。绝大多数数据都是历史:关于历史区块、交易和收据的数据,其中绝大部分已有多年历史。这意味着,即使 gas 限制根本没有增加,节点的大小每年也会增加几百GB。
历史存储问题的一个关键简化特征是,由于每个区块通过哈希链接指向前一个区块(以及其他 结构),对当前的一致性达成共识就足以对历史达成共识。只要网络对最新区块有共识,任何历史区块、交易或状态(账户余额、nonce、代码、存储)都可以由任何单一参与者提供,以及一个 Merkle 证明,这个证明可以让其他任何人验证其正确性。虽然共识是 N/2-of-N 信任模型,但历史却是1-of-N 信任模型。
这为我们存储历史提供了许多选择。一个自然的选择是一个网络,在这个网络中,每个节点仅存储一小部分数据。这就是 torrent 网络几十年来的运作方式:虽然整个网络存储和分发数百万个文件,但每个参与者仅存储和分发其中的一些文件。或许,这种方法看起来不太直观,实际并不一定会减少数据的鲁棒性。如果通过降低节点运行成本,我们能让网络的节点数达到 100,000,每个节点存储随机的 10% 历史数据,那么每条数据将被复制 10,000 次——这与一个拥有 10,000 个节点且每个节点都存储所有数据的网络的复制因子完全相同。
如今,以太坊已经开始摆脱所有节点永久存储所有历史数据的模式。共识块(即与权益证明共识相关的部分)仅存储约 6 个月。数据块仅存储约 18 天。EIP-4444 的目标是在历史区块和收据上引入一年的存储期。长期目标是拥有一个协同的存储期(可能是~18天),在此期间每个节点负责存储所有内容,然后形成一个由以太坊节点组成的点对点网络,以分布式的方式存储旧数据。
可恢复编码可以用来提高鲁棒性,同时保持相同的复制因子。事实上,数据块已经采用可恢复编码,以支持数据可用性采样。最简单的解决方案可能是重用这一可恢复编码,并将执行和共识块数据也放入数据块中。
剩下的主要工作涉及构建和集成一个具体的分布式历史存储解决方案——至少是执行历史,最终也是共识和数据块。对这一点最简单的解决方案是 (i) 简单引入现有的 torrent 库,(ii) 引入一个以太坊本地解决方案,称为门户网络。一旦引入其中之一,我们就可以激活 EIP-4444。EIP-4444 本身 不 需要硬分叉,但需要一个新的网络协议版本。因此,同时针对所有客户端启用它是有价值的,因为否则将存在从连接到其他节点而发生故障的风险,这些节点预计要下载完整历史却实际上无法获得。
主要的交易成本涉及我们在尽力提供“古老”历史数据方面的决心。最简单的解决方案是明天停止存储古老历史,依靠现有的存档节点和各种中心化提供者来进行复制。这是容易的,但削弱了以太坊作为永久记录载体的地位。更困难但更安全的途径是首先构建并集成分布式存储历史的 torrent 网络。在这里,关于“我们尽力去做”的难点有两个方面:
对于 (1) 的极度谨慎的方法涉及 保管证明:实际上要求每个权益证明验证者存储百分比的历史数据,并定期进行加密检查,验证他们确实如此。更为温和的方法是设定每个客户端存储历史的自愿标准。
对于 (2),基本的实现涉及直接使用今天已经完成的工作:Portal 已经存储包含整个以太坊历史的 ERA 文件。更彻底的实现将涉及通过同步过程实际将其连接起来,以便如果有人想同步一个存储完整历史的节点或一个存档节点,即使没有其他在线存档节点,他们也可以直接从门户网络同步。
如果我们希望极其容易地运行或启动一个节点,那么减少历史存储要求可以说比无状态性更重要:在节点所需的 1.1TB 中,约有 300GB 是状态,其余的约 800GB 是历史。以太坊节点在智能手表上运行并且只需几分钟就能完成设置的愿景,只有在同时实施无状态性和 EIP-4444 时才能实现。
限制历史存储还使得新的以太坊节点实现更有可能只支持协议的最新版本,从而使其变得更加简单。例如,由于 2016 年 DoS 攻击期间创建的空存储槽现已全部删除,因此现在可以安全地移除许多行代码。随着向权益证明的切换成为陈年往事,客户端可以安全地删除所有与工作量证明相关的代码。
状态过期 是解决什么问题?
即使我们消除了客户端存储历史的必要性,客户端的存储需求将继续以每年约 50GB 的速度增长,因为 状态 会不断增长:账户余额和非ces,合约代码和合约存储。用户能够支付一次性成本,从而对当前和未来的以太坊客户端施加负担。
状态的“过期”比历史要困难得多,因为 EVM 从根本上是围绕一种假设设计的,即一旦创建状态对象,它将始终存在,并且可以被任何交易在任何时候读取。如果我们引入无状态性,可有一个观点认为这种问题并不严重:只有一类专门的区块构建者需要实际存储状态,而其他所有节点(甚至包含清单制作!)都可以无状态运行。然而,也有观点认为我们不想过多依赖无状态性,最终我们可能想要过期状态以保持以太坊的去中心化。
如今,当你创建一个新的状态对象时(这可以通过三种方式之一发生:(i) 向新账户发送 ETH,(ii) 创建带代码的新账户,(iii) 设置先前未触及的存储槽),该状态对象将永久存在于状态中。我们想要的,是状态对象在时间上自动过期。关键挑战是以一种实现以下三个目标的方式做到这一点:
很容易在不满足这些目标的情况下解决这个问题。例如,你可以让每个状态对象还存储一个过期日期的计数器(可以通过销毁 ETH 来延长,销毁可以在读取或写入时自动发生),并且有一个进程循环扫描状态以删除过期状态对象。然而,这会引入额外的计算(甚至存储要求),而且显然不会满足用户友好性要求。开发人员也会在思考包括存储值有时重设为零的边界情况时遇到困难。如果令过期计时器以合约为单位,这将为开发人员的生活提供技术上的便利,但会在经济方面带来困难:开发人员需思考如何将正在进行的存储成本“传递”给其用户。
这些都是以太坊核心开发社区多年来苦苦挣扎的问题,同时包括了诸如“区块链租金”和“再生”的提案。最终,我们结合了这些提案中最好的部分,并汇聚出了两类“已知的最坏解决方案”:
部分状态过期提案的工作原理都遵循相同的原则。我们将状态分块。每个人永久存储“顶层映射”,该映射指示哪些块是空的或非空的。每个块内的数据仅在该数据被最近访问时才存储。有一个“复活”机制,如果某个块不再被存储,任何人都可以通过提供数据的证明将其恢复。
这些提案之间的主要区别是:(i) 我们如何定义“最近”,以及(ii) 我们如何定义“块”?一个具体的提案是 EIP-7736,该提案建立在为 Verkle 树引入的“干-叶”设计 之上(尽管它与任何形式的无状态性兼容,例如二叉树)。在该设计下,互为邻接的 header、代码和存储槽将存储在同一“干”之下。存储在一个单元下的数据最多为 256 * 31 = 7,936
字节。在许多情况下,账户的整个 header 和代码,以及许多关键存储槽,都会存储在同一个单位下。如果给定单位下的数据在 6 个月内未被读取或写入,则不再存储这些数据,而只存储对该数据的 32 字节承诺(“存根”)。未来访问该数据的交易需要使用一个证明确保数据能够复活,而此证明将通过存根进行核对。
另外还有其他方式来实现相似的想法。例如,如果账户级别的粒度不够,我们可以设计一个方案,其中树的每个 1/232 部分将由类似的干-叶机制来管理。
由于激励机制,这可能会更复杂:攻击者可以通过将大量数据放入一个子树,并每年发送一次交易以“更新树”来迫使客户端永久存储大量状态数据。如果你使续成本与树的大小成比例(或续时间与树的大小成反比),那么某人就可能通过将大量数据放入与他们一样的子树中来故意打击另一个用户。人们可以尝试通过根据子树大小动态调整粒度来限制这两种问题:例如,每 216 = 65536 个状态对象可以被视作一个“组”。然而,这些想法更复杂;干结构方法则简单,因为通常存储在同一保障单元下的所有数据与同一应用程序或用户都有关联。
如果我们想要避免任何永久状态增长,甚至是 32 字节的存根呢?这是一个困难的问题,因为复活冲突:如果一个状态对象被移除,后续 EVM 执行在相同的位置放入另一个状态对象,但后来又有一个关心原状态对象的用户回来尝试恢复它,那会怎样?使用部分状态过期,“存根”阻止了新数据的创建;而在全面的状态过期中,我们无法存储任何存根。
基于地址周期的设计是解决此问题的最佳思路。我们不再有一个状态树存储全部状态,而是有一个不断增长的状态树列表,任何被读取或写入的状态都会保存在最新的状态树中。每个周期(如:一年)添加一个新的空状态树。较旧的状态树则被冻结。完整节点仅需存储最近的两个树。如果某个状态对象在两个周期内未被触及并因此被放入过期树中,那么它仍然可以被读取或写入,但交易将需要提供一个 Merkle 证明——一旦得到该证明,复制会再次保存在最新树中。
使这整个过程对用户和开发人员友好的一个关键思路是 地址周期 的概念。地址周期是地址的一部分数字。一个关键规则是:带有地址周期 N 的地址只能在周期 N(即状态树列表达到长度 N 后)及之后进行读取或写入。如果你要保存一个_新_状态对象(例如新合约或新的 ERC20 余额),如果确保将状态对象放入一个地址周期为 N 或 N-1 的合约中,则无需提供证明,即可立即保存。相反,对于较旧地址周期的状态的所有添加或编辑,则需要证明。
该设计保留了以太坊现有的多数特性,在额外计算上非常轻,并使得应用几乎能够按照当前的方式编写(ERC20s 需要重写,以确保地址周期为 N 的地址的余额存储在一个自身地址周期也是 N 的子合约中),同时解决了“用户在洞里待五年”的问题。然而,它有一个大的问题:地址需要扩展到超过 20 字节以适应地址周期。
一种提案是引入一个新的 32 字节的地址格式,其中包括版本号、地址周期号和扩展哈希。
0x01000000000157aE408398dF7E5f4552091A69125d5dFcb7B8C2659029395bdF
其中红色是版本号。这里的四个橙色零表示空白,可以未来放置分片编号。绿色是地址周期编号。蓝色是 26 字节的哈希。
这里的关键挑战是向后兼容性。现有合约是围绕 20 字节地址设计的,通常使用严格的字节打包技术,这些技术明确假设地址恰好是 20 字节长。解决这个问题的一个想法 涉及一个映射表,在老式合约与新式地址交互时,会看到新式地址的 20 字节哈希。然而,要让这一切安全存在显著的复杂性。
另一种方法是走相反的方向:我们立即禁止一些 2128 大小的地址子范围(例如,所有以 0xffffffff
开头的地址),然后使用该范围引入带有地址周期和 14 字节哈希的地址。
0xffffffff000169125d5dFcb7B8C2659029395bdF
这种方法的一个关键牺牲是,为反事实地址引入安全风险:那些持有资产或权限但其代码尚未发布到链上的地址。这一风险涉及某人创建一个地址,该地址声称有一个尚未发布的代码,同时又有另一个有效的代码,两个代码哈希到同一地址。生成这样的冲突现在需要 280 个哈希;而地址空间收缩会将这一数字降低到一个非常可访问的 256 个哈希。
虽然今天关于反事实地址的关键风险区域,并不为单一拥有者持有的钱包,但由于重新向多 L2 世界的过渡,这一情况可能会变得越来越普遍。唯一的解决方案是简单接受这一风险,但识别出所有可能出现问题的常见用例,并想出有效的变通方案。
我看到未来有四条可行的路径:
唯一需要访问部分状态的功能是 包含列表生产,但我们可以以一种去中心化的方式来实现:每个用户负责维护包含其账户的状态树部分。当他们广播交易时,通过状态对象可访问的证明一同广播(这适用于 EOAs 和 ERC-4337 账户)。无状态验证者可以将这些证明汇总为整个包含列表的证明。
一个重要的观点是,无论是否实现依赖地址格式变化的状态过期方案,地址空间扩展和收缩的复杂问题最终都必须得到解决。目前,生成地址冲突大约需要 280 个哈希,这个计算负担对于资源丰富的参与者而言已经是可行的:一个 GPU 每秒能进行大约 227 个哈希,因此运行一年可计算约 252 个哈希,因此全球大约 230 个 GPU 可以在三分之一年的时间内生成一个冲突,FPGA 和 ASIC 可以进一步加速这一过程。未来,此类攻击将越来越向普通人开放。因此,实施完整状态过期的实际成本可能并不像看起来那么高,因为无论如何我们都必须解决这个极具挑战性的地址问题。
实施状态过期可以潜在简化从一种状态树格式到另一种格式的过渡,因为不需要过渡程序:可以简单开始使用新格式生成新树,随后再进行硬分叉以转换较旧的树。因此,尽管状态过期复杂,但在简化路线图其他方面确实具有优势。
功能清理 解决什么问题?
安全性、可访问性和可信中立性的关键前提之一是简洁性。 如果一个协议既美观又简单,它就减少了出现错误的机会。这增加了新开发人员能够参与并使用协议的各部分的机会。它更可能公平,更容易抵御特殊利益的影响。不幸的是,协议和任何社会系统一样,默认情况下会随着时间的推移而变得更加复杂。如果我们不希望以太坊陷入无尽增加复杂性的黑洞,我们需要采取其中一个步骤:(i)停止改变并 使协议固化;(ii)能够真正移除功能并 降低复杂性。使协议减少更少的变化,并随时间推移至少移除一点复杂性的中间路线也是可能的。本节将讨论我们如何减少或消除复杂性。
没有一个大的单一修复可以减少协议复杂性;问题的内在性质就在于有许多小的修复。
一个几乎已经完成的示例,可以作为处理其他示例的蓝图,是去除 SELFDESTRUCT 操作码。SELFDESTRUCT 操作码是唯一能够在单个区块内修改无限多个存储槽的操作码,要求客户端实施显著更多的复杂性来避免 DoS 攻击。该操作码原先的目的是实现自愿的状态清除,从而允许状态规模随时间减少。实际上,却很少有人使用它。该操作码在 Dencun 硬分叉中被削弱,仅允许在同一交易中创建自毁账户。这样解决了 DoS 问题,并使客户端代码得到了显著的简化。未来,最终完全删除该操作码是合理的。
到目前为止,一些已识别的协议简化机会的关键示例包括以下内容。首先,有一些位于 EVM 之外的示例;这些比较不具侵入性,因此更容易达成共识并在短时间内实施。
现在,有一些保留在 EVM 内的话题示例:
进行此类功能简化的主要权衡在于(i)我们简化的程度和速度 vs(ii)向后兼容性。以太坊作为链的价值在于它作为一个平台,可以在上面部署一个应用,并对其在未来数年仍能正常工作的信心。同时,有可能将理想追逐得过于尖锐,用威廉·詹宁斯·布赖恩的话形容,“在向后兼容性的十字架上钉死以太坊”。如果以太坊只有两个应用使用某个功能,并且其中一个多年没有用户,另一个几乎完全不使用,保障的总价值为 57 美元,那么我们就应直接移除该功能,如有需要,可以支付受害者的 57 美元。
更广泛的社会问题是在创建一个标准化的流程,以进行非紧急的向后兼容性破坏更改。一种方法是检查并扩展现有先例,例如 SELFDESTRUCT 过程。该流程大致如下:* 第一步:开始讨论 删除特性 X
在第一步和第四步之间应有一个为期数年的流程,清楚地提供各项目前所处的步骤信息。在这一点上,特性删除流程的激烈和快速程度与更为保守、将更多资源投入其他协议开发领域之间存在权衡,但我们仍距离帕累托前沿较远。
对 EVM 提出的一个主要变化集是 EVM Object Format (EOF)。EOF 引入了大量更改,例如禁止 gas 可观察性、代码可观察性(即不许使用 CODECOPY),仅允许静态跳转。其目标是在保留向后兼容性的同时,以更强的属性,更方便地升级 EVM(因为预 EOF 的 EVM 仍将存在)。
这有一个好处,就是为添加新 EVM 特性创建了自然路径,同时鼓励迁移到更具限制性的 EVM,提供更强的保证。缺点是显著增加了协议复杂性,除非我们能够找到一种方法最终弃用并删除旧的 EVM。其中一个主要问题是:EOF 在 EVM 简化提案中发挥什么作用,尤其是当目标是减少整个 EVM 的复杂性时?
路线图其余部分中的许多“改进”提案也提供了简化旧特性的机会。这里重复一些上述例子:
一种更激进的以太坊简化策略是保持协议现状,但将其大部分内容从协议特性移动到合约代码。
这一策略的最极端版本将是使以太坊 L1 “技术上”仅仅是信标链,并引入一个最小的虚拟机(例如 RISC-V、Cairo,或其他针对证明系统的更简化的虚拟机),允许其他任何人创建他们自己的 Rollup。EVM 随后将转变为第一个这样的 Rollup。这一结果具有讽刺意味,恰恰与 2019-20 年的执行环境提案 完全相同,尽管 SNARKs 使得它的实际实现更具可行性。
一种更温和的方法是保持信标链与当前以太坊执行环境之间的关系不变,但对 EVM 进行原地替换。我们可以选择 RISC-V、Cairo 或其他虚拟机作为新的“官方以太坊虚拟机”,然后强制将所有 EVM 合约转换为新虚拟机代码,以解释原始代码的逻辑(通过编译或解释)。理论上,这甚至可以通过将“目标虚拟机”设为 EOF 的一个版本来实现。
- 原文链接: vitalik.eth.limo/general...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!