本文详细分析了EIP-8141和EIP-7701的实现与规范之间的差距,并结合Vitalik Buterin的审查反馈,指出了7个关键缺陷(包括3个严重缺陷),涉及Gas结算、内存池安全、2D Nonce、APPROVE检查、交易池模拟和EOA兼容性。文章为每个缺陷提供了详细的修复方案、代码路径和测试覆盖,并列出了11项已通过验证的实现。
对我们的 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 转移 |
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 被扣除 |
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 仍然可用 |
uint64,而非 256 位 nonceEIP-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 管理。
uint64nonce 破坏了隐私保证并限制了交易格式。
| 文件:行 | 代码 | 问题 |
|---|---|---|
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 |
tx_frame.go 使用 *big.Int)CALLER==ADDRESS 代理,而非 ADDRESS==frame.TargetEIP-8141 第 2.2 节 (APPROVE 语义):
APPROVE 必须验证 ADDRESS(正在执行的合约)等于帧的目标地址。这确保只有预期的合约才能批准执行或支付。
Vitalik 评论:
当前的
CALLER==ADDRESS检查是真实要求的代理。当帧目标直接调用自身时它有效,但在 delegatecall 场景中可能会失败,其中CALLER != ADDRESS但ADDRESS == 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 的正常路径 |
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 的帧交易被接受 |
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 帧返回清晰错误 |
与发现 #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 测试。
| # | 项 | 理由 |
|---|---|---|
| 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!