本文讨论了 WAX 项目中实施的 4337 费用优化及其压缩方法,介绍了如何通过合理的数据压缩显著降低以太坊交易费用,包括以太坊转账和 ERC20 转账的具体费用对比。文章还探讨了当前费用环境和 BLS 签名的应用,并提出了优化用户操作的潜在策略。
注意: 本文档为 Dencun 之前的版本。Blob 的变化极大地影响了经济学,但压缩仍然对未来的可扩展性很重要。这里有另一篇关于此的文档。
基于 PSE 在 WAX 项目中实施和即将推出的方法,得到了 以太坊基金会 的支持。
我们在这里支持生态系统 - 所有内容都是免费和模块化的。你可以完全使用这些方法,也可以与自己的想法混合你喜欢的部分。源代码在 我们的代码库 中,我们可以通过 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 |
在使用 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 字节中:
userOp.callData
和 userOp.signature
,将在其他地方减少(见 账户压缩 和 BLS 签名聚合)userOp
字段上述编码的详细信息:
uint256
值进行紧凑打包,这然后使用 这个 solidity 库 进行解读c90c36
) 并不足以表示所有地址,但使用 RegIndex 可以为前 800 万个地址分配 3 字节 ID,为下一个十亿地址分配 4 字节 ID,依此类推(供参考,L1 上使用了 2.5 亿个地址)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,依此类推473dee
)不足以表示所有地址,但使用 RegIndex,我们可以为前 800 万个地址分配 3 字节 ID,为下一个十亿地址分配 4 字节 ID,依此类推(供参考,L1 上使用了 2.5 亿个地址)ECDSA 签名使用 65 字节。在对其他字段启用紧凑编码后,它通常成为用户操作所需字节的绝大多数。
BLS 签名更小,为 64 字节,并可用于验证来自不同方的无限交易。实际上,单个 BLS 签名的成本可以在所有用户操作之间共享。
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 |
注意:
data
字段的字节)l2Gas
特别容易玩弄,因 Arbitrum 报告 "gasUsed" 为使 fee = gasUsed * gasPrice
的各种数量。并不是说这是个坏主意,但这不是在这个模型中所表示的。其他 L2 可能在报告 gas 数字中也存在类似的复杂情况。在应用此模型时,在开发环境中独立测量 gas 是至关重要的。在 EntryPoint 压缩 中,我们看到了 113 字节用于编码一个包含一个 ETH 转账的捆绑。通过用 账户压缩 示例中的 6 个有效字节替换 userOp.callData
字段中的 33 个有效字节,我们将捆绑的大小减少到 86 (并执行 ERC20 转账)。
根据 WAX 原型的实验,这个捆绑的普通 gas (L2 gas) 估计为 174,000。
现在重要的是区分固有于捆绑的费用和由每个用户操作增加的费用。为了更有效成本,捆绑应该包含几个用户操作,越多越好。
有效字节 | L2 Gas | |
---|---|---|
固有于捆绑 | 2 | 34,000 |
由用户操作增加 | 84 | 140,000 |
我们现在可以用 fixedL1Gas
和 l1GasPerEffectiveDataByte
来结合计算 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。
捆绑大小是费用和延迟之间的权衡。你越希望费用更低,就越愿意为与更多用户共享费用而等待。这里有一个更大的博弈论故事,因为愿意支付更高费用的用户不会受到想要为捆绑开销支付最小份额用户的影响。这可能导致用户要么支付整个捆绑开销,要么不支付,愿意承担全部开销的用户便没有激励使用第三方捆绑者。看看这将如何发展将会很有趣。
总之,为了获得每个用户操作的总费用,捆绑者还应包含其运营的利润边际。下面包含的 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 成本极低。当这种局面反转时,压缩可能需要变更。
¹ 可能会奇怪 BLS 有每个用户操作的 gas 成本。这是因为 BLS 验证涉及处理的不仅仅是签名数据,还有相关的消息数据,每个用户操作都增加了消息数据。
² 在解释 Arbitrum gas 时需要小心。请参见 揭开 Arbitrum 的费用面纱。
- 原文链接: hackmd.io/@voltrevo/Bkz8...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!