以太坊使用 EOF 的理由

EVM对象格式(EOF)是一项期待已久的EVM升级,旨在现代化以太坊语言和工具生态。本文详细论述了EOF在开发工具、编译器、语言及应用程序等方面的优势,强调其对提高可用性和改善用户体验的重要性。相较于其他提案,EOF提供了一种清晰且结构化的方法来解决当前EVM中的多项问题。

The EVM Object Format 是一个期待已久的升级,它使 EVM 现代化,并消除了对以太坊整个语言、工具和应用生态系统产生广泛影响的障碍。 Solidity 全力支持这一提议,在这篇文章中,我们想解释原因。 我们将讨论它带来的好处,以及目前提出的替代方案为何未能实现其目标。

EOF 的好处

工具

开发工具是 EOF 好处最明显的领域。 当前 EVM 在这方面最大的缺陷是字节码结构不够,以至于无法有效分析。 这使得开发工具变得更为困难,而 EOF 是降低入门门槛的重要一步。

形式验证和静态分析

暴露更多结构对形式验证和静态分析工具至关重要,这些工具变得越来越不可或缺。 一旦合约被编译,它就失去了大量有用的高级信息,这些信息无法被完美重建。 例如,像区分函数和循环这样概念上简单的事情,在没有额外调试信息的情况下变得不再显而易见,而这些信息在区块链上并不存在。 动态跳转和数据与代码之间缺乏清晰分离也是正确分析的障碍。

转译

在其本地环境之外执行 EVM 代码的能力有很多应用。 一个显而易见的应用是运行在使用完全不同虚拟机的链上,或者在 SNARK 中。 长期以来,zkEVM 是 L2 的圣杯,正是这样一种应用。

在另一个虚拟机内对 EVM 进行完全模拟,即便有可能,也在性能上带来了可观的开销。 字节码转译,即直接翻译为一组不同的底层指令,同时保持相同的语义,可能是一个更好的解决方案,而 EOF 的雄心勃勃的目标之一就是使其更易实现。

目前合约可以检查自身的实现细节,例如消耗的确切 gas 数量或字节级结构,这意味着这些细节也必须在目标架构上进行模拟,使得翻译变得困难甚至不可能。 禁止代码和 gas 反射意味着 EOF 合约将更易于转译。

仅使用静态跳转是另一个重要的简化限制,使得在合约中预先确定并验证所有代码路径成为可能。

代码验证

Solidity 和现有编译器发出的字节码已经具有明确定义的内部结构,但缺乏适当的容器,导致其未被标记,这意味着它本身无法被发现。 这一事实使得工具利用这种结构变得比本来复杂得多。 例如,源代码验证需要在字节码中识别不可变变量和 元数据 的位置。 虽然元数据被附加在运行时代码的末尾以便于识别,但合约字节码可以是嵌套的,这使得 可靠找到它成为更大的挑战

这个具体问题实际上可以通过让编译器发出额外信息来解决,但解决方案必须为每种语言单独实现。 一种通用的容器格式使得工具在编译器运作中的独立性大大增强。

编译器和语言

今天存在的 EVM 不是一个容易开发的平台,这多年来大大限制了可用语言的多样性。 生态系统被 Solidity 主导,而 Vyper 尽管是一个成熟的全功能语言,但仍然是一个遥不可及的第二名。 虽然 Fe 正在发展成为另一个竞争者,新语言和编译器不时出现,但现实是几乎没有其他语言能在生产中得到充分使用。 他们中的大多数相对较快地消亡,而 这个领域是一片被遗弃项目的墓地,而不是一个繁荣的生态系统。 尽管 Solidity 取得了成功,这一点让我们并不感到高兴。 在这个领域有一个健康的竞争是非常重要的,因为没有单一的语言可以适合所有的需求。

为什么这么难?

为什么新语言在这个领域如此难以流行? 我们认为这与 EVM 本身关系密切。 Gas 优化是一个主要因素。 现有编译器已经建立了复杂的优化器,虽然高级开发人员仍然可以通过手工编写汇编代码轻松超越它们,但现实是,在新的编译器中达到足够的优化水平仍是一项巨大的工程任务。

