WAX 中的 ERC4337 压缩

  • voltrevo
  • 发布于 2024-03-21 15:49
  • 阅读 54

本文讨论了 WAX 项目中实施的 4337 费用优化及其压缩方法,介绍了如何通过合理的数据压缩显著降低以太坊交易费用,包括以太坊转账和 ERC20 转账的具体费用对比。文章还探讨了当前费用环境和 BLS 签名的应用,并提出了优化用户操作的潜在策略。

WAX 4337 Compression

注意: 本文档为 Dencun 之前的版本。Blob 的变化极大地影响了经济学,但压缩仍然对未来的可扩展性很重要。这里有另一篇关于此的文档

基于 PSEWAX 项目中实施和即将推出的方法,得到了 以太坊基金会 的支持。

我们在这里支持生态系统 - 所有内容都是免费和模块化的。你可以完全使用这些方法,也可以与自己的想法混合你喜欢的部分。源代码在 我们的代码库 中,我们可以通过 discord 提供帮助。

(WAX 也涉及到 EIP 4337 所启用的奇妙功能,但本文档侧重于费用优化。)

摘要

简单的用户操作可以压缩到大约 18 字节

使用这些方法,捆绑者可以潜在地按下表中最右侧的列收费,同时实现盈利:

ETH 转账 EOA 4337 4337 压缩
主网 $ 1.4196 $ 6.5018 $12.1660
Arbitrum One $ 0.1359 $ 0.4868 $ 0.0868
Optimism $ 0.0935 $ 0.3131 $ 0.0302
ERC20 转账 EOA 4337 4337 压缩
主网 $ 2.7040 $ 7.1406 $13.1597
Arbitrum One $ 0.2054 $ 0.5316 $ 0.0918
Optimism $ 0.1155 $ 0.3412 $ 0.0312
  • 请参阅 当前费用环境整合
  • 我们对小数点后 4 位不够有信心,但通常的小数点后 2 位引入过多的舍入误差,小数点后 3 位对于货币而言容易被误解
  • 压缩的好处受到以下因素的较大影响:
    • blob 数据的价格 (4844) (上面的费用是预 4844)
    • 一起捆绑的用户操作数量(上述假设为 10 个操作/捆绑)

Google 表格

互动计算器

交易数据

在使用 4337 时,处理一个捆绑的完整调用数据仍然需要以常规的 ECDSA 签名以太坊交易开头。使用 1559 格式为:

0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s])

https://eips.ethereum.org/EIPS/eip-1559

在没有对协议进行更改(我们将在这里认为不在范围内)的情况下,我们只能压缩上述 data 字段。这使我们还有大约 110 字节需要由包含的用户操作支持。越多的用户操作 = 每个用户操作的共享成本越少。

EntryPoint 与账户压缩

压缩可以在两个独立的阶段进行:

  • EntryPoint:应用于 EntryPoint 的调用数据(即捆绑和受益人),由捆绑者压缩并由包装 EntryPoint 的合约解压缩
  • 账户:应用于 userOp.callData,由钱包(用户的链下软件)压缩并由用户的账户解压缩

EntryPoint 压缩

这种压缩引入一个包装合约(例如),该合约调用 EntryPoint。包装器接收压缩的调用数据,解压缩并调用 EntryPoint

今天可以使用相对简单的通用编码来使捆绑者受益,而无需让钱包参与或深入研究传递给每个钱包的调用数据的复杂性。

例如,发送一个单独的用户操作需要向 EntryPoint 发送如下字节:

1fad948c
    - handleOps(UserOperation[],address)

0000000000000000000000000000000000000000000000000000000000000040
    - 操作数组位于 0x40 (= 2x 32 字节)

000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
    - 受益人地址

0000000000000000000000000000000000000000000000000000000000000001
    - 数组长度(即有 1 个用户操作)

数组数据:
0000000000000000000000000000000000000000000000000000000000000020
    - 用户操作位于数组数据后的 0x20 字节

