Vitalik新作:什么是多维Gas定价?

转载
201 天前
7454
Vitalik Buterin

文章转载来源: Vitalik Buterin

作者:Vitalik Buterin

编译:Karen,Foreisght News

在以太坊中,资源直到最近还是有限的,并通过一种称为「Gas」的单一资源来定价。Gas 是衡量处理特定交易或区块所需「计算量」(computational effort)的度量单位。Gas 将多种类型的「计算量」融合在一起,其中最重要的包括:

1、原始计算(Raw computation,例如 ADD、MULTIPLY);

2、读写和写入以太坊存储(例如 SSTORE、SLOAD、ETH 转账);

3、数据带宽;

4、生成区块的 ZK-SNARK 证明的成本。

例如,我发送的这笔交易总共消耗了 47,085 Gas。其中包括:(i)基本成本为 21000 Gas,(ii)作为交易一部分包含的 calldata 字节消耗了 1556 Gas,(iii)读写存储消耗了 16500 Gas,(iv)生成日志(log)消耗了 2149 Gas,其余用于 EVM 执行。用户必须支付的交易费用与交易消耗的 Gas 成正比。一个区块最多可以包含 3000 万 Gas,并且 Gas 价格通过 EIP-1559 targeting 机制不断调整,确保每个区块平均包含 1500 万 Gas。

这种方法有一个主要的优势:因为所有内容都合并为一个虚拟资源,所以市场设计非常简单。优化交易以最小化成本很容易,优化区块以收取尽可能高的费用相对容易(不包括 MEV),并且没有奇怪的激励机制鼓励一些交易与其他交易捆绑以节省费用。

然而,这种方法也存在低效性:它将不同的资源视为可以相互转换,而实际的底层限制并不一样。要理解这个问题,你可以先看下面这张图表:

Gas 限制强加了一个约束条件:

实际的底层安全约束通常更接近:

这种差异导致 Gas 限制要么无端地排除了实际安全的区块,要么接受了实际上不安全的区块,或者两者兼而有之。

如果有 n 种资源具有不同的安全限制,那么一维 Gas 可能会使吞吐量最多降低 n 倍。因此,长期以来,人们一直对多维 Gas 的概念感兴趣,而通过 EIP-4844,我们现在实际上已经在以太坊上实现了多维 Gas。本文探讨了这种方法的优点,以及进一步进行增强的前景。

Blob:Dencun 中的多维 Gas

今年年初,平均区块大小为 150 kB。其中很大一部分是 Rollup 数据:Layer2 协议在链上存储数据。这些数据非常昂贵:尽管 Rollup 上的交易成本仅为以太坊 L1 上相应交易的 5-10 倍,但即使这样的成本对于许多用例来说也太高了。

那为什么不降低 calldata 的 Gas 成本(目前非零字节为 16 Gas,零字节为 4 Gas),以使 Rollup 更便宜呢?我们之前这样做过,现在也可以再次这样做。但这里的答案是:区块的最大大小是 30,000,000/16=1,875,000 非零字节,而网络勉强能或者几乎不能处理这样大小的区块了。再将成本降低 4 倍会使最大值提高到 7.5 MB,这将给安全性带来巨大风险。

这个问题最终通过在每个区块中引入一个独立的、对 Rollup 友好的数据空间(称 blob)来解决。

这两种资源有不同的价格和限制:在 Dencun 硬分叉之后,一个以太坊区块最多可以包含(i)3000 万 Gas 和(ii)6 个 blob,每个 blob 可以包含约 125 kB 的 calldata。这两种资源都有单独的价格,并通过单独的类似于 EIP-1559 的定价机制进行调整,目标是每区块平均使用 1500 万 Gas 和 3 个 blob。

结果是,Rollup 的成本降低了 100 倍,Rollup 上的交易量增加了 3 倍以上,而理论上的最大区块大小仅略有增加:从约 1.9 MB 增加到约 2.6 MB。

注:Rollup 交易费用,由 Growthepie.xyz 提供。Dencun 分叉发生于 2024 年 3 月 13 日,引入了多维定价 blob。

多维 Gas 和无状态客户端