这是内联的一个主要例子。创建良好的内联启发式存在两个相互矛盾的压力。 一方面,跳转指令相对昂贵,这使得尽可能多地内联成为了当务之急。 另一方面,内联函数增加了局部变量的数量和所需堆栈空间,因而并不总是可以内联。 这是一个微妙的平衡,EOF 通过其更廉价的相对跳转提供了更大的自由度给新编译器。

有限的堆栈是每个新项目都面临的另一个重大障碍。 在受限的堆栈空间下进行最佳堆栈调度是一个未解决的问题。 我们资助了这个主题的长期研究项目,但这并不应该是开始的前提。 消除这一限制将有助于使初步的朴素方法变得可行。

另一个例子是 不可变变量。降低 gas 成本的愿望通常会导致一些不直观的解决方案,其中之一是使用合约的字节码存储值,而不是使用存储或内存。 目前,这需要在部署期间对字节码进行临时编辑,并且当想要支持超过定尺寸值类型的任何类型时会迅速变得复杂。 如今,这样的机制是理所当然的,然而新编译器必须重新发明这些。 EOF 的专用数据部分使得这一切变得简单。 编译器可以将一组值放入数据部分,而无需将其破坏到代码中去。

这不过是冰山一角。EOF 特性的还有许多较小的间接好处。 例如,前期验证提供了一些保护,使得生成无效代码的风险降低。EOF 的功能使得从高级语言翻译变得更加直接。 虽然成熟的编译器已经有其自身的机制来处理这些,但在项目初期依赖它们以简化开发的选项不容忽视。

最后,编译器一个主要的优势是它只需要将合约编译为可执行的形式,而不必生成所有可能的字节码形式。 它可以轻松决定仅支持 EOF 而不会损害其用户,这也是我们预计新项目将如此做的原因。 Solidity 也期待尽快放弃对旧版 EVM 的支持,利用这种机制并减少整体维护负担。 工具和应用也很可能随之而来。

应用

dApps 将主要间接受益于改进的编译器和工具,然而,它们仍将受益于一些较小的直接可观察的改进。

放松字节码大小限制

其中之一是提升字节码大小限制的潜力 (EIP-7830)。 目前部署的字节码大小不能超过~24 kB,这导致出现了多个复杂的合约拆分模式,例如警戒代理,这将不再必要。

节省 gas

正如我们当前的 EOF 原型所示,编译为 EOF 的合约通常规模更小,成本更低。 例如,一项早期基准研究 (Measurable benefits of EOF) 表明某些真实合约的改进在 10-15% 范围内。

尽管这看起来不算突出,但请注意这些数值来源于不完整的原型,其基于操作码优化器的大部分组件在 EOF 上仍未实现。 随着块去重器、用于堆栈洗牌的 EXCHANGE、尾调用优化、放宽内联以及能够删除未使用的 RETURNDATACOPY 等,可能会获得更高的节省。

我们还想强调,节省 gas 并不是 EOF 的主要目的。 在设计时,重点是确保现有合约不会变得更昂贵,因为任何成本增加都将对采用构成重大抑制。 在这方面,EOF 已经超出我们的预期,任何收益只是锦上添花。

L2

一种普遍的说法是 二层 是像 EOF 这样的创新应该发生的地方,因为 L2 在实验执行层方面有更大的自由。 不幸的是,当前情况并不是这样。 对 EVM 兼容性的高度重视意味着 L2 紧密跟随 L1,往往滞后于它而非扩展它。 目前推动 EVM 发展的依然是以太坊主网。

我们坚信,等待 L2 引领将导致执行层完全停滞。 EOF 必须首先在主网上采用,这将使其非常可能被 L2 以自己的节奏拾起。