用户操作开始:
000000000000000000000000b734eb54c90c363d017b27641cc534caf7004fc4
    - 发送者地址(即用户的账户地址)

0000000000000000000000000000000000000000000000000000000000000001
    - 随机数 = 1

0000000000000000000000000000000000000000000000000000000000000160
    - initCode 位于用户操作开始后的 0x160 字节

0000000000000000000000000000000000000000000000000000000000000180
    - callData 位于用户操作开始后的 0x180 字节

000000000000000000000000000000000000000000000000000000000001228f
    - callDataGasLimit 为 74,383 (=0x01228f)

00000000000000000000000000000000000000000000000000000000000186a0
    - verificationGasLimit 为 100,000 (=0x186a0)

000000000000000000000000000000000000000000000000000000000000d494
    - preVerificationGas 为 54,420 (=0xd494)

000000000000000000000000000000000000000000000000000000003e08feb0
    - maxFeePerGas 为 1,040,776,880 (=0x3e08feb0, 大约 1.04 gwei)

000000000000000000000000000000000000000000000000000000003b9aca00
    - maxPriorityFeePerGas 为 1 gwei (=0x3b9aca00)

0000000000000000000000000000000000000000000000000000000000000220
    - paymasterAndData 位于用户操作开始后的 0x220 字节

0000000000000000000000000000000000000000000000000000000000000240
    - 签名位于用户操作开始后的 0x240 字节

0000000000000000000000000000000000000000000000000000000000000000
    - initCode 的长度为零

0000000000000000000000000000000000000000000000000000000000000064
    - callData 的长度为 0x64 (= 3x 32 + 4 字节)

2d1634c5
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000008
0103000001810000000000000000000000000000000000000000000000000000
    - callData

00000000000000000000000000000000000000000000000000000000
    - 填充(因为调用数据不是 32 的倍数)

0000000000000000000000000000000000000000000000000000000000000000
    - paymasterAndData 的长度为零

0000000000000000000000000000000000000000000000000000000000000041
    - 签名的长度为 65 (=0x41)

6d0d86052da1995cb95f7d51fe68375c182e82822263a38a4976251e6d4a6918
162b605c71bea200f55e314cdea53112ab61a440fa1a0d260e119ebe417780be
1c
    - 签名

00000000000000000000000000000000000000000000000000000000000000
    - 填充(因为签名不是 32 的倍数)

这些字节是必要的,因为它符合 Solidity ABI 确定的布局。我们不再发送它们,而是让另一个合约发送这些字节。该合约可以解码下面的字节并将等效的 Solidity ABI 编码传递给该合约。通过在交易内产生额外数据,它不需要发布到 L1,因此我们可以避免为它付费。

01
    - 1 个用户操作 (VLQ)

04
    - 位栈编码 (1)00 (0x04 == 0b100)
        - 首先读取最不重要的位:
        - 0: initCode 为空
        - 0: paymasterAndData 为空
        - 1: 位栈结束

c90c36
    - 发送者 (使用包含的查找表:
        c90c36 => b734eb54c90c363d017b27641cc534caf7004fc4)

01
    - 随机数 = 1

(无字节)
    - initCode 为空,但我们不需要任何字节存储它
        因为位栈已经表示它为空

64
    - callData 的长度为 100 (VLQ)
2d1634c5
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000008
0103000001810000000000000000000000000000000000000000000000000000
    - callData

185d
    - callDataGasLimit 为 74,400 (PseudoFloat)

3100
    - verificationGasLimit 为 100,000 (PseudoFloat)

1944
    - preVerificationGas 为 54,500 (PseudoFloat)

410d
    - maxFeePerGas 为 1.05 gwei (PseudoFloat)

3900
    - maxPriorityFeePerGas 为 1 gwei (PseudoFloat)

(无字节)
    - paymasterAndData 为空,但我们不需要任何字节存储它
        因为位栈已经表示它为空

41
    - 签名的长度为 65 字节 (VLQ)