在不久的将来,无状态客户端(stateless clients)的存储证明也会出现类似的问题。无状态客户端是一种新型客户端,将能够验证链而无需在本地存储大量或任何数据。无状态客户端通过接受该区块中交易需要访问的以太坊状态的特定部分的证明来实现这一点。

上图展示了一个无状态客户端接收一个区块,以及证明该区块执行所触及的状态特定部分(例如,账户余额、代码、存储)当前值的证明,这使得节点能够在没有任何存储的情况下验证一个区块。

一次存储读取需要花费 2100-2600 Gas,具体取决于读取类型,而存储写入成本更高。平均而言,一个区块会执行大约 1000 次存储读写操作(包括 ETH 余额检查、 SSTORE 和 SLOAD 调用、合约代码读取和其他操作)。然而,理论上的最大值是 30,000,000/2,100=14,285 次读取。无状态客户端的带宽负载与该数字成正比。

目前的计划是通过将以太坊的 State tree 设计从 Merkle Patricia trees 转变为 Verkle trees 来支持无状态客户端。然而,Verkle trees 不具备抗量子性,并且对于较新的 STARK 证明系统来说并不是最优选择。因此,许多人有兴趣通过二进制 Merkle trees 和 STARKs 来支持无状态客户端,要么完全跳过 Verkle,要么在 Verkle 过渡几年后,一旦 STARK 变得更加成熟,就进行升级。

基于二进制哈希树分支的 STARK 证明具有许多优点,但其关键弱点在于生成证明的时间很长:Verkle 树可以每秒证明十万个以上的值,基于哈希的 STARKs 通常只能每秒证明几千个哈希,而证明每个值都需要包含许多哈希的「分支」(branch)。

考虑到今天从 Binius 和 Plonky3 等超优化证明系统以及 Vision-Mark-32 等专用哈希中预测的数字,我们似乎将在一段时间内处于一个实用范围内,即每秒证明 1000 个值是可行的,但证明 14,285 个值则不可行。平均区块会没问题,但潜在最坏情况下的区块(由攻击者发布)会破坏网络。

我们处理此类情况的 default 方法是重新定价:提高存储读取的成本,以减少每个区块的最大值到更安全的水平。但是,我们已经这样做了很多次,如果再次这样做,会使太多应用变得太昂贵。一个更好的方法是多维 Gas:分别对存储访问进行限制和收费,将平均使用量保持在每个区块 1,000 次存储访问,但设置每个区块的上限进行设置,例如 2000 次。

多维 Gas 的普遍性

另一个值得考虑的资源是状态大小的增长:即增加以太坊状态大小的操作,这些操作之后需要全节点来保存。状态大小增长的独特之处在于,限制它的理由完全来自于长期持续的使用,而不是峰值。

因此,为增加状态大小的操作(例如,zero-to-nonzero SSTORE、合约创建)添加一个单独的 Gas 维度可能是有价值的,但目标不同:我们可以设定一个浮动价格来针对特定的平均使用量,但完全不设置每个区块的限制。

这展示了多维 Gas 的一个强大属性:它让我们能够分别针对每个资源,询问(i)理想平均使用量是多少?(ii)每个区块的安全最大使用量是多少?与基于每个区块的最大值来设定 Gas 价格,并让平均使用量跟随其后不同,我们有 2n 自由度来设定 2n 参数,根据对网络安全的考虑来调整每一个参数。

更复杂的情况,例如当两种资源的安全性考虑部分相加时,可以通过使一个操作码或资源消耗多种类型的 Gas 的某种数量来处理(例如,一个 zero-to-nonzero SSTORE 可能消耗 5000 个无状态客户端证明 Gas 和 20000 个存储扩展 Gas)。

每笔交易 Max(选取数据或计算消耗更大的那种)

令 ?1 为数据的 Gas 成本, ?2 为计算 Gas 成本,因此在一维 Gas 系统中我们可以写出一笔交易的 Gas 成本:

在这个方案中,我们将交易的 Gas 成本定义为:

也就是说,交易不是根据数据加计算来收费,而是根据它消耗的两种资源中哪一种资源更多来收费。这可以轻松扩展以覆盖更多维度(例如 ???(...,?3∗???????_??????) )。