重要的是,像编译器一样,L2 可以仅需对 EVM 的一个合理而自我包含的子集感到满意,而不是它的全部,包括所有遗留和瑕疵,只要这样的子集存在。 作为 L1 的一部分,意味着它们仍然是 EVM 兼容的。 它们在字节码级别上负担较小,因为要求合约在部署前重新编译是合理的。

轻松转译的潜力也是对非 EVM 基础的 L2 可能有帮助的特性。

客户端

毋庸置疑,以太坊客户端承受着这一变化的大部分维护负担,因为它们在可预见的未来将不得不同时支持 EOF 和旧版 EVM。 采用 EOF 对客户端的好处也并不那么明显。 例如,字节码执行中的性能提升可能会被其他因素所掩盖。

然而,我们怀疑这种负担被大大夸大了。 EOF 不是一个完全新的虚拟机设计。 它在与旧版 EVM 的相似性上远超过其差异。

从长远来看,客户端的维护负担,以及更一般来说,旧版 EVM 的重要性将会减少。 在为新增功能的 EIP 目标设定时,将没有理由继续关注旧版 EVM,除了确保它仍然有效,并保留现有合约的行为。 当几乎所有合约都部署到 EOF 时,因网络升级而可能真正被打破的合约数量至少将停止增长。

归根结底,这是设计选择不当,在一个需求强大向后兼容性的系统中的代价。 问题在于这种代价是否合理。 对于客户端以上的生态系统来说,EOF 是全面改进。 普遍采用将使旧版 EVM 在那里变得无关紧要。

过早地使执行层僵化

对 EOF 的一个抱怨是它是不必要的,因为以太坊应仅专注于改善其共识层。 扩展是唯一重要的,我们仍能生存,不管当前 EVM 有什么样的瑕疵。 我认为这很短视,更忽略了事情的相对规模。 执行层在很大程度上未得到充分服务,近年来仅仅在一些操作码(例如 PUSH0MCOPY)方面进行了微小的升维。 虽然扩展至关重要,理所当然地得到了大量关注。 但是,糟糕的开发者和用户体验并非完全无关紧要。 EOF 仍然只是对此的一个相对较小的让步。 采纳它几乎并不意味着将所有关注转移到执行层上,而且非常值得为了它解锁的各种功能。

如果观点是,执行层根本不应作出任何变化,我们的看法是,现在还远未时机使其僵化。 这项工作的主要份额将在客户端之上进行。 EOF 是这一工作众多任务的基本前提,能够阻止本可独立发生的改进,并且不会对共识层造成任何干扰,等到我们最终将其推出时。 当然,它也使未来在 EVM 级别的变化成为可能,但得益于它,这些变化将更加渐进。

我们相信,EOF 中包含的变化是足够根本的,如果被拒绝,它们肯定会以某种形式早晚回归。 一次又一次地争论这些,注定将严重浪费核心开发者的时间,而不如直接合并 EOF 更加有效。 EOF 简直是以最有效的方式完成这项工作的最佳途径。

EOF 是唯一的方法吗?

EOF 是一组相互关联、相辅相成的变化。 版本化的容器格式使得代码验证和数据与代码的分离成为可能。 代码验证允许安全地引入带有立即参数的新操作码。 立即参数对于静态跳转、函数和更好的堆栈操作码是必要的。 静态跳转使得移除 JUMPDEST 成为安全的选择。 禁止代码和 gas 的反射也依赖于其中几项变化。 本质上,它是一系列紧密相关的 EIP 的组合,这些都是必要的,结合在一起显得相得益彰。

这些功能并不是新的。 多年来,它们中的大多数以某种方式被提出。 它们从未获得采用,往往是由于显著的未解决缺陷,而解决这些缺陷确实需要进行 EOF 中所做的工作量。

EOF 是非常周密的,能立刻解决这些问题,且在一次性处理上解决。 与其他 EIP 相比,它经过极为充分的测试,目前累计了数千个 EEST 测试。 它已经有一个编译器实现,显示出这一点是可行的。 理论上,它可以以不同方式完成,但不太可能在完善的其他方法中,获得某种以不同方式复杂低于 EOF 的结果。