6d0d86052da1995cb95f7d51fe68375c182e82822263a38a4976251e6d4a6918
162b605c71bea200f55e314cdea53112ab61a440fa1a0d260e119ebe417780be
1c
    - 签名

这样可以将捆绑的有效字节从 319 减少到 113。

在这 113 字节中:

  • 2 字节是捆绑的固有部分
  • 100 字节用于 userOp.callDatauserOp.signature,将在其他地方减少(见 账户压缩BLS 签名聚合
  • 11 字节用于剩余的 userOp 字段

上述编码的详细信息:

  • 受益人地址根本不出现,因为它可以存储在包装合约中,而无需为每个捆绑提供相同的值
  • 位栈使用 VLQuint256 值进行紧凑打包,这然后使用 这个 solidity 库 进行解读
  • 3 字节 (c90c36) 并不足以表示所有地址,但使用 RegIndex 可以为前 800 万个地址分配 3 字节 ID,为下一个十亿地址分配 4 字节 ID,依此类推(供参考,L1 上使用了 2.5 亿个地址)
  • PseudoFloat 使用 2 字节表示大多数最大精度为 3 位小数的量,并根据需要扩展以支持所有 uint256 值(无损压缩)
  • 使用 PseudoFloat 的 5 个字段(如 callGasLimit)对小误差是容忍的,并需要舍入到 3 位有效数字以充分利用该格式。这需要用户钱包软件的合作,因为舍入会改变 userOpHash,从而改变所需的签名。如果不舍入,将使用额外的 5-10 字节。

EntryPoint 压缩还可以通过压缩发送给钱包的调用数据进一步减少,从而可能消除对账户压缩的需求。如果试图支持各种不同的竞争智能账户的不断发展的调用数据格式,这可能具有挑战性,但如果捆绑者和智能账户由同一组织提供,则可能更方便。

账户压缩

此压缩应用于 userOp.callData 字段,该字段决定了 EntryPoint 发送到账户的字节。解压将在账户内部发生,以确定要执行的操作。

这使得账户可以受益于压缩,而无需依赖专用的捆绑者,并且可以随意更换捆绑者而不会丧失其压缩特性。

在这个层次上使用压缩减少了捆绑者的实现复杂性,但账户压缩无法帮助每个用户操作的许多其他字段,因此始终建议某种 EntryPoint 压缩。

例如,没有压缩的账户可能使用如下方法:

function execute(
    address dest,
    uint256 value,
    bytes calldata func
) external {
  // ...
}

(该方法来自 eth-infinitism 的 SimpleAccount 示例。)

要以这种方式发送 ERC20 代币,我们的 userOp.calldata 字段将编码如下:

b61d27f6
    - execute(address,uint256,bytes)

000000000000000000000000c845d6b81d6d1f3b45f2353fec8c960085a9a42e
    - dest / ERC20 地址

0000000000000000000000000000000000000000000000000000000000000000
    - value(因为我们不发送ETH,所以为零)

0000000000000000000000000000000000000000000000000000000000000060
    - func 的位置 (0x60 = 3x 32 字节)

0000000000000000000000000000000000000000000000000000000000000024
    - func 的长度 (0x24 = 2x 32 + 4 字节)

a9059cbb
    - transfer(address,uint256) 的 4 字节

000000000000000000000000e30a735c9b90549f8171f17dd698ab6048dde5ab
    - 接收者地址

0000000000000000000000000000000000000000000000000de0b6b3a7640000
    - 一个代币 (10 ^ 18)

00000000000000000000000000000000000000000000000000000000
    - 填充,以便如果有下一个字段的
      execute(address,uint256,bytes),可以下一个开始并在 32 字节对齐边界上

这是当我们使用 Solidity ABI 时得到的编码。它被设计为高效计算以适应 256 位 EVM,但这对于字节来说效率非常低。

总共,这使用了 228 字节。零是更便宜的(根据 L2 大约便宜 75%),因此可以更有效地将其视为 98 有效字节(即具有相同成本的非零字节的等效数量)。

具有压缩的账户可以通过接收以下调用数据来实现相同的事情:

02
    - 要使用的压缩方案 (02 = ERC20 转账)

6a
    - 代币(使用查找表:
        6a => c845d6b81d6d1f3b45f2353fec8c960085a9a42e)

473dee
    - 收件人(使用查找表:
        473dee => e30a735c9b90549f8171f17dd698ab6048dde5ab)

9900
    - 数量 = 10^18,编码为 PseudoFloat

这将 userOp.calldata 的有效字节从 98 减少到 6。

上述编码的详细信息:

  • 虽然单个字节(6a)并不足以表示所有 ERC20 代币,使用 VLQ,我们可以为受欢迎的代币使用单字节 ID,为下一个最受欢迎的 16384 个代币使用两字节 ID,依此类推
  • 同样,3 字节(473dee)不足以表示所有地址,但使用 RegIndex,我们可以为前 800 万个地址分配 3 字节 ID,为下一个十亿地址分配 4 字节 ID,依此类推(供参考,L1 上使用了 2.5 亿个地址)
  • PseudoFloat 使用 2 字节表示大多数最大精度为 3 位小数的量,并根据需要扩展以支持所有 uint256 值(无损压缩)

BLS 签名聚合

ECDSA 签名使用 65 字节。在对其他字段启用紧凑编码后,它通常成为用户操作所需字节的绝大多数。

BLS 签名更小,为 64 字节,并可用于验证来自不同方的无限交易。实际上,单个 BLS 签名的成本可以在所有用户操作之间共享。

Screen Shot 2024-01-11 at 13.28.23

BLS 签名还伴随更高的 L2 gas 成本:

ECDSA BLS
固有于捆绑 0 90,000
由用户操作增加 3,000 36,000 ¹

注意: 这一切都是基于 BN254 曲线上的 BLS。BLS 也可以在 BLS12-381 曲线(在信标链中使用)上实现,但这需要等新的预编译出现以使其适用 EVM。

当前费用环境

参数 说明
ETH $2,600 最新(写作时)
ETH gas 26 gwei 上个月的中位数
Arbitrum gas 0.1 gwei 上个月的中位数
Optimism gas 0.0054 gwei 上个月的中位数

L2 链收取费用的确切细节因链而异。例如,Arbitrum 增加了其 gas 值以便为它需要支付的 L1 gas 进行账单处理,但 Optimism 将 L1 gas 的费用单独计费(另外加上 gasPrice \* gasUsed)。

为了我们的目的,可以通过找到以下统一模型的正确参数来准确预测 L2 收费:

l1GasUsed = fixedL1Gas + l1GasPerEffectiveDataByte \* dataBytes

l2GasUsed = (由协议定义的普通 gas / L1 和本地开发相同)

fee = l1GasUsed \ l1GasPrice + l2GasUsed \ l2GasPrice

fixedL1Gas 是对所有交易收取的最小 L1 gas。

l1GasPerEffectiveDataByte 是逐个收费的 L1 gas 对应于 data 字段中的 有效字节(即传送到目标地址的字节)。这里的 "有效" 有点不清楚,但不可压缩的数据,对于我们使用的传统来说是一个不错的近似(因为我们已经进行了压缩),应该是 100% “有效”。换句话说,我们在数据字段中输入的每一个字节都算作一个“有效”字节。

虽然理论上可以通过细读每个 L2 的细节来找到这些参数,但用真实交易直接测量它们更简单且不易出错。这可以通过 WAX 收费测量工具 来完成。

撰写时,这种方法产出如下值:

参数 Arbitrum One Optimism
fixedL1Gas 1816 1302
l1GasPerEffectiveDataByte 16.2 11.0

注意

  • L2 链内部定义了类似的参数以供它们的实际计算,但它们与这里使用的参数不同(例如,计算总交易字节而不是 data 字段的字节)
  • l2Gas 特别容易玩弄,因 Arbitrum 报告 "gasUsed" 为使 fee = gasUsed * gasPrice 的各种数量。并不是说这是个坏主意,但这不是在这个模型中所表示的。其他 L2 可能在报告 gas 数字中也存在类似的复杂情况。在应用此模型时,在开发环境中独立测量 gas 是至关重要的。
  • 当 L2 链为 L1 gas 收费时,它们使用其内部 L1 gas 价格的视图。这与实际的 L1 gas 价格密切相关,但不完全相同,并且受 L2 的控制。

整合

EntryPoint 压缩 中,我们看到了 113 字节用于编码一个包含一个 ETH 转账的捆绑。通过用 账户压缩 示例中的 6 个有效字节替换 userOp.callData 字段中的 33 个有效字节,我们将捆绑的大小减少到 86 (并执行 ERC20 转账)。

根据 WAX 原型的实验,这个捆绑的普通 gas (L2 gas) 估计为 174,000。

现在重要的是区分固有于捆绑的费用和由每个用户操作增加的费用。为了更有效成本,捆绑应该包含几个用户操作,越多越好。

有效字节 L2 Gas
固有于捆绑 2 34,000
由用户操作增加 84 140,000

我们现在可以用 fixedL1Gasl1GasPerEffectiveDataByte 来结合计算 Arbitrum 和 Optimism 的捆绑和用户操作的 L1 gas 值。fixedL1Gas 固有于捆绑,因为我们只需每个捆绑支付一次。

Arbitrum One L1 Gas ² L2 Gas ²
固有于捆绑 2,885 124,000
由用户操作增加 292 173,000
Optimism L1 Gas L2 Gas
固有于捆绑 2,028 124,000
由用户操作增加 198 173,000

此时,让我们为假设捆绑中的用户操作数量选择 10。这是在展示低成本潜力和尽量接近规模之间的平衡。如果你能达到 30 ops/bundle,那么便会下降到大约 1.3x。

L1 Gas 与捆绑大小

捆绑大小是费用和延迟之间的权衡。你越希望费用更低,就越愿意为与更多用户共享费用而等待。这里有一个更大的博弈论故事,因为愿意支付更高费用的用户不会受到想要为捆绑开销支付最小份额用户的影响。这可能导致用户要么支付整个捆绑开销,要么不支付,愿意承担全部开销的用户便没有激励使用第三方捆绑者。看看这将如何发展将会很有趣。

总之,为了获得每个用户操作的总费用,捆绑者还应包含其运营的利润边际。下面包含的 5% 已经算在内。

ERC20 转账费用 L1 Gas ² L2 Gas ²
Arbitrum One 609 194,670
Optimism 421 194,670

我们可以将这些与 当前费用环境 中真实的 gas 价格和 ETH 的价值结合来获得总费用(单位为美元):

ERC20 转账费用 L1 基于 L2 基于 总计
Arbitrum One $0.0412 $0.0506 $0.0918
Optimism $0.0284 $0.0027 $0.0312

最后,blob 即将到来。4844 应该显著降低上述基于 L1 的费用。确切的减少量很难预测,但 20 倍是我听说的折扣中较低的,如果更多的折扣不会对上述总计产生太大影响。所以,假设新费用为 20 倍:

ERC20 转账费用 (4844) L1 基于 L2 基于 总计
Arbitrum One $0.0021 $0.0506 $0.0527
Optimism $0.0014 $0.0027 $0.0042

在这个费用由 L2 而不是发布到 L1 的成本主导的场景中,重新评估我们的压缩选择便十分重要,目前,压缩很容易盈利,因为所有压缩工作发生在 L2,且 L2 成本极低。当这种局面反转时,压缩可能需要变更。

脚注 1

¹ 可能会奇怪 BLS 有每个用户操作的 gas 成本。这是因为 BLS 验证涉及处理的不仅仅是签名数据,还有相关的消息数据,每个用户操作都增加了消息数据。

脚注 2

² 在解释 Arbitrum gas 时需要小心。请参见 揭开 Arbitrum 的费用面纱

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

0 条评论

请先 登录 后评论
voltrevo
voltrevo
江湖只有他的大名,没有他的介绍。