应该很容易看出这如何在保证安全性的同时提高吞吐量。理论上一个区块中的最大数据量仍然是 GasLIMIT/?1,与一维 Gas 方案中完全相同。类似地,理论上的最大计算量是 GasLIMIT/?2 ,同样与一维 Gas 方案中完全相同。然而,任何消耗数据和计算的交易的 Gas 成本都会降低。

这大概是提议的 EIP-7623 中采用的方案,以减少最大区块大小,同时进一步增加 blob 计数。 EIP-7623 中的精确机制稍微复杂一些:它保持当前的 calldata 价格为每字节 16 Gas,但增加了每字节 48 Gas 的 floor price;交易支付 ( 16 * bytes + execution_Gas ) 和 ( 48 * bytes ) 中的较高者。因此,EIP-7623 将区块中理论最大交易调用数据从约 1.9 MB 减少到约 0.6 MB,同时保持大多数应用程序的成本不变。这种方法的好处是它与当前的一维 Gas 方案相比变化非常小,因此非常容易实现。

不过这种方法有两个缺点:

1、即使区块中的所有其他交易只使用很少的该资源,但大量占用一种资源的交易仍然会不必要地收取大量费用;

2、它激励数据密集型和计算密集型交易合并到一个捆绑包中以节省成本。

我认为,EIP-7623 这样的规则,无论是对于交易 calldata 还是其他资源,都可以带来足够大的好处,即使存在这些缺点,也是值得的。

然而,如果我们愿意投入(显着更高的)开发努力,就会出现一种更理想的方法。

多维 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 数量少于目标,则 excess_blobs 会减少。然后我们设置 blob_basefee = exp(excess_blobs / 25.47) ,其中 exp 是指数函数 ???(?)=2.71828^? 的近似值。

也就是说,每当 excess_blobs 增加约 25 时,blob 基本费用就会增加约 2.7 倍。如果 blob 变得太贵,平均使用量就会下降,并且 excess_blobs 开始减少,从而自动再次降低价格。 Blob 的价格不断调整,以确保平均而言,区块是半满的,也就是说,每个区块平均包含 3 个 Blob。

如果使用量出现短期峰值,则会出现限制:每个区块最多只能包含 6 个 blob,在这种情况下,交易可以通过提高优先费来相互竞争。然而,在正常情况下,每个 blob 只需支付 blob_basefee 加上少量的额外优先级费用作为被纳入的激励。

这种 Gas 定价在以太坊中已经存在多年:早在 2020 年,EIP-1559 就引入了非常相似的机制。通过 EIP-4844,我们为 Gas 和 Blobs 设置了两个独立的浮动价格。

注:2024 年 5 月 8 日一小时内的 Gas 基本费用,单位为 gwei。来源:ultrasound.money

原则上,我们可以为存储读取和其他类型的操作添加更多独立浮动的费用,不过我在下一节中将详细阐述一个需要注意的问题。

对于用户来说,这种体验与今天非常相似:你不再支付一笔基本费用(basefee),而是支付两项基本费用,但你的钱包可以将其从你的手中抽象出来,只向你显示可以预期支付的预期费用和最高费用。

对于区块构建者来说,大多数时候最佳策略与今天相同:包括任何有效的内容。大多数区块都未满——无论是 Gas 还是 Blob。一个具有挑战性的情况是,当有足够的 Gas 或足够的 Blob 超过区块限制时,构建者需要潜在地解决多维 knapsack 问题以最大化其利润。然而,即使存在相当好的近似算法,在这种情况下,通过制定专有算法来优化利润所获得的收益也比使用 MEV 进行相同操作所获得的收益要小得多。

对于开发人员来说,主要的挑战是需要重新设计 EVM 及其相关基础设施的功能,这些功能目前是基于单一价格和单一限制设计的,而现在需要将其改造成能够适应多个价格和多个限制的设计。

应用程序开发人员面临的一个问题是优化变得稍微困难:在某些情况下,您不能再明确地说 A 比 B 更高效,因为如果 A 使用更多的 calldata 而 B 使用更多的执行,那么当 calldata 为便宜,当 calldata 昂贵时则更昂贵。