我们能在没有 EOF 的情况下解决 “堆栈太深” 吗?

作为 Solidity 项目团队,我们一直高度关注解决“堆栈太深”问题,这有必要澄清一点。 虽然这是一个重要问题,但它在很大程度上是偶然与 EOF 的,而且不是其明确目标之一。 EIP-663 早于 EOF,而它只是由于无法安全地添加带有立即参数的操作码而被阻碍的一项 EIP。 我们已请求 Ipsilon 团队将其包含在 EOF 中,理由有三:EIP-663 依赖于 EOF,极其简单,其快速交付直接转化为大量节省的工程努力。 如果该 EIP 不在其中,我们仍将全力支持 EOF,但我们看不到理由 为什么 这 项 EIP 需要在其中,而好处却是巨大的。

我们为何如此渴望? 有人提出 解决 “堆栈太深” 只是编译器设计的挑战,并且可以在不修改 EVM 的情况下解决。 或是通过调整内存价格更简单地进行。 这是真的吗?

首先,SWAPN/DUPN 并不是唯一的解决方案。 它只是应对一个已经浪费了无数工程时光而未得到充分解决的问题的权宜之计。 简化的解决方案不足以完全解决这个问题,而更复杂的方案则需要进行严肃研究(实际上,我们正在进行这种研究)。 同时,语言的用户正在要求其他功能,我们可以将时间用于这些功能的开发。 我们正在探索多条通向目标的路径,而 EIP-663 说实话只是最简单的一条。

16 槽限制是人为限制

事实是……这个挑战在很大程度上是人为的。 16 槽限制并非源于 EVM 的任何固有限制,而对增加更多的潜在缺陷尚未经证实且高度推测。 据我们所知,这一限制仅仅是可寻址堆栈项目数量与可能 1 字节操作码数量之间的任意权衡(只能有 256 个)。 DUP1..DUP16 和 SWAP1..SWAP16 占用了 32 个位置,这被认为已足够。 毕竟,如果必要,还可以添加更多,而移除它们将困难得多。

我们可以花费数年资源在 EVM 或编译器上,以应对这一限制,从而慢慢解决这一限制,抓住这一挑战。 或者我们可以务实地,最终通过提高限制来移除不必要的限制(几乎没有任何后果),并专注于更重要的事情。

内存模型差异

那么,为什么 Solidity 必须解决这个问题,而 Vyper 显然不受影响? 差异在于内存模型。 Solidity 为所有函数参数和局部变量使用堆栈,而 Vyper 将其存储在内存中的固定位置。 Vyper 的方法显著降低了堆栈压力,并使对至多 16 个顶部元素的访问显得足够。 然而,它并非没有限制。 使用固定位置意味着一个函数在调用栈上只能出现一次,实际上有效地阻止了递归(除了通过昂贵的外部调用),并要求每个变量在前设上声明其大小的上限。 变量需要在每次操作时从内存中加载,这增加了额外成本。 设置较大的上限也可能将其位置推向高地址,带来二次内存扩展成本。 即使当前交易中该函数从未被调用,因为必须保证所有函数堆栈帧能够在内存中同时存在。

Vyper 只是受到不同限制的影响——它在堆栈问题上进行了权衡,而对内存定价模型更为敏感,这也并不理想。 这两种方法都不比另一种方法更正确。 这是一种有立场的选择,具有权衡性质。 Vyper 选择以某种方式限制其表达能力,但没有理由认为 EVM 不应允许比这更具表现力的语言。

实际上,我们对更改内存定价根本就不反对。 更便宜的内存将使 Solidity 也受益,即便不是,如今没有理由反对它,只要这对网络没有伤害。 EIP-663 仅仅是一个极其简单的变更,明确带来好处且没有真正的缺陷。 而重新定价内存则需要更为仔细的评估,并不太可能很快到来。 这当然是一个很好的解决方案,但并不适用于相同的问题。

为什么不选择 EIP-615/EIP-2315?

