这篇文章上次修改于 376 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
如何让链实现 gasless,godwoken 提供了一个很好的方案,使用了简化的 ERC-4337,包括去掉了账户抽象,去掉了 bunlder,仅用到了 entrypoint 合约,并实现了 paymaster 合约。
其中,paymaster 合约会表明什么情况下愿意为交易付手续费,并 deposit 一笔钱到 entrypoint 合约,交易的手续费就从这笔钱中扣,扣除的手续费会给到 block producer。用户只需要发送 0 gas price 交易就可以了,这就需要对节点代码进行修改,能够接受 0 gas price 交易。
不过,这个方案有个缺点,就是手续费只能转给指定的 block producer,block producer 的地址在部署 entrypoint 合约的时候指定,但对于 L2 一般也只有一个 block producer,这个方案也足够了。另外,让节点接收 0 gas price 的方式增大了 DDoS 的风险,因为没有钱的账户发送的交易也能到达 tx queue
Arbtrum Nitro 实现 gasless 的思路也可以参考 godwoken 的方案,这要求链能够接受 0 gas price 交易,所以要对 Arbtrum Nitro 进行一些改造。
gasless 交易:只要 tx.to 是 entrypoint 地址的,就是 gasless 交易。tx.data 是 UserOperation
下面主要讲述如何修改 Arbtrum Nitro,使其能接受 gasless 交易。
避免手动在 Metamask 设置 gas price 为 0
用户发交易的时候一般需要用 Metamask 进行签名,为了避免用户每次手动在 Metamask 将 gas price 调成 0,在链的内部将 gasless 交易的 gas price 置为 0。
会发现 go-ethererum 的 TransactionToMessage() 中会重置 gas price
// If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil {
msg.GasPrice = cmath.BigMin(msg.GasPrice.Add(msg.GasTipCap, baseFee), msg.GasFeeCap)
}
所以需要紧接着添加如下设置
if arbutil.IsGaslessTx(tx) {
msg.GasPrice = common.Big0
}
另外,当用户没有 ETH 时,Metamask 在调用 eth_estimateGas 后会报错余额不足,为了避免这种情况,还需要修改 go-ethereum 中的 DoEstimateGas(),在开头添加
if args.To != nil && *args.To == arbutil.ENTRYPOINT_CONTRACT {
args.GasPrice = (*hexutil.Big)(common.Big0)
}
避免交易的 gas price 为 0 时被拒收
接下来就是让链能够接受 gasless 交易。交易进入的链的时候,会对 gas price 进行一些检查,比如 nitro 的 PreCheckTx()
if arbmath.BigLessThan(tx.GasFeeCap(), baseFee) {
return fmt.Errorf("PreCheckTx() %w: address %v, maxFeePerGas: %s baseFee: %s", core.ErrFeeCapTooLow, sender, tx.GasFeeCap(), header.BaseFee)
}
go-ethereum 的 preCheck()
if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 {
return fmt.Errorf("%w: address %v, maxFeePerGas: %s baseFee: %s", ErrFeeCapTooLow,
msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee)
}
需要改为当交易是 gasless 交易时,不进入如上 check
避免因为 gas price 为 0 导致收取的 gas fee 错误
可以确认 go-ethereum 中在从用户账户中扣费时,如果 gas price 为 0 时,buyGas() 中扣费为 0,在扣多了再返回给用户时,refundGas() 中返回的也是 0
虽然扣费为 0,但是收费却未必是 0
go-ethereum 的 TransitionDb() 中,会把 effectiveTip * st.gasUsed() 转给 networkAccount,但如果 rules.IsLondon 为 true,gas price 为 0 时,effectiveTip 是负数
effectiveTip := msg.GasPrice
if rules.IsLondon {
effectiveTip = cmath.BigMin(msg.GasTipCap, new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee))
}
因为用户发的交易是 LegacyTx,而对于LegacyTx 来说,GasFeeCap 就是 GasPrice:
func (tx *LegacyTx) gasPrice() *big.Int { return tx.GasPrice }
func (tx *LegacyTx) gasTipCap() *big.Int { return tx.GasPrice }
func (tx *LegacyTx) gasFeeCap() *big.Int { return tx.GasPrice }
所以 msg.GasFeeCap - st.evm.Context.BaseFee = -st.evm.Context.BaseFee
,所以 effectiveTip 是负数,会导致 networkAccount 余额减少。所以对于 gasless 交易,应该将 effectiveTip 值改为 0
在 nitro 的 EndTxHook() 中,也会有收手续费逻辑
totalCost := arbmath.BigMul(basefee, arbmath.UintToBig(gasUsed)) // total cost = price of gas * gas burnt
computeCost := arbmath.BigSub(totalCost, p.PosterFee) // total cost = network's compute + poster's L1 costs
...
util.MintBalance(&infraFeeAccount, infraComputeCost, p.evm, scenario, purpose)
...
util.MintBalance(&networkFeeAccount, computeCost, p.evm, scenario, purpose)
...
util.MintBalance(&posterFeeDestination, p.PosterFee, p.evm, scenario, purpose)
可以看到,totalCost 的计算与 gas price 无关,只与 basefee 有关,所以结果不为 0。相当于没有从用户账户扣费,而收费账户凭空多了一笔钱,这肯定是不行的,所以对于 gasless 交易应跳过上述 util.MintBalance
部分。
避免因为修改了 State Transition Function 导致停链
与收费相关的代码都位于 TransitionDb() 中,是 State Transition Function,而 nitro 白皮书中有如下描述:
One of the challenges in designing a practical rollup system is the tension between wanting the system to perform well in ordinary execution, versus being able to reliably prove the results of execution. Nitro resolves this by using the same source code for both execution and proving, but compiling it to different targets for the two cases.
When compiling the Nitro node software for execution, the ordinary Go compiler is used, producing native
code for the target architecture, which of course will be different for different node deployments. (The node software is distributed in source code form, and as a Docker image containing a compiled binary.)
Separately, the portion of the code that is the State Transition Function is compiled by the Go compiler to WebAssembly (wasm), which is a typed, portable machine code format. The wasm code then goes through a simple transformation into a format we call WAVM, which is detailed below. If there is a dispute about the correct result of computing the STF, it is resolved by an interactive fraud proof protocol (described in Section 5) with reference to the WAVM code.
也就是说 TransitionDb() 会被编成两份 target,一份是 bin,一份是 wasm,wasm 会被用于欺诈证明。
但 nitro 的 Dockerfile 中并没有用新编译出的 relpay.wasm,而是下载了旧的:
RUN ./download-machine.sh consensus-v10.2 0x0754e09320c381566cc0449904c377a52bd34a6b9404432e80afd573b67f7b17
这会导致停链:
shutting down due to fatal error err="validation failed: expected {0x390f48d872f09802373c98e5b1495a5b71efabb357768f0a9daf52dfcc675f59 0x0000000000000000000000000000000000000000000000000000000000000000 3 0} got {0x324a490b05bd4f6a58cf838fd445fc64635669350457075fbc896873a4b87129 0x0000000000000000000000000000000000000000000000000000000000000000 3 0}"
所以需要修改 Dockerfile,改为使用新编译出的 relpay.wasm
# RUN ./download-machine.sh consensus-v10.2 0x0754e09320c381566cc0449904c377a52bd34a6b9404432e80afd573b67f7b17
COPY --from=module-root-calc /workspace/target/machines/latest/replay.wasm ./0x0754e09320c381566cc0449904c377a52bd34a6b9404432e80afd573b67f7b17/replay.wasm
RUN ln -sfT 0x0754e09320c381566cc0449904c377a52bd34a6b9404432e80afd573b67f7b17 latest
这样,链就可以接受 0 gas price 交易了。
最后提一下,Arbtrum Nitro 的 Sequencer 对交易的排序采用先到先处理方式,也就是说并不会因为 gas price 比较高而被优先处理,所以交易的排序逻辑不需要修改。
没有评论