以太坊面临的挑战之一是,默认情况下,任何区块链协议的膨胀和复杂性都会随着时间的推移而增加。这发生在两个地方:
为了使以太坊能够长期维持下去,我们需要对这两种趋势施加强大的反压力, 随着时间的推移减少复杂性和膨胀。但与此同时,我们需要 保留使区块链变得伟大的关键属性之一:它们的持久性。你可以把一张 NFT、一张交易通话数据中的情书、或者一份包含 100 万美元的智能合约放在链上,进入一个洞穴十年,出来后发现它仍然在那里等待你阅读和交互。对于 DApp 而言,要想完全去中心化并删除升级密钥,它们需要确信其依赖项不会以破坏它们的方式升级——尤其是 Layer 1 本身。
The Purge (净化),2023 年路线图。
如果我们下定决心,在这两种需求之间取得平衡,并在保持连续性的同时最大限度地减少或扭转臃肿、复杂性和衰退,这是绝对可能的。生物体可以做到这一点:虽然大多数生物体会随着时间的流逝而衰老,但少数幸运儿却不会。即使是社会系统也可以拥有极长的寿命。在某些情况下,以太坊已经取得了成功:工作量证明消失了, SELFDESTRUCT(自毁)操作码基本消失了,信标链节点已经存储了最多六个月的旧数据。以更通用的方式为以太坊找出这条道路,并朝着长期稳定的最终结果前进,是以太坊长期可扩展性、技术可持续性甚至安全性的终极挑战。
截至撰写本文时,完全同步的以太坊节点需要 大约 1.1 TB 的磁盘空间 执行客户端,再加上共识客户端的另外几百GB。其中绝大部分是历史记录:有关历史区块、交易和收据的数据,其中大部分是多年前的数据。这意味着即使 Gas 限制根本没有增加,节点的大小每年也会增加数百 GB。
历史存储问题的一个关键简化特证是,由于每个区块都通过哈希链接(和其他 结构)指向前一个区块,因此对当前达成共识就足以对历史达成共识。只要网络对最新区块达成共识,任何历史区块或交易或状态(账户余额、随机数、代码、存储)都可以由任何单个参与者提供以及 Merkle 证明,并且该证明允许其他任何人验证它的正确性。虽然共识是 N/2-of-N 信任模型,但历史是 N 中之 1(1-of-N) 的信任模型。
这为我们如何存储历史记录提供了很多选择。一种自然的选择是每个节点仅存储一小部分数据的网络。这就是种子网络几十年来的运作方式:虽然网络总共存储和分发了数百万个文件,但每个参与者仅存储和分发其中的几个文件。也许与直觉相反,这种方法甚至不一定会降低数据的稳健性。 如果通过让节点运行更加经济实惠,我们可以建立一个拥有 100,000 个节点的网络,其中每个节点存储随机 10% 的历史记录,那么每条数据将被复制 10,000 次 - 与 10,000 个节点的复制因子完全相同-节点网络,每个节点存储所有内容。
如今,以太坊已经开始摆脱所有节点永久存储所有历史的模型。共识区块(即与权益证明共识相关的部分)仅存储约 6 个月。 Blob 仅存储约 18 天。 EIP-4444 旨在为历史区块和收据引入一年的存储期。长期目标是建立一个统一的时期(可能约为 18 天),在此期间每个节点负责存储所有内容,然后构建一个由以太坊节点组成的对等网络,以分布式方式存储旧数据。
纠删码(Erasure Codes)可用于提高鲁棒性,同时保持复制因子相同。事实上,Blob 已经进行了擦除编码,以支持数据可用性采样。最简单的解决方案很可能是重新使用这种纠删码,并将执行和共识块数据也放入 blob 中。
剩下的主要工作包括构建和集成一个具体的分布式解决方案来存储历史记录——至少是执行历史记录,但最终还包括共识和 blob。最简单的解决方案是(i)简单地引入现有的 torrent 库,以及 (ii) 一个名为「Portal Network」的以太坊原生解决方案。一旦引入其中任何一个,我们就可以启用 EIP-4444。EIP-4444 本身不需要硬分叉,但它确实需要新的网络协议版本。因此,同时为所有客户端启用它是有价值的,否则将存在客户端连接到其他节点时出现故障的风险,这些节点期望下载完整的历史记录,但实际上却无法获取。
主要的权衡涉及我们如何努力提供“古老的”历史数据。最简单的解决方案是明天停止存储古代历史,并依赖现有的存档节点和各种集中式提供程序进行复制。这很容易,但这削弱了以太坊作为永久记录场所的地位。更困难但更安全的途径是首先构建并集成 torrent 网络,以分布式方式存储历史记录。在这里,“我们的努力程度”有两个维度:
对于(1),一种最偏执的方法是使用托管证明:实际上要求每个权益证明验证者存储一定比例的历史记录,并定期通过加密方式检查他们是否这样做。更温和的方法是为每个客户端存储的历史记录百分比设定一个自愿标准。
对于 (2),基本实现只涉及今天已经完成的工作:Portal 已经存储了包含整个以太坊历史的 ERA 文件。更彻底的实现将涉及实际将其连接到同步过程,这样,如果有人想要同步完整历史记录存储节点或存档节点,即使没有其他存档节点在线存在,他们也可以通过直接同步来实现来自门户网络。
如果我们想让节点运行或启动变得极其容易,那么减少历史存储需求可以说比无状态性更重要:在节点需要的 1.1 TB 中,约 300 GB 是状态,剩余的约 800 GB GB 已成为历史。只有在同时实现无状态性和 EIP-4444 的情况下,以太坊节点在智能手表上运行且只需几分钟即可设置的愿景才能实现。
限制历史存储还使较新的以太坊节点实现能更容易地仅支持协议的最新版本,从而简化它们的结构。例如,由于 2016 年 DoS 攻击期间创建的空存储槽已全部清除,现在可以安全地删除大量相关代码。同样,随着以太坊转向权益证明成为历史,客户端可以安全地移除所有与工作量证明相关的代码。
即使我们消除了客户端存储历史记录的需求,客户端的存储需求仍将继续增长,每年约 50 GB,因为状态持续增长:账户余额和随机数、合约代码和合约存储。用户可以支付一次性费用,永远给现在和未来的以太坊客户端带来负担。
状态比历史更难“过期”,因为 EVM 从根本上是围绕这样的假设设计的:一旦创建了状态对象,它就会始终存在,并且可以随时被任何事务读取。如果我们引入无状态性,有一种观点认为这个问题也许并没有那么糟糕:只有一类专门的区块构建器需要实际存储状态,而所有其他节点(甚至 包含列表 生产!)可以无状态运行。然而,有一种观点认为,我们不想过分依赖无状态性,最终我们可能希望让状态过期以保持以太坊的去中心化。
今天,当您创建一个新的状态对象时(可以通过以下三种方式之一发生:(i)将 ETH 发送到新帐户,(ii)使用代码创建新帐户,(iii)设置以前未触及的存储槽) ,该状态对象永远处于该状态。相反,我们想要的是对象随着时间的推移自动过期。关键的挑战是以实现三个目标的方式做到这一点:
在不满足这些目标的情况下,解决问题反而变得简单。例如,我们可以让每个状态对象存储一个表示其到期日期的计数器(通过销毁 ETH 可以延长过期日期,这可以在读取或写入时自动进行),并设置一个循环来「遍历状态」并删除过期的状态对象。然而,这种方法会引入额外的计算开销(甚至增加存储需求),而且显然无法满足用户友好性的要求。对于开发人员来说,处理存储值偶尔重置为零的边缘情况也会变得困难。如果在整个合约范围内设置到期计时器,虽然在技术上简化了开发人员的工作,但在经济上却变得更加棘手:开发人员必须考虑如何将持续的存储成本「转嫁」给用户。
这些都是以太坊核心开发社区多年来一直在努力解决的问题,包括 “区块链租金” 和 “再生(Regenesis)” 等提案。最终,我们结合了提案中最好的部分,并集中在两类“已知最不糟糕的解决方案”上:
部分状态到期提案都遵循相同的原则。我们将状态分成块。每个人都永久存储“顶层映射”,其中块为空或非空。数据 之内 仅当最近访问过该数据时才存储每个块。有一种“复活”机制,如果不再存储某个块,任何人都可以通过提供数据内容的证明来恢复该数据。
这些提案的主要区别在于:(i)如何定义”最近”,以及(ii)如何定义”大数据块”。一个具体的提案是EIP-7736,它基于为 Verkle 树引入的“茎叶”设计(尽管与任何形式的无状态树兼容,如二叉树)。在这种设计中,相邻的头部、代码和存储槽存储在同一个”茎”下。”茎”下存储的数据最多为 7936 字节(256 * 31)。通常,一个账户的整个头部、代码以及多个关键存储槽都会存储在同一个”茎”下。如果某个”茎”下的数据在 6 个月内未被读取或写入,该数据将不再存储,而只保留一个 32 字节的承诺(”存根”)。未来访问这些数据的交易需要”复活”数据,并提供基于”存根”验证的证明。
还有其他实现类似想法的方法。例如,如果账户级别的粒度不够精细,我们可以设计一个方案,将”树”的每个”2^-32”部分都用类似的”茎和叶”机制来管理。
由于激励因素,这变得更加棘手:攻击者可以通过将大量数据放入单个子树并每年发送单个交易来“更新树”,从而迫使客户端永久存储大量状态。如果您使更新成本与树大小成正比(或更新持续时间成反比),那么有人可能会通过将大量数据放入与他们相同的子树中来伤害其他用户。人们可以尝试通过根据子树大小动态调整粒度来限制这两个问题:例如,每个连续的 216 = 65536 个状态对象可以被视为一个“组”。然而,这些想法更为复杂;基于“茎”的方法很简单,并且可以调整激励措施,因为通常“茎”下的所有数据都与同一应用程序或用户相关。
如果我们想完全避免任何永久性的状态增长,甚至是 32 字节的「存根」,该怎么办?这是一个棘手的问题,主要是因为@vbuterin/state_size_management#Resurrection-conflicts">复活冲突(Resurrection Conflicts)。设想这样一个场景:一个状态对象被删除后,EVM 执行将另一个状态对象放在相同位置。然而,之后有人想恢复原始状态对象,我们该如何处理?在部分状态到期的情况下,「存根」可以阻止创建新数据。但在完全状态到期时,我们连「存根」都无法存储。
基于地址周期的设计是解决此问题的最佳方法。我们不再使用单一状态树存储整个状态,而是维护一个不断增长的状态树列表。任何读取或写入的状态都会保存在最新的状态树中。每个周期(比如:1 年)都会添加一个新的空状态树。旧状态树保持不变。完整节点只需存储最近的两棵状态树。如果一个状态对象在两个周期内未被访问,从而落入过期状态树,它仍然可以被读取或写入。但这时交易需要提供该对象的 Merkle 证明。一旦证明有效,该对象的副本将再次保存在最新的状态树中。
地址周期的概念是使这一切对用户和开发人员都友好的关键思想。地址周期是地址中的一个数字部分。核心规则是:地址周期为 N 的地址只能在周期 N 或之后(即当状态树列表达到长度 N 时)被读取或写入。当用户需要保存新的状态对象(如新合约或新的 ERC20 余额)时,只要确保将状态对象放入地址周期为 N 或 N-1 的合约中,就可以立即保存,无需提供证明之前不存在任何内容。然而,对旧地址周期中的状态进行任何添加或编辑都需要提供证明。
这种设计保留了以太坊当前的大部分属性,不需要额外的计算,允许几乎像现在一样编写应用程序(ERC20 需要重写,以确保地址周期为 N 的地址余额存储在本身具有地址周期为 N 的子合约中),解决了“用户进山洞五年”的问题。然而,它有一个大问题: 地址需要扩展到 20 个字节以上才能适应地址周期。
一项建议是引入一种新的 32 字节地址格式,其中包括版本号、地址周期号和扩展散列。
0x01000000000157aE408398dF7E5f4552091A69125d5dFcb7B8C2659029395bdF
红色的是版本号。这里橙色的四个零旨在作为空白空间,将来可以容纳分片编号。绿色是地址周期数。蓝色是 26 字节的哈希值。
这里的关键挑战是向后兼容性。现有合约是围绕 20 字节地址设计的,并且通常使用严格的字节打包技术,明确假设地址正好是 20 字节长。 @ipsilon/address-space-extension-exploration">解决这个问题的一个想法是使用一个转换映射,其中与新式地址交互的旧式合约将看到新式地址的 20 字节哈希。然而,要确保这一点的安全性,存在着很大的复杂性。
另一种方法则相反:我们立即禁止某些 2128- 大小的地址子范围(例如,以 0xffffffff 0xffffffff),然后使用该范围引入带有地址周期和 14 字节哈希值的地址。
0xfffffff000169125d5dFcb7B8C2659029395bdF
这种方法做出的主要牺牲是,它为反事实地址引入了安全风险:持有资产或权限,但代码尚未发布到链上的地址。风险涉及有人创建一个地址,该地址声称拥有一段(尚未发布)代码,但也有另一段有效的代码散列到同一地址。计算这样的碰撞目前需要280个哈希值;地址空间收缩会将这个数字减少到易于访问的 256 个哈希值。
关键风险领域是反事实地址,这些地址不由单个所有者持有。虽然这种情况在当前相对罕见,但随着我们进入多 L2 世界,它可能变得更加普遍。解决这个问题的唯一方法是接受这种风险,同时识别所有可能出现问题的常见用例,并为它们制定有效的缓解策略。
我认为未来有四种可行的道路:
包含列表生成是需要访问部分状态的一项功能,但我们可以通过去中心化的方式实现:每个用户负责维护状态树中包含自己账户的部分。用户广播交易时,会同时广播验证步骤中所访问状态对象的证明(适用于 EOA 和 ERC-4337 账户)。无状态验证器随后可以将这些证明组合,生成完整的包含列表证明。
重要的一点是,无论是否实施依赖于地址格式更改的状态到期方案,最终都必须解决有关地址空间扩展和收缩的难题。今天,大约需要 280 哈希值来生成地址冲突,对于资源极其丰富的参与者来说,这种计算负载已经是可行的:GPU 可以进行大约 227 哈希值运算,因此运行一年可以计算 252 次,因此世界上所有约“2 的 30 次方”个 GPU 都可以在 约 1/4 年的时间内计算一次碰撞,而 FPGA 和 ASIC 可以进一步加速这一过程。在未来,此类攻击将会向越来越多的人开放。因此,实现完全状态到期的实际成本可能没有看起来那么高,因为无论如何我们都必须解决这个非常具有挑战性的地址问题。
进行状态过期可能会使从一种状态树格式到另一种状态树格式的转换变得更容易,因为不需要转换过程:您可以简单地开始使用新格式创建新树,然后执行硬分叉来转换旧树。因此,虽然状态到期很复杂,但它确实有利于简化路线图的其他方面。
安全性、可访问性和可用性的关键先决条件之一 可信的中立性 是简单。如果协议美观且简单,就会减少出现错误的可能性。它增加了新开发人员能够参与其中的任何部分的机会。它更有可能是公平的,也更容易抵御特殊利益。不幸的是,协议就像任何社交系统一样,默认情况下会随着时间的推移而变得更加复杂。如果我们不希望以太坊陷入日益复杂的黑洞,我们需要做以下两件事之一:(i)停止进行更改并使协议僵化, (ii) 能够实际删除功能并降低复杂性。一种中间路线,即对协议进行较少的更改,并且随着时间的推移至少消除一点复杂性,这也是可能的做到的。本节将讨论如何减少或消除复杂性。
没有任何重大的单一修复可以降低协议的复杂性;这个问题的本质是有许多小的解决办法。
@vbuterin/selfdestruct">一个基本已经完成的示例是删除 SELFDESTRUCT 操作码。 SELFDESTRUCT 操作码是唯一可以修改单个块内无限数量存储槽的操作码,要求客户端实现显着更高的复杂性以避免 DoS 攻击。该操作码的最初目的是实现自愿状态清算,从而允许状态大小随着时间的推移而减小。实际上,很少有人最终使用它。在 Dencun 硬分叉中,该操作码被削弱为仅允许在同一交易中创建的自毁账户。这解决了 DoS 问题,并允许显著简化客户端代码。 未来,最终完全删除该操作码可能是有意义的。
迄今为止,已确定了一些关键的协议简化机会。首先,让我们看看 EVM 之外的例子。这些变更相对非侵入性,因此更容易达成共识并在短期内实施。
以下是 目前 EVM 中的一些示例:
进行此类功能简化的主要权衡在于(i)我们简化的程度和速度与(ii)向后兼容性之间的平衡。以太坊作为一条链的价值源于它是一个平台,用户可以在其中部署应用程序,并确信这些应用在多年后仍能正常运行。然而,这种理想也可能被过分强调。借用William Jennings Bryan 的话,我们不应”将以太坊钉在向后兼容性的十字架上”。设想一个场景:如果整个以太坊生态系统中只有两个应用程序在使用某个特定功能,其中一个多年来没有用户,几乎完全闲置,而且这两个应用的总价值仅为 57 美元。在这种情况下,我们应该考虑删除该功能,必要时甚至可以自掏腰包向受影响的用户补偿这 57 美元。
更广泛的社会问题在于创建一个标准化的管道来进行非紧急的向后兼容性破坏的更改。解决这个问题的一种方法是检查和扩展现有的先例,例如自毁过程。该流程看起来如下:
在第 1 步和第 4 步之间应该有一个持续数年的流程,并明确标识每个项目所处的步骤。此时,我们需要权衡”功能移除流程的力度和速度”与”采取更保守的方法并将更多资源投入协议开发的其他领域”。尽管如此,我们距离”帕累托前沿(Pareto frontier)”还有相当长的距离。
对 EVM 提出的一系列主要更改是 EVM 对象格式 (EOF)。 EOF引入了大量的改变,例如禁止gas可观察性、代码可观察性(即无CODECOPY)、仅允许静态跳转。目标是允许 EVM 以具有更强属性的方式进行更多升级,同时保持向后兼容性(因为 EOF 之前的 EVM 仍然存在)。
这样做的优点是,它创建了一条添加新 EVM 功能的自然路径,并鼓励迁移到具有更强保证的更严格的 EVM。它的缺点是它显着 增加 协议复杂性,除非我们能找到一种方法最终弃用并删除旧的 EVM。一个主要问题是: EOF 在 EVM 简化提案中发挥什么作用,特别是如果目标是降低整个 EVM 的复杂性?
路线图其余部分中的许多“改进”建议也是对旧功能进行简化的机会。重复上面的一些例子:
一个更激进的以太坊简化策略是保持协议不变,但将其大部分从协议功能转移到合约代码。
最激进的方案是将以太坊 L1 “技术上”简化为信标链,并引入一个精简的虚拟机(VM)。这可以是RISC-V、Cairo,或专为证明系统设计的更简单 VM。这种设计将允许开发者创建自己的 Rollup,而 EVM 将成为这些 Rollups 中的首个实例。有趣的是,这一方案与2019-20 年提出的执行环境方案殊途同归,尽管 SNARK 技术的进步使得这一设想更易实现。
一种更温和的方法是,保持信标链和当前以太坊执行环境之间的关系不变,但对 EVM 进行就地交换。我们可以选择 RISC-V、Cairo 或其他 VM 作为新的“官方以太坊 VM”,然后将所有 EVM 合约强制转换为解释原始代码逻辑的新 VM 代码(通过编译或解释)。理论上来说,甚至可以将这个”目标 VM”作为 EOF 的一个版本来实现。
以太坊面临的挑战之一是,默认情况下,任何区块链协议的膨胀和复杂性都会随着时间的推移而增加。这发生在两个地方:
为了使以太坊能够长期维持下去,我们需要对这两种趋势施加强大的反压力, 随着时间的推移减少复杂性和膨胀。但与此同时,我们需要 保留使区块链变得伟大的关键属性之一:它们的持久性。你可以把一张 NFT、一张交易通话数据中的情书、或者一份包含 100 万美元的智能合约放在链上,进入一个洞穴十年,出来后发现它仍然在那里等待你阅读和交互。对于 DApp 而言,要想完全去中心化并删除升级密钥,它们需要确信其依赖项不会以破坏它们的方式升级——尤其是 Layer 1 本身。
The Purge (净化),2023 年路线图。
如果我们下定决心,在这两种需求之间取得平衡,并在保持连续性的同时最大限度地减少或扭转臃肿、复杂性和衰退,这是绝对可能的。生物体可以做到这一点:虽然大多数生物体会随着时间的流逝而衰老,但少数幸运儿却不会。即使是社会系统也可以拥有极长的寿命。在某些情况下,以太坊已经取得了成功:工作量证明消失了, SELFDESTRUCT(自毁)操作码基本消失了,信标链节点已经存储了最多六个月的旧数据。以更通用的方式为以太坊找出这条道路,并朝着长期稳定的最终结果前进,是以太坊长期可扩展性、技术可持续性甚至安全性的终极挑战。
截至撰写本文时,完全同步的以太坊节点需要 大约 1.1 TB 的磁盘空间 执行客户端,再加上共识客户端的另外几百GB。其中绝大部分是历史记录:有关历史区块、交易和收据的数据,其中大部分是多年前的数据。这意味着即使 Gas 限制根本没有增加,节点的大小每年也会增加数百 GB。
历史存储问题的一个关键简化特证是,由于每个区块都通过哈希链接(和其他 结构)指向前一个区块,因此对当前达成共识就足以对历史达成共识。只要网络对最新区块达成共识,任何历史区块或交易或状态(账户余额、随机数、代码、存储)都可以由任何单个参与者提供以及 Merkle 证明,并且该证明允许其他任何人验证它的正确性。虽然共识是 N/2-of-N 信任模型,但历史是 N 中之 1(1-of-N) 的信任模型。
这为我们如何存储历史记录提供了很多选择。一种自然的选择是每个节点仅存储一小部分数据的网络。这就是种子网络几十年来的运作方式:虽然网络总共存储和分发了数百万个文件,但每个参与者仅存储和分发其中的几个文件。也许与直觉相反,这种方法甚至不一定会降低数据的稳健性。 如果通过让节点运行更加经济实惠,我们可以建立一个拥有 100,000 个节点的网络,其中每个节点存储随机 10% 的历史记录,那么每条数据将被复制 10,000 次 - 与 10,000 个节点的复制因子完全相同-节点网络,每个节点存储所有内容。
如今,以太坊已经开始摆脱所有节点永久存储所有历史的模型。共识区块(即与权益证明共识相关的部分)仅存储约 6 个月。 Blob 仅存储约 18 天。 EIP-4444 旨在为历史区块和收据引入一年的存储期。长期目标是建立一个统一的时期(可能约为 18 天),在此期间每个节点负责存储所有内容,然后构建一个由以太坊节点组成的对等网络,以分布式方式存储旧数据。
纠删码(Erasure Codes)可用于提高鲁棒性,同时保持复制因子相同。事实上,Blob 已经进行了擦除编码,以支持数据可用性采样。最简单的解决方案很可能是重新使用这种纠删码,并将执行和共识块数据也放入 blob 中。
剩下的主要工作包括构建和集成一个具体的分布式解决方案来存储历史记录——至少是执行历史记录,但最终还包括共识和 blob。最简单的解决方案是(i)简单地引入现有的 torrent 库,以及 (ii) 一个名为「Portal Network」的以太坊原生解决方案。一旦引入其中任何一个,我们就可以启用 EIP-4444。EIP-4444 本身不需要硬分叉,但它确实需要新的网络协议版本。因此,同时为所有客户端启用它是有价值的,否则将存在客户端连接到其他节点时出现故障的风险,这些节点期望下载完整的历史记录,但实际上却无法获取。
主要的权衡涉及我们如何努力提供“古老的”历史数据。最简单的解决方案是明天停止存储古代历史,并依赖现有的存档节点和各种集中式提供程序进行复制。这很容易,但这削弱了以太坊作为永久记录场所的地位。更困难但更安全的途径是首先构建并集成 torrent 网络,以分布式方式存储历史记录。在这里,“我们的努力程度”有两个维度:
对于(1),一种最偏执的方法是使用托管证明:实际上要求每个权益证明验证者存储一定比例的历史记录,并定期通过加密方式检查他们是否这样做。更温和的方法是为每个客户端存储的历史记录百分比设定一个自愿标准。
对于 (2),基本实现只涉及今天已经完成的工作:Portal 已经存储了包含整个以太坊历史的 ERA 文件。更彻底的实现将涉及实际将其连接到同步过程,这样,如果有人想要同步完整历史记录存储节点或存档节点,即使没有其他存档节点在线存在,他们也可以通过直接同步来实现来自门户网络。
如果我们想让节点运行或启动变得极其容易,那么减少历史存储需求可以说比无状态性更重要:在节点需要的 1.1 TB 中,约 300 GB 是状态,剩余的约 800 GB GB 已成为历史。只有在同时实现无状态性和 EIP-4444 的情况下,以太坊节点在智能手表上运行且只需几分钟即可设置的愿景才能实现。
限制历史存储还使较新的以太坊节点实现能更容易地仅支持协议的最新版本,从而简化它们的结构。例如,由于 2016 年 DoS 攻击期间创建的空存储槽已全部清除,现在可以安全地删除大量相关代码。同样,随着以太坊转向权益证明成为历史,客户端可以安全地移除所有与工作量证明相关的代码。
即使我们消除了客户端存储历史记录的需求,客户端的存储需求仍将继续增长,每年约 50 GB,因为状态持续增长:账户余额和随机数、合约代码和合约存储。用户可以支付一次性费用,永远给现在和未来的以太坊客户端带来负担。
状态比历史更难“过期”,因为 EVM 从根本上是围绕这样的假设设计的:一旦创建了状态对象,它就会始终存在,并且可以随时被任何事务读取。如果我们引入无状态性,有一种观点认为这个问题也许并没有那么糟糕:只有一类专门的区块构建器需要实际存储状态,而所有其他节点(甚至 包含列表 生产!)可以无状态运行。然而,有一种观点认为,我们不想过分依赖无状态性,最终我们可能希望让状态过期以保持以太坊的去中心化。
今天,当您创建一个新的状态对象时(可以通过以下三种方式之一发生:(i)将 ETH 发送到新帐户,(ii)使用代码创建新帐户,(iii)设置以前未触及的存储槽) ,该状态对象永远处于该状态。相反,我们想要的是对象随着时间的推移自动过期。关键的挑战是以实现三个目标的方式做到这一点:
在不满足这些目标的情况下,解决问题反而变得简单。例如,我们可以让每个状态对象存储一个表示其到期日期的计数器(通过销毁 ETH 可以延长过期日期,这可以在读取或写入时自动进行),并设置一个循环来「遍历状态」并删除过期的状态对象。然而,这种方法会引入额外的计算开销(甚至增加存储需求),而且显然无法满足用户友好性的要求。对于开发人员来说,处理存储值偶尔重置为零的边缘情况也会变得困难。如果在整个合约范围内设置到期计时器,虽然在技术上简化了开发人员的工作,但在经济上却变得更加棘手:开发人员必须考虑如何将持续的存储成本「转嫁」给用户。
这些都是以太坊核心开发社区多年来一直在努力解决的问题,包括 “区块链租金” 和 “再生(Regenesis)” 等提案。最终,我们结合了提案中最好的部分,并集中在两类“已知最不糟糕的解决方案”上:
部分状态到期提案都遵循相同的原则。我们将状态分成块。每个人都永久存储“顶层映射”,其中块为空或非空。数据 之内 仅当最近访问过该数据时才存储每个块。有一种“复活”机制,如果不再存储某个块,任何人都可以通过提供数据内容的证明来恢复该数据。
这些提案的主要区别在于:(i)如何定义”最近”,以及(ii)如何定义”大数据块”。一个具体的提案是EIP-7736,它基于为 Verkle 树引入的“茎叶”设计(尽管与任何形式的无状态树兼容,如二叉树)。在这种设计中,相邻的头部、代码和存储槽存储在同一个”茎”下。”茎”下存储的数据最多为 7936 字节(256 * 31)。通常,一个账户的整个头部、代码以及多个关键存储槽都会存储在同一个”茎”下。如果某个”茎”下的数据在 6 个月内未被读取或写入,该数据将不再存储,而只保留一个 32 字节的承诺(”存根”)。未来访问这些数据的交易需要”复活”数据,并提供基于”存根”验证的证明。
还有其他实现类似想法的方法。例如,如果账户级别的粒度不够精细,我们可以设计一个方案,将”树”的每个”2^-32”部分都用类似的”茎和叶”机制来管理。
由于激励因素,这变得更加棘手:攻击者可以通过将大量数据放入单个子树并每年发送单个交易来“更新树”,从而迫使客户端永久存储大量状态。如果您使更新成本与树大小成正比(或更新持续时间成反比),那么有人可能会通过将大量数据放入与他们相同的子树中来伤害其他用户。人们可以尝试通过根据子树大小动态调整粒度来限制这两个问题:例如,每个连续的 216 = 65536 个状态对象可以被视为一个“组”。然而,这些想法更为复杂;基于“茎”的方法很简单,并且可以调整激励措施,因为通常“茎”下的所有数据都与同一应用程序或用户相关。
如果我们想完全避免任何永久性的状态增长,甚至是 32 字节的「存根」,该怎么办?这是一个棘手的问题,主要是因为@vbuterin/state_size_management#Resurrection-conflicts">复活冲突(Resurrection Conflicts)。设想这样一个场景:一个状态对象被删除后,EVM 执行将另一个状态对象放在相同位置。然而,之后有人想恢复原始状态对象,我们该如何处理?在部分状态到期的情况下,「存根」可以阻止创建新数据。但在完全状态到期时,我们连「存根」都无法存储。
基于地址周期的设计是解决此问题的最佳方法。我们不再使用单一状态树存储整个状态,而是维护一个不断增长的状态树列表。任何读取或写入的状态都会保存在最新的状态树中。每个周期(比如:1 年)都会添加一个新的空状态树。旧状态树保持不变。完整节点只需存储最近的两棵状态树。如果一个状态对象在两个周期内未被访问,从而落入过期状态树,它仍然可以被读取或写入。但这时交易需要提供该对象的 Merkle 证明。一旦证明有效,该对象的副本将再次保存在最新的状态树中。
地址周期的概念是使这一切对用户和开发人员都友好的关键思想。地址周期是地址中的一个数字部分。核心规则是:地址周期为 N 的地址只能在周期 N 或之后(即当状态树列表达到长度 N 时)被读取或写入。当用户需要保存新的状态对象(如新合约或新的 ERC20 余额)时,只要确保将状态对象放入地址周期为 N 或 N-1 的合约中,就可以立即保存,无需提供证明之前不存在任何内容。然而,对旧地址周期中的状态进行任何添加或编辑都需要提供证明。
这种设计保留了以太坊当前的大部分属性,不需要额外的计算,允许几乎像现在一样编写应用程序(ERC20 需要重写,以确保地址周期为 N 的地址余额存储在本身具有地址周期为 N 的子合约中),解决了“用户进山洞五年”的问题。然而,它有一个大问题: 地址需要扩展到 20 个字节以上才能适应地址周期。
一项建议是引入一种新的 32 字节地址格式,其中包括版本号、地址周期号和扩展散列。
0x01000000000157aE408398dF7E5f4552091A69125d5dFcb7B8C2659029395bdF
红色的是版本号。这里橙色的四个零旨在作为空白空间,将来可以容纳分片编号。绿色是地址周期数。蓝色是 26 字节的哈希值。
这里的关键挑战是向后兼容性。现有合约是围绕 20 字节地址设计的,并且通常使用严格的字节打包技术,明确假设地址正好是 20 字节长。 @ipsilon/address-space-extension-exploration">解决这个问题的一个想法是使用一个转换映射,其中与新式地址交互的旧式合约将看到新式地址的 20 字节哈希。然而,要确保这一点的安全性,存在着很大的复杂性。
另一种方法则相反:我们立即禁止某些 2128- 大小的地址子范围(例如,以 0xffffffff 0xffffffff),然后使用该范围引入带有地址周期和 14 字节哈希值的地址。
0xfffffff000169125d5dFcb7B8C2659029395bdF
这种方法做出的主要牺牲是,它为反事实地址引入了安全风险:持有资产或权限,但代码尚未发布到链上的地址。风险涉及有人创建一个地址,该地址声称拥有一段(尚未发布)代码,但也有另一段有效的代码散列到同一地址。计算这样的碰撞目前需要280个哈希值;地址空间收缩会将这个数字减少到易于访问的 256 个哈希值。
关键风险领域是反事实地址,这些地址不由单个所有者持有。虽然这种情况在当前相对罕见,但随着我们进入多 L2 世界,它可能变得更加普遍。解决这个问题的唯一方法是接受这种风险,同时识别所有可能出现问题的常见用例,并为它们制定有效的缓解策略。
我认为未来有四种可行的道路:
包含列表生成是需要访问部分状态的一项功能,但我们可以通过去中心化的方式实现:每个用户负责维护状态树中包含自己账户的部分。用户广播交易时,会同时广播验证步骤中所访问状态对象的证明(适用于 EOA 和 ERC-4337 账户)。无状态验证器随后可以将这些证明组合,生成完整的包含列表证明。
重要的一点是,无论是否实施依赖于地址格式更改的状态到期方案,最终都必须解决有关地址空间扩展和收缩的难题。今天,大约需要 280 哈希值来生成地址冲突,对于资源极其丰富的参与者来说,这种计算负载已经是可行的:GPU 可以进行大约 227 哈希值运算,因此运行一年可以计算 252 次,因此世界上所有约“2 的 30 次方”个 GPU 都可以在 约 1/4 年的时间内计算一次碰撞,而 FPGA 和 ASIC 可以进一步加速这一过程。在未来,此类攻击将会向越来越多的人开放。因此,实现完全状态到期的实际成本可能没有看起来那么高,因为无论如何我们都必须解决这个非常具有挑战性的地址问题。
进行状态过期可能会使从一种状态树格式到另一种状态树格式的转换变得更容易,因为不需要转换过程:您可以简单地开始使用新格式创建新树,然后执行硬分叉来转换旧树。因此,虽然状态到期很复杂,但它确实有利于简化路线图的其他方面。
安全性、可访问性和可用性的关键先决条件之一 可信的中立性 是简单。如果协议美观且简单,就会减少出现错误的可能性。它增加了新开发人员能够参与其中的任何部分的机会。它更有可能是公平的,也更容易抵御特殊利益。不幸的是,协议就像任何社交系统一样,默认情况下会随着时间的推移而变得更加复杂。如果我们不希望以太坊陷入日益复杂的黑洞,我们需要做以下两件事之一:(i)停止进行更改并使协议僵化, (ii) 能够实际删除功能并降低复杂性。一种中间路线,即对协议进行较少的更改,并且随着时间的推移至少消除一点复杂性,这也是可能的做到的。本节将讨论如何减少或消除复杂性。
没有任何重大的单一修复可以降低协议的复杂性;这个问题的本质是有许多小的解决办法。
@vbuterin/selfdestruct">一个基本已经完成的示例是删除 SELFDESTRUCT 操作码。 SELFDESTRUCT 操作码是唯一可以修改单个块内无限数量存储槽的操作码,要求客户端实现显着更高的复杂性以避免 DoS 攻击。该操作码的最初目的是实现自愿状态清算,从而允许状态大小随着时间的推移而减小。实际上,很少有人最终使用它。在 Dencun 硬分叉中,该操作码被削弱为仅允许在同一交易中创建的自毁账户。这解决了 DoS 问题,并允许显著简化客户端代码。 未来,最终完全删除该操作码可能是有意义的。
迄今为止,已确定了一些关键的协议简化机会。首先,让我们看看 EVM 之外的例子。这些变更相对非侵入性,因此更容易达成共识并在短期内实施。
以下是 目前 EVM 中的一些示例:
进行此类功能简化的主要权衡在于(i)我们简化的程度和速度与(ii)向后兼容性之间的平衡。以太坊作为一条链的价值源于它是一个平台,用户可以在其中部署应用程序,并确信这些应用在多年后仍能正常运行。然而,这种理想也可能被过分强调。借用William Jennings Bryan 的话,我们不应”将以太坊钉在向后兼容性的十字架上”。设想一个场景:如果整个以太坊生态系统中只有两个应用程序在使用某个特定功能,其中一个多年来没有用户,几乎完全闲置,而且这两个应用的总价值仅为 57 美元。在这种情况下,我们应该考虑删除该功能,必要时甚至可以自掏腰包向受影响的用户补偿这 57 美元。
更广泛的社会问题在于创建一个标准化的管道来进行非紧急的向后兼容性破坏的更改。解决这个问题的一种方法是检查和扩展现有的先例,例如自毁过程。该流程看起来如下:
在第 1 步和第 4 步之间应该有一个持续数年的流程,并明确标识每个项目所处的步骤。此时,我们需要权衡”功能移除流程的力度和速度”与”采取更保守的方法并将更多资源投入协议开发的其他领域”。尽管如此,我们距离”帕累托前沿(Pareto frontier)”还有相当长的距离。
对 EVM 提出的一系列主要更改是 EVM 对象格式 (EOF)。 EOF引入了大量的改变,例如禁止gas可观察性、代码可观察性(即无CODECOPY)、仅允许静态跳转。目标是允许 EVM 以具有更强属性的方式进行更多升级,同时保持向后兼容性(因为 EOF 之前的 EVM 仍然存在)。
这样做的优点是,它创建了一条添加新 EVM 功能的自然路径,并鼓励迁移到具有更强保证的更严格的 EVM。它的缺点是它显着 增加 协议复杂性,除非我们能找到一种方法最终弃用并删除旧的 EVM。一个主要问题是: EOF 在 EVM 简化提案中发挥什么作用,特别是如果目标是降低整个 EVM 的复杂性?
路线图其余部分中的许多“改进”建议也是对旧功能进行简化的机会。重复上面的一些例子:
一个更激进的以太坊简化策略是保持协议不变,但将其大部分从协议功能转移到合约代码。
最激进的方案是将以太坊 L1 “技术上”简化为信标链,并引入一个精简的虚拟机(VM)。这可以是RISC-V、Cairo,或专为证明系统设计的更简单 VM。这种设计将允许开发者创建自己的 Rollup,而 EVM 将成为这些 Rollups 中的首个实例。有趣的是,这一方案与2019-20 年提出的执行环境方案殊途同归,尽管 SNARK 技术的进步使得这一设想更易实现。
一种更温和的方法是,保持信标链和当前以太坊执行环境之间的关系不变,但对 EVM 进行就地交换。我们可以选择 RISC-V、Cairo 或其他 VM 作为新的“官方以太坊 VM”,然后将所有 EVM 合约强制转换为解释原始代码逻辑的新 VM 代码(通过编译或解释)。理论上来说,甚至可以将这个”目标 VM”作为 EOF 的一个版本来实现。