EIP-2315EIP-615 的简化版,并因非常好的原因 被强烈拒绝。 请遵循链接了解详情,但主要问题是它使用了动态跳转(即不仅没有解决 EOF 所解决的问题,反而加剧了这一问题),且没有提供任何防止在函数内或跨函数跳转的保护。 动态跳转的问题最终通过使指令使用立即参数得到解决,但仍未解决固有的 JUMPDEST 问题。

必须说明,EIP-2315 的拒绝并非因为“gas 节省不足”。 据 EIP 的作者称,所谓的 gas 成本节省是其主要目标,然而我们已表明,甚至连这个目标都未实现。 这甚至可能足以使其采用变得不太可能,但最终拒绝的理由则是更根本的。

我们真的需要立即参数吗?

立即参数是某些类型问题的自然解决方案。 值直接嵌入代码中,预先已知,而不是放置在堆栈上的值。

  • 跳转到静态位置恰恰是这样的问题。
    • 在字节码中引用函数或容器基本上是相同的用例。
  • DUPN/SWAPN EIP 在 EOF 之前很久就遇到了这个问题。
  • 在 EOF 之外,甚至 EIP-615/EIP-2315 也执行过此情况。
  • EVMMAX 中的 ADDMODX、SUBMODX 和 MULMODX 操作码 (EIP-6690) 需要 7 个常量参数,所有这些都以立即参数形式实现。

引入立即参数一直是一个众所周知的问题。 争论提出的需求立即参数提案寥寥是混淆因果关系,因为这样做将首先需要先解决这个问题。 EOF 通过将这一目前尚未解决的依赖关系转变为非问题,提供了巨大的价值。

JUMPDEST 问题

多字节操作码的问题 本质上在于它们可能包含 JUMPDEST 操作码的值。 如果这样的序列已经出现在现有合约中,升级将改变它们的行为。 解决这个问题的安全方案通常依赖于引入某种帐户版本化。

即使是 引入 BEGINDATA 的提议 也面临如何安全地引入该操作码本身的问题,因为其本质上等同于具有很大立即参数的操作码,易受相同的问题影响。 EOF 目前已提供非常优雅的版本化解决方案。

基于区块号的版本化当然在可能的情况下优于其他方案,因为它确保所有合约的语义相同。 问题在于,成为唯一的解决方案意味着许多 EIP 无法成功落地,因为总有一些合约将在新语义下被打破。 EOF 不排除它,而是提供了一种替代方案。

PUSH 破解

实际上,有一种方法可以安全地破解 JUMPDEST 问题:使用强制 PUSH 操作码。 如果我们要求操作码总是由压入堆栈的值前置,而不是添加立即参数,我们就能避免 JUMPDEST 问题,并保证该值是已知和恒定的,就像立即参数一样。 本质上,我们可以通过以下方式模拟操作码:

RJUMP 0x42

通过以下方式实现:

PUSH1 0x42
RJUMP

这种黑客攻击之前在过去已经提出,但至今从未被认真考虑过。

其缺点之一是,该序列长度增加了 50%,这一点可能是一个可接受的权衡,适用于很少使用的操作码,但如果用于跳转等功能,这仅会加重问题,因为跳转本身已然高价。 以如此复杂的方式实现具有可变数量立即参数的指令,如 RJUMPV,也更加复杂。

总体而言,尽管这是一种可行的临时解决方法,但作为一个普遍解决方案,它是不可接受的。 它完全偏离了要点,旨在从短期内解决单个问题,同时在长期削弱 EVM 的设计。

总结

EOF 将惠及所有依赖执行层的整个堆栈:编译器、工具、应用程序,甚至 L2。 它简化了与 EVM 的接触面,同时也提供了未来扩展更易于实现的必要前提。 我们无法完全摆脱旧版 EVM,但生态系统中的很大一部分将能够或多或少地将其抛诸脑后。

从理论上讲,这并不是唯一的选择,但它在这里,它已就绪,内聚且在我们看来绝对必要。

  • 原文链接: soliditylang.org/blog/20...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
SolidityLang
SolidityLang
https://soliditylang.org/