作者:Vitalik,以太坊创始人;翻译:0xjs@金色财经
在以太坊中,直到最近,资源都是有限的,并且使用一种称为“Gas”的单一资源定价。 Gas是处理给定交易或区块所需的“计算量”的量度。 Gas 将多种类型的“努力”融合在一起,最值得注意的是:
例如,我发送的这笔交易(https://etherscan.io/tx/0xc5195b64cc333b8098d71fbd0f032e05d4545917e3b0be8d123ab06e1ad7998e)总共花费了 47,085 Gas。这分为 (i) 21000 Gas 的“基本成本”,(ii) 作、包含部分交易的调用数据字节的 1556 Gas,(iii) 用于读取和写入存储的 16500 Gas,(iv) 用于生成日志的2149 Gas,其余用于 EVM 执行。用户必须支付的交易费用与交易消耗的gas成正比。一个区块最多可包含 3000万 Gas,并且 Gas 价格通过EIP-1559 目标机制不断调整,确保区块平均包含1500万 Gas。
这种方法有一个主要效率:因为所有东西都合并到一个虚拟资源中,所以它导致了非常简单的市场设计。优化交易以最小化成本很容易,优化区块以收取尽可能高的费用相对容易(不包括MEV),并且没有奇怪的激励措施鼓励某些交易与其他交易捆绑在一起以节省费用。
但这种方法也有一个主要的低效率问题:它将不同的资源视为可以相互转换,而网络可以处理的实际潜在限制却并非如此。理解这个问题的一种方法是看这个图:
Gas 限制强制执行以下约束:x1*data+x2*computation<N。实际的底层安全约束通常更接近max(x1*data,x2*computation)<N。这种差异导致Gas限制不必要地排除实际安全的区块,或接受实际不安全的区块,或两者的某种混合。
如果有?个资源具有不同安全限制,那么一维gas可能会降低吞吐量高达一个因子?。因此,长期以来人们对多维Gas的概念很感兴趣,通过EIP-4844,我们今天实际上在以太坊上运行了多维Gas。这篇文章探讨了这种方法的好处以及进一步增强这种方法的前景。
Blobs:Dencun中的多维Gas
今年年初,以太坊平均区块大小为 150 kB。该大小的很大一部分是Rollup数据:为了安全起见,2 层协议将数据存储在链上。这些数据非常昂贵:尽管Rollup交易的成本比以太坊 L1 上的相应交易低约 5-10 倍,但对于许多用例来说,这个成本仍然太高。
为什么不降低 calldata 的 Gas 成本(目前每个非零字节 16 Gas,每个零字节 4 Gas),以使Rollup更便宜?我们以前这样做过,我们可以再做一次。这里的答案是:最坏情况下的区块大小是30000000/16=1875000非零字节,并且网络已经几乎无法处理该大小的区块。如果成本再降低 4 倍,则最大容量将增加到 7.5 MB,这将带来巨大的安全风险。
这个问题最终通过在每个区块中引入一个单独的易于Rollup的数据空间(称为“blob”)来解决。这两种资源有不同的价格和不同的限制:在 Dencun 硬分叉之后,一个以太坊区块最多可以包含 (i) 3000 万 Gas,以及 (ii) 6 个 blob,每个 blob 可以包含约 125 kB 的调用数据。这两种资源都有单独的价格,通过单独的类似 EIP-1559 的定价机制进行调整,目标是每个区块平均使用 1500 万Gas 和 3 个 blob。
结果,rollups 的成本降低了 100 倍,rollups 上的交易量增加了 3 倍以上,而理论上的最大区块大小仅略有增加:从约 1.9 MB 增加到约 2.6 MB。
Rollup交易费用,由Growthepie.xyz提供。 Dencun 分叉发生于 2024 年 3 月 13 日,引入了多维定价的 blob。
多维gas和无状态客户端
在不久的将来,无状态客户端的存储证明也会出现类似的问题。无状态客户端是一种新型客户端,它将能够验证区块链而无需在本地存储大量或任何数据。无状态客户端通过接受该区块中的交易需要接触的以太坊特定部分的证明来实现这一点。
无状态客户端接收一个区块,以及证明区块执行涉及的状态特定部分(例如帐户余额、代码、存储)的当前值的证明。这允许节点在没有任何存储本身的情况下验证区块。
一次存储读取需要花费 2100-2600 Gas,具体取决于读取类型,而存储写入成本更高。平均而言,一个区块会执行大约 1000 次存储读写(包括 ETH 余额检查、SSTORE调用SLOAD、合约代码读取等操作)。然而,理论上的最大值是30000000/2100=14285次读取。无状态客户端的带宽负载与该数字成正比。
今天,该计划是通过将以太坊的状态树设计从Merkle Patricia 树转移到Verkle 树来支持无状态客户端。然而,Verkle 树不具备量子抗性,并且对于更新的 STARK 证明系统来说并不是最佳选择。因此,许多人有兴趣通过二元 Merkle 树和STARK支持无状态客户端- 要么完全跳过 Verkle,要么在 Verkle 过渡几年后,一旦 STARK 变得更加成熟,就进行升级。
二叉哈希树分支的 STARK 证明有很多优点,但它们有一个关键弱点,即证明需要很长时间才能生成:虽然Verkle 树每秒可以证明超过十万个值,但基于哈希的 STARK 通常只能每秒证明几千个哈希,证明每个值需要一个包含许多哈希值的“分支”。
考虑到今天从超优化的证明系统(例如Binius和Plonky3)以及专门的哈希(例如Vision-Mark-32)预测的数字,我们很可能在一段时间内处于一种可以不到一秒实际证明 1,000 个值的状态,但不是 14,285 个值。每个区块平均没问题,但最坏情况下的区块(可能由攻击者发布)会破坏网络。
我们处理这种情况的“默认”方式是重新定价:使存储读取更加昂贵,以将每个区块的最大值减少到更安全的程度。然而,我们已经这样做过很多次了,如果再这样做的话,太多的应用程序会变得过于昂贵。更好的方法是多维 Gas:分别对存储访问进行限制和收费,将平均使用量保持在每个区块 1,000 次存储访问,但将每个区块的限制设置为例如 2,000次。
更普遍的多维Gas
另一种值得考虑的资源是状态大小增长:增加以太坊状态大小的操作,全节点将需要持有完整状态。状态规模增长的独特之处在于,限制状态增长的理由完全来自长期持续的使用,而不是峰值。因此,为状态规模增加操作(例如,从零到非零SSTORE、合约创建)添加单独的 Gas 维度可能有价值,但目标不同:我们可以设置浮动价格来针对特定的平均使用量,但是根本不设置每个区块的限制。
这显示了多维Gas的强大特性之一:它让我们分别询问以下问题:(i)每种资源的理想平均使用量是多少,以及(ii)每个区块的安全最大使用量是多少。我们不是根据每个区块的最大值来设置 Gas 价格,而是平均使用量,其有2?自由度来设置2?参数,并根据对网络安全的情况调整每一项。
更复杂的情况,例如两个资源具有部分累加的安全考虑,可以通过使操作码或资源花费一定数量的多种类型的gas来处理(例如,零到非零SSTORE可能花费 5000 无状态客户端证明的gas和20000存储膨胀的gas)。
每笔交易取最大值:获取多维Gas的较弱但更简单的方法
让x1是数据的 Gas 成本,x2是计算的 Gas 成本,因此在一维 Gas 系统中我们可以写出一笔交易的 Gas 成本gas=x1*data+x2*computation
在新方案中,我们将交易的 Gas 成本定义为:gas=max(x1*data,x2*computation)
也就是说,交易不是根据数据加计算来收费,而是根据它消耗的两种资源中哪一种资源更多来收费。这可以很容易地扩展到覆盖更多维度(例如max(…,x3*storage_access))。
应该很容易看出这如何在保证安全性的同时提高吞吐量。理论上一个区块中的最大数据量仍然是GASLIMIT/x1,与一维Gas方案完全相同。同理,理论最大计算量为GASLIMI/x2,再次与一维Gas方案完全相同。然而,任何消耗数据和计算的交易的 Gas 成本都会降低。
这大约是提议的EIP-7623中采用的方案,以减少最大区块大小,同时进一步增加 blob 计数。 EIP-7623 中的精确机制稍微复杂一些:它保持当前的 calldata 价格为每字节 16 Gas,但增加了每字节 48 Gas 的“底价”;交易支付 ( 16 * bytes + execution_gas) 和 ( 48 * bytes) 中较高者。因此,EIP-7623 将区块中理论最大交易调用数据从约 1.9 MB 减少到约 0.6 MB,同时保持大多数应用程序的成本不变。这种方法的好处是它与当前的一维gas方案相比变化非常小,因此非常容易实现。
它有两个缺点:
1、即使区块中的所有其他交易只使用很少的该资源,但大量占用一种资源的交易仍然会不必要地收取大量费用。
2、它激励数据密集型和计算密集型交易合并到一个捆绑包中以节省成本。
我认为,EIP-7623 风格的规则,无论是对于交易调用数据还是其他资源,都可以带来足够大的好处,即使存在这些缺点,也是值得的。然而,如果我们愿意投入(显著更高的)开发努力,就有一种更理想的方法。
多维 EIP-1559:更困难但理想的策略
让我们首先回顾一下“常规”EIP-1559 的工作原理。我们将重点关注 EIP-4844 中针对 blob 引入的版本,因为它在数学上更加优雅。
我们跟踪一个参数,excess_blobs。在每个块期间,我们设置:
excess_blobs <-- max(excess_blobs + len(block.blobs) - TARGET, 0)
这里TARGET = 3。也就是说,如果一个区块的blob 数量多于目标,excess_blobs则增加,如果区块的 blob 数量少于目标,则减少。然后我们设置blob_basefee = exp(excess_blobs / 25.47),其中exp是指数函数的近似值exp(x)。
也就是说,每当excess_blobs增加约 25 倍时,blob 基本费用就会增加约 2.7 倍。如果 blob 变得太贵,平均使用量就会下降,然后excess_blobs开始下降,从而自动再次降低价格。 Blob 的价格不断调整,以确保平均而言,区块是半满的 - 也就是说,每个区块平均包含 3 个 Blob。
如果使用量出现短期峰值,则会出现限制:每个区块最多只能包含 6 个 blob,在这种情况下,交易可以通过提高优先费来相互竞争。然而,在正常情况下,每个 blob 只需要支付blob_basefee加一点额外的优先权费用,作为被纳入的激励。
这种 Gas 定价在以太坊中已经存在多年:早在 2020 年,EIP-1559就引入了非常相似的机制。通过 EIP-4844,我们现在有两个单独的 Gas 和 Blob 浮动价格。
2024 年 5 月 8 日一小时内的 Gas 基本费用,单位为 gwei。来源:ultrasonic.money
原则上,我们可以为存储读取和其他类型的操作添加更多单独浮动的费用,但有一个警告,我将在下一节中详细说明。
对于用户来说,这种体验与今天非常相似:你不再支付一笔基本费用,而是支付两项基本费用,但你的钱包可以将其从你那里抽象出来,只向你显示你可以预期支付的预期费用和最高费用。
对于区块构建者来说,大多数时候最佳策略与今天相同:包括任何有效的内容。大多数区块都未满——无论是gas还是blob。一个具有挑战性的情况是,当有足够的gas或足够的blob超过区块限制时,构建者需要潜在地解决多维knapsack问题以最大化其利润。然而,即使存在相当好的近似算法,在这种情况下,通过制定专有算法来优化利润所获得的收益也比使用 MEV 进行相同操作所获得的收益要小得多。
对于开发者来说,主要挑战是需要重新设计 EVM 及其周边基础设施的功能,这些基础设施目前是围绕一个价格和一个限制设计的,而设计为适应多个价格和多个限制的设计。应用程序开发人员面临的一个问题是优化变得稍微困难 :在某些情况下,你不能再明确地说 A 比 B 更高效,因为如果 A 使用更多的 calldata 而 B 使用更多的执行,那么当 calldata 为便宜,当 calldata 昂贵时则更昂贵。然而,开发者仍然能够通过根据长期历史平均价格进行优化来获得相当好的结果。
多维定价、EVM 和子调用(sub-calls)
有一个问题不会出现在 blob 中,也不会出现在 EIP-7623 中,甚至不会出现在 calldata 的“完整”多维定价实现中,但如果我们尝试单独对状态访问或任何其他资源进行定价,则会出现这个问题:子调用中的gas 限制。
EVM 中的 Gas 限制存在于两个地方。首先,每笔交易都会设置一个 Gas 限制,该限制限制了该交易中可以使用的 Gas 总量。其次,当一个合约调用另一个合约时,该调用可以设置自己的gas limit。这允许合约调用他们不信任的其他合约,并且仍然保证他们在调用后仍有剩余的gas来执行其他计算。
帐户抽象交易的踪迹,其中一个帐户调用另一个帐户,并且仅向被调用者提供有限数量的gas,以确保即使被调用者消耗了分配给它的全部gas,外部调用也可以继续运行。
挑战在于:在不同类型的执行之间使 Gas 成为多维的,似乎需要子调用来为每种类型的 Gas 提供多个限制,这将需要对 EVM 进行真正深入的更改,并且与现有应用程序不兼容。
这就是多维gas提案通常停留在两个维度的原因之一:数据和执行。数据(无论是交易 calldata 还是 blob)仅在 EVM 外部分配,因此 EVM 内部无需更改任何内容即可使 calldata 或 blob 单独定价。
我们可以想出一个“EIP-7623式的解决方案”来解决这个问题。这是一种简单的实现:在执行期间,对存储操作收取 4 倍的费用;为了简化分析,我们假设每个存储操作为10000 gas。交易结束,退款min(7500 * storage_operations, execution_gas)。结果是,在扣除退款后,用户需要支付以下费用:
execution_gas + 10000 * storage_operations - min(7500 * storage_operations, execution_gas)
这等于:
max(execution_gas + 2500 * storage_operations, 10000 * storage_operations)
这反映了 EIP-7623 的结构。另一种方法是实时跟踪storage_operations和execution_gas,并根据调用操作码时上涨的金额max(execution_gas + 2500 * storage_operations, 10000 * storage_operations)多少收取 2500 或 10000 gas。这避免了交易需要过度分配天gas,而这些gas主要通过退款来收回。
我们没有获得子调用的细粒度许可:子调用可能会消耗交易的所有“津贴”以进行廉价的存储操作。但我们确实得到了足够好的东西,其中进行子调用的合约可以设置限制,并确保一旦子调用完成执行,主调用仍然有足够的gas来执行它需要执行的任何后处理。
我能想到的最简单的“完整的多维定价解决方案”是:我们将子调用gas限制视为成比例。也就是说,假设有
?个不同类型的执行,每笔交易设置多维度限制?1…??。假设在当前执行点,剩余gas
为?1…??。假设CALL调用了一个操作码,带有子调用gas limit S。让s1=S, 进而s2=s1/g1*g2,s3=s1/g1*g3, 等等。
也就是说,我们将第一种类型的gas(实际上是虚拟机执行)视为一种特权“帐户单位”,然后分配其他类型的gas,以便子调用在每种类型建获得相同百分比的可用gas。这有点难看,但它最大限度地提高了向后兼容性。如果我们想让方案在不同类型的gas之间更加“中立”,以牺牲向后兼容性为代价,我们可以简单地让子调用gas limit参数代表剩余gas的一小部分(例如[1...63] / 64)。
然而,无论哪种情况,值得强调的是,一旦开始引入多维执行gas,固有的丑陋程度就会增加,这似乎很难避免。
因此,我们的任务是做出一个复杂的权衡:我们是否接受 EVM 级别的一些更丑陋的东西,以便安全地释放显著的 L1 可扩展性收益,如果是这样,哪种具体建议最适合协议经济学和应用程序开发人员?
很可能,它不是我上面提到的任何一个,并且仍然有空间想出更优雅、更好的东西。
特别感谢Ansgar Dietrichs、Barnabe Monoton 和 Davide Crapis的反馈和审查。