应用开发者面临的一个问题是优化会变得稍微困难一些:在某些情况下,你无法明确地说 A 比 B 更有效率,因为如果 A 使用了更多的 calldata,而 B 使用了更多的执行,那么当 calldata 便宜时 A 可能更便宜,而当 calldata 昂贵时 A 可能更贵。

然而,开发者仍然可以通过基于长期历史平均价格进行优化,来获得相当不错的结果。

多维定价、EVM 和 sub-calls

有一个问题在 blobs 中没有出现,在 EIP-7623 或甚至是针对 calldata 的完整多维定价实现中也不会出现,但如果我们试图对状态访问或其他任何资源进行单独定价,那么这个问题就会出现:即子调用(sub-calls)中的 Gas 限制。

EVM 中的 Gas 限制存在于两个地方。首先,每笔交易都会设置一个 Gas 限制(Gas Limit),限制了该交易中可以使用的 Gas 总量。其次,当一个合约调用另一个合约时,该调用可以设置自己的 Gas 限制。这允许合约调用他们不信任的其他合约,并且仍然保证他们在调用后仍有剩余的 Gas 来执行其他计算。

注:账户抽象交易的踪迹,其中一个账户调用另一个账户,并且仅向被调用者提供有限数量的 Gas,以确保即使被调用者消耗了分配给它的全部 Gas,外部调用也可以继续运行。

挑战在于:让不同类型的执行之间实现多维 Gas 似乎需要子调用为每种类型的 Gas 提供多个限制,这将需要对 EVM 进行非常深入的更改,并且与现有应用程序不兼容。

这就是多维 Gas 提案通常停留在两个维度的原因之一:数据和执行。数据(无论是交易 calldata 还是 blob)仅在 EVM 外部分配,因此 EVM 内部无需更改任何内容即可使 calldata 或 blob 单独定价。

我们可以想出一个「EIP-7623 式的解决方案」来解决这个问题。这是一种简单的实现:在执行期间,对存储操作收取 4 倍的费用;为了简化分析,假设每个存储操作有 10000 气体。交易结束时,退款 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 主要通过退款来收回。

我们没有获得子调用的细粒度许可:子调用可能会消耗交易的所有 allowance 以进行廉价的存储操作。

但我们确实得到了一些足够好的东西,即进行子调用的合约可以设置一个限制,并确保一旦子调用执行完毕,主调用仍有足够的 Gas 进行所需的后处理(post-processing)。

我能想到的最简单的「完整的多维定价解决方案」是:我们将子调用 Gas 限制视为成比例的。也就是说,假设有 ? 种不同的执行类型,并且每个交易设置了多维限制 ?1...?? 。假设在当前执行点,剩余 Gas 为 ?1...?? 。假设调用 CALL 操作码,并使用子调用 Gas 限制 ? 。让 ?1=? ,然后 ?2=?1/?1∗?2 , ?3=?1/?1∗?3 ,以此类推。

也就是说,我们将第一种类型的 Gas(实际上是 VM 执行)视为一种特殊的「账户单位」,然后分配其他类型的 Gas,以便子调用在每种类型的 Gas 中都获得相同百分比的可用 Gas。这种方法有点难看(ugly),最大限度地保证了向后兼容性。

如果我们想在不牺牲向后兼容性的情况下,使该方案在不同类型的 Gas 之间更加「中立」,我们可以简单地将子调用的 Gas 限制参数表示为当前 context 中剩余 Gas 的一部分(例如,[1...63]/64)。

然而,无论在哪种情况下,都值得强调的是,一旦开始引入多维执行 Gas,固有的复杂性(ugliness)就会增加,这似乎很难避免。

因此,我们的任务是做出一个复杂的权衡:我们是否接受在 EVM 层面上的某种程度的复杂性(ugliness)增加,以安全地解锁显著的 L1 可扩展性增益,如果是的话,哪种具体提案对协议经济和应用程序开发者最有效?很有可能,我上面提到的两个方案都不是最好的,但仍有空间提出更优雅、更好的方案。

特别感谢 Ansgar Dietrichs、Barnabe Monnot 和 Davide Crapis 的反馈和审查。