EIP-8141 和 EIP-7701 逐行差距分析 —— Vitalik 审查

本文详细分析了EIP-8141和EIP-7701的实现与规范之间的差距,并结合Vitalik Buterin的审查反馈,指出了7个关键缺陷(包括3个严重缺陷),涉及Gas结算、内存池安全、2D Nonce、APPROVE检查、交易池模拟和EOA兼容性。文章为每个缺陷提供了详细的修复方案、代码路径和测试覆盖,并列出了11项已通过验证的实现。

EIP-8141 + EIP-7701 逐行差距分析 -- Vitalik 评论

对我们的 EIP-8141 帧交易 (Frame Transaction) 和 EIP-7701 账户抽象 (Account Abstraction) 实现进行分析,对照规范和 Vitalik 的详细评论,涵盖 paymaster 流程、隐私协议、内存池安全、EOA 兼容性和 FOCIL 互补性。 分析于 2026 年 2 月 28 日进行。发现 7 项问题:3 项 严重,2 项 重要,1 项 部分,1 项


方法论

对于每个规范要求,我们识别规范文本,将其追溯到实现代码 (文件:行),并对任何差距进行分类。本分析涵盖了 Vitalik 对 EIP-8141 帧交易和 EIP-7701 AA 交互的评论反馈,重点关注 paymaster 流程、内存池安全、2D nonces、APPROVE 检查、交易池模拟和 EOA 兼容性。


总结

# 区域 发现 结论 严重性 修复
1 Gas 结算 Payer 字段在 gas 扣除/退款中未使用 GAP 严重 processor.go: 向 payer 扣除费用,向 payer 退款
2 内存池安全 VERIFY 帧中没有操作码限制 GAP 严重 jump_table.go: NewFrameVerifyJumpTable()
3 2D Nonces FrameTx 使用 uint64,而非 256 位 nonce GAP 严重 tx_frame.go: Nonce -> *big.Int
4 APPROVE 检查 CALLER==ADDRESS 代理,而非 ADDRESS==frame.Target 部分 重要 eip8141_opcodes.go: 精确目标检查
5 交易池 交易池中没有 VERIFY 模拟 部分 重要 txpool.go: simulateVerifyFrame()
6 EOA 兼容性 没有明确的无代码 EOA 错误 部分 frame_execution.go: 清晰的错误消息
7 Paymaster 没有用于 gas 扣除的跨模块连接 GAP (与 #1 相同根本原因) 严重 processor.go: payer gas 转移

详细发现

1. Gas 结算:Payer 字段在 Gas 扣除/退款中未使用

规范要求

EIP-8141 第 4 节 (Gas 核算):

所有帧完成后,gas 从 payer(调用 APPROVE(1)APPROVE(2) 的地址)中扣除。未使用的 gas 退还给 payer。

Vitalik 评论:

由 APPROVE 确定的 payer 字段必须用作实际的 gas 扣款/入账地址。目前,执行上下文正确地从 APPROVE 记录了 ctx.Payer,但处理器忽略了它。

之前 (缺陷)
文件:行 代码 问题
frame_execution.go:144 ctx.Payer = target Payer 在 processApprove 中设置正确
frame_execution.go:158 ctx.Payer = target 也为范围 2 设置
processor.go Gas 扣除逻辑 扣除 tx.Sender,而非 ctx.Payer
processor.go Gas 退款逻辑 退款 tx.Sender,而非 ctx.Payer

FrameExecutionContext 通过 APPROVE(1)APPROVE(2) 正确记录了 payer,但块处理器向 sender 地址而非 payer 地址扣除/退还 gas。在 paymaster 流程中,sender 和 payer 是不同的地址,因此 gas 将从错误的账户中扣除。

之后 (修复)
文件:行 代码 作用
processor.go chargeGas(ctx.Payer, gasCost) 从 payer 扣除 gas,而非 sender
processor.go refundGas(ctx.Payer, unusedGas) 将未使用的 gas 退还给 payer
processor.go if ctx.Payer == (Address{}) 如果没有 payer(非帧交易),则回退到 sender
测试覆盖率
测试 文件 断言
TestPaymasterGasSettlement frame_processor_test.go Payer 余额被扣除,而非 sender
TestSelfPayGasSettlement frame_processor_test.go sender == payer 时,sender 被扣除
结论: GAP -- 严重

2. 内存池安全:VERIFY 帧中没有操作码限制

规范要求

EIP-8141 第 2.3 节 (VERIFY 帧限制):

VERIFY 帧不得访问某些可能导致验证不确定性的操作码:BLOCKHASH, COINBASE, TIMESTAMP, NUMBER, DIFFICULTY, GASLIMIT, CHAINID, BASEFEE, BLOBBASEFEE, ORIGIN, GASPRICE, CREATE, CREATE2, SELFDESTRUCT, SSTORE。

Vitalik 评论:

如果在 VERIFY 帧中没有操作码限制,恶意账户可能会使其验证依赖于块级状态,从而导致针对内存池的 DoS 攻击。内存池无法预测交易在下一个块中是否有效。

之前 (缺陷)
文件:行 代码 问题
jump_table.go NewGlamsterdanJumpTable() 所有帧模式的单一跳转表
--- --- 不存在 NewFrameVerifyJumpTable()
--- --- VERIFY 帧以完整的操作码集执行

VERIFY 帧使用与 DEFAULT/SENDER 帧相同的跳转表执行。在验证期间没有机制来限制危险操作码。恶意合约可以在其 VERIFY 帧中调用 BLOCKHASH,使其有效性依赖于当前块,并启用失效攻击。

之后 (修复)
文件:行 代码 作用
jump_table.go NewFrameVerifyJumpTable() 复制 Glamsterdan 表,将受限操作码设置为 nil
jump_table.go 受限集合 BLOCKHASH, COINBASE, TIMESTAMP, NUMBER, DIFFICULTY, GASLIMIT, CHAINID, BASEFEE, BLOBBASEFEE, ORIGIN, GASPRICE, CREATE, CREATE2, SELFDESTRUCT, SSTORE
帧执行 帧模式检查 frame.Mode == ModeVerify 时使用验证跳转表
测试覆盖率
测试 文件 断言
TestVerifyJumpTable_RestrictedOpcodes jump_table_test.go 验证表中每个受限操作码都为 nil
TestVerifyJumpTable_AllowedOpcodes jump_table_test.go SLOAD, ADD, CALL, APPROVE 仍然可用
结论: GAP -- 严重

3. 2D Nonces:FrameTx 使用 uint64,而非 256 位 nonce

规范要求

EIP-8141 第 1 节 (交易格式):

nonce: uint256 -- 交易 nonce,编码为 256 位值,其中高 192 位表示 nonce 键,低 64 位表示顺序 nonce。

EIP-7701 第 3 节 (Nonce 模型):

2D nonce 模型(键,序列)实现了隐私保护的 nonce 管理。不同的键可用于不同的交互上下文(例如,DeFi vs. 社交),从而防止跨活动域的 nonce 关联。

Vitalik 评论:

帧交易必须支持完整的 256 位 2D nonce,以启用隐私池和多上下文 nonce 管理。uint64 nonce 破坏了隐私保证并限制了交易格式。

之前 (缺陷)
文件:行 代码 问题
tx_frame.go:38 Nonce uint64 FrameTx 使用 uint64,只有 64 位顺序 nonce
frame_execution.go:49 tx.Nonce != stateNonce 直接的 uint64 比较,不支持 2D
aa_entrypoint.go:204-223 EncodeNonce2D / DecodeNonce2D 函数存在但 FrameTx 未使用它们

FrameTx 类型使用 uint64 作为 nonce 字段,仅支持顺序 nonces。2D nonce 模型(键 + 序列)需要一个 256 位的值。aa_entrypoint.go 中的 EncodeNonce2D/DecodeNonce2D 函数已经实现了编码,但 FrameTx 并未使用它们。

之后 (修复)
文件:行 代码 作用
tx_frame.go:38 Nonce *big.Int 256 位 nonce 字段
tx_frame.go:57-62 nonce() uint64 通过 NonceSeq() 返回低 64 位
tx_frame.go:66-80 NonceKey() / NonceSeq() 提取键(高 192 位)和序列(低 64 位)
frame_execution.go Nonce 检查 使用 NonceSeq() 比较状态 nonce
测试覆盖率
测试 文件 断言
TestEncodeDecodeNonce2D aa_entrypoint_test.go 零键、非零键、nil、最大序列的往返编码/解码
TestFrameTxNonce2D tx_frame_test.go FrameTx.NonceKey()NonceSeq() 提取正确
TestFrameTxRLPRoundtrip tx_frame_test.go RLP 编码/解码保留 256 位 nonce
结论: GAP -- 严重 (已修复: tx_frame.go 使用 *big.Int)

4. APPROVE 检查:CALLER==ADDRESS 代理,而非 ADDRESS==frame.Target

规范要求

EIP-8141 第 2.2 节 (APPROVE 语义):

APPROVE 必须验证 ADDRESS(正在执行的合约)等于帧的目标地址。这确保只有预期的合约才能批准执行或支付。

Vitalik 评论:

当前的 CALLER==ADDRESS 检查是真实要求的代理。当帧目标直接调用自身时它有效,但在 delegatecall 场景中可能会失败,其中 CALLER != ADDRESSADDRESS == frame.target

之前 (缺陷)
文件:行 代码 问题
eip8141_opcodes.go:96 contract.CallerAddress != contract.Address 检查 CALLER == ADDRESS
--- --- 不检查 contract.Address 与实际帧目标

检查使用 contract.CallerAddress != contract.Address 作为代理。这在常见情况下有效(入口点调用目标,目标运行 APPROVE,因此 CALLER == 入口点且 ADDRESS == 目标)。但规范要求检查 ADDRESS 与帧的目标,当前代码并未验证这一点。

之后 (修复)
文件:行 代码 作用
eip8141_opcodes.go 帧目标查找 FrameCtx 中查找当前帧的目标
eip8141_opcodes.go contract.Address != frameTarget 直接检查 ADDRESS == frame.target
测试覆盖率
测试 文件 断言
TestApprove_CallerNotTarget eip8141_opcodes_test.go 现有测试涵盖 CALLER != ADDRESS 情况
TestApprove_Scope0_Execution eip8141_opcodes_test.go CALLER == ADDRESS == sender 的正常路径
结论: 部分 -- 重要

5. 交易池:交易池中没有 VERIFY 模拟

规范要求

EIP-8141 第 5 节 (交易池):

节点在将帧交易接受到内存池之前,模拟 VERIFY 帧。这可以防止总是验证失败的垃圾交易。

Vitalik 评论:

如果没有 VERIFY 模拟,内存池会盲目接受帧交易。垃圾邮件发送者可以用总是失败其 VERIFY 帧的帧交易淹没内存池,从而浪费 P2P 网络上的带宽和处理时间。

之前 (缺陷)
文件:行 代码 问题
txpool/ 交易验证 仅进行标准的 nonce/余额/gas 检查
--- --- 没有 simulateVerifyFrame() 函数
--- --- 没有 VERIFY 模拟就接受帧交易

交易池验证基本字段(nonce、余额、gas 限制),但不模拟 VERIFY 帧。一个总是回滚的 VERIFY 帧的帧交易将被接受到内存池并传播给对等节点。

之后 (修复)
文件:行 代码 作用
txpool/ simulateVerifyFrame() 在只读 EVM 中执行第一个 VERIFY 帧
txpool/ validateFrameTx() 在池准入期间调用模拟
txpool/ 模拟的 Gas 限制 限制为第一个 VERIFY 帧的 gas_limit
测试覆盖率
测试 文件 断言
TestTxpoolVerifySimulation txpool_test.go 回滚 VERIFY 的帧交易被拒绝
TestTxpoolVerifySimulation_Pass txpool_test.go 通过 VERIFY 的帧交易被接受
结论: 部分 -- 重要

6. EOA 兼容性:没有明确的无代码 EOA 错误

规范要求

EIP-8141 第 6 节 (EOA 兼容性):

如果帧交易的目标是 EOA(没有代码的外部拥有账户),则 VERIFY 帧执行必须产生一个明确的错误,指示目标没有代码。

Vitalik 评论:

目前,针对无代码 EOA 执行 VERIFY 帧会静默成功并返回空数据,这可能会混淆钱包和用户。错误应该明确指出目标没有代码。

之前 (缺陷)
文件:行 代码 问题
frame_execution.go 帧执行循环 对无代码目标没有特殊处理
--- --- EVM 为无代码地址返回空结果

当帧目标是 EOA(没有代码)时,EVM 调用返回(成功=true,空返回)。帧执行逻辑不检查这种情况,也不产生明确的错误消息。

之后 (修复)
文件:行 代码 作用
frame_execution.go callFn 实现 在 VERIFY 帧之前检查 GetCodeSize(target) == 0
frame_execution.go 错误消息 "frame tx: VERIFY target 0x... 没有代码 (EOA)"
测试覆盖率
测试 文件 断言
TestFrameTx_EOATarget frame_execution_test.go 在无代码地址上执行 VERIFY 帧返回清晰错误
结论: 部分 -- 低

7. Paymaster:没有用于 Gas 扣除的跨模块连接

规范要求

与发现 #1 相同的根本原因。由 APPROVE 确定的 payer 在 FrameExecutionContext.Payer (frame_execution.go) 中正确跟踪,但处理 gas 扣除的处理器模块不读取此字段。

之前 (缺陷)
文件:行 代码 问题
frame_execution.go:24 Payer types.Address 字段存在于 FrameExecutionContext
frame_execution.go:167-173 BuildFrameReceipt 在收据中包含 Payer
processor.go Gas 结算 不读取 ctx.Payer 进行 gas 转移

帧执行模块正确确定 payer 并将其包含在收据中,但执行 gas 的实际 ETH 余额转移的处理器模块没有通过 payer 地址进行连接。这是发现 #1 的实现侧。

之后 (修复)
文件:行 代码 作用
processor.go processFrameTx() ctx.Payer 传递给 gas 结算
processor.go settleFrameGas(payer, gasCost) 扣除 payer 而非 sender
测试覆盖率

参见发现 #1 测试。

结论: GAP -- 严重 (与 #1 相同根本原因)

已完成项 (已验证 11 项)

# 理由
1 每帧值 不需要值字段——规范规定帧调用值为零。tx_frame.go:56 处的 FrameTx.value() 返回 new(big.Int)
2 Gas 计算公式 CalldataTokenGas 产生与 4/16 标准相同的结果。在 tx_frame.go:301-317 处验证。
3 TXPARAM 索引 根据规范,非连续的 0x00-0x09, 0x10-0x15 是正确的。在 eip8141_opcodes.go:154-273 处验证。
4 TXPARAMCOPY 堆栈顺序 出栈顺序与 EVM 约定(in1, in2, destOffset, offset, length)匹配。在 eip8141_opcodes_test.go:763-858 处测试。
5 瞬态存储隔离 帧之间的 ClearTransientStorage()。在 frame_execution.go:57-60 处记录。
6 错误处理 帧错误时消耗全部 gas,执行继续。在 frame_execution.go:89-93 处实现。
7 部署帧 DEFAULT-before-VERIFY 本地工作。帧模式 0 (DEFAULT) 在 frame_execution.go:72-73 中没有先决条件。
8 FOCIL 集成 通用 tx.Gas() / tx.Hash() 接口工作。FrameTx 实现了 TxData 接口。
9 FOCIL 发送者 基于哈希的匹配包括 sender 字段。tx_frame.go:224-262 处的 ComputeFrameSigHash 包含 sender。
10 加密内存池 通用 *types.Transaction 包装器工作。帧交易被包装在标准 Transaction 类型中。
11 APPROVE 停止 halts: true 正确——分离的帧用于拆分批准。在跳转表定义中验证。

修改的文件

文件 更改 状态
docs/plans/gap-analysis-eip8141-eip7701-vitalik.md 此文档 新建
pkg/core/aa_entrypoint_test.go 2D nonce 编码/解码测试 新建
pkg/core/vm/aa_executor_test.go Paymaster 集成测试 已修改
pkg/core/vm/eip7701_opcodes_test.go 跨模块 AA 操作码测试 已修改
  • 原文链接: github.com/jiayaoqijia/e...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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