Skip to content

fix(airaccount): ECDSA algId prefix + SuperPaymaster postGas for v0.7 gasless ops#21

Merged
fanhousanbu merged 4 commits into
mainfrom
fix/m7-ecdsa-algid-prefix
May 22, 2026
Merged

fix(airaccount): ECDSA algId prefix + SuperPaymaster postGas for v0.7 gasless ops#21
fanhousanbu merged 4 commits into
mainfrom
fix/m7-ecdsa-algid-prefix

Conversation

@fanhousanbu
Copy link
Copy Markdown
Contributor

@fanhousanbu fanhousanbu commented May 15, 2026

Summary

两个独立 bug fix,均影响 v0.7 EntryPoint 的 gasless 操作:

Fix 1 — ECDSA algId prefix (commit 27af31c)

  • Root cause: compositeValidator 要求签名以 algId=0x02 开头,SDK 直接传 raw ECDSA sig → AA24 (account signature failed)
  • Fix: BackendSignerAdapter 在签名前拼接 0x02 algId 前缀

Fix 2 — SuperPaymaster postGas (commit e602529)

  • Root cause: paymasterPostOpGasLimit 设为 100,000,但 SuperPaymaster postOp 实际消耗 ~117k gas(recordXPNTsDebt + event emit),导致 AA50 (postOp OOG),user operation 静默回滚
  • Fix: PaymasterManager.getPaymasterData() 中 SuperPaymaster 分支 postGas: BigInt(100000) → BigInt(300000)
  • Verified: Ethereum Sepolia 成功 gasless aPNTs 转账 TX 0xd6eb1c2274be1925cdb77b805f03a44803b3b436564840cbb2c462ecf2327eaf

Files Changed

  • packages/airaccount/src/server/services/paymaster-manager.ts — postGas 100k→300k (1 line)
  • packages/airaccount/src/server/constants/entrypoint.ts — (algId fix 相关)

Test Plan

  • pnpm --filter @aastar/airaccount test 单测通过
  • pnpm -r build 构建通过
  • Sepolia 手动验证:通过 SuperPaymaster 发送 gasless ERC-20 转账,确认无 AA24 / AA50 回滚
  • 确认 postOp gas 用量低于 300k(当前观测值 ~117k)

…alidator

M7 composite validator routes by algorithm ID prefix. Plain ECDSA
signatures without the prefix caused AA24 (signature error) on first
transfer for accounts without a guardian. Prepend 0x02 to the raw
65-byte ECDSA sig so the validator dispatches to the ECDSA verifier.
@fanhousanbu fanhousanbu requested a review from jhfnetboy as a code owner May 15, 2026 03:13
SuperPaymaster's postOp consumes ~117k gas for aPNTs accounting
(recordXPNTsDebt, event emission). The previous limit of 100,000
caused AA50 (postOp out-of-gas), reverting successful user operations.

Minimum safe value confirmed via on-chain tracing on Ethereum Sepolia.
@fanhousanbu fanhousanbu changed the title fix(airaccount): prepend algId=0x02 to ECDSA signature for compositeValidator fix(airaccount): ECDSA algId prefix + SuperPaymaster postGas for v0.7 gasless ops May 21, 2026
@jhfnetboy
Copy link
Copy Markdown
Member

🤖 Round 1 — Claude Code Review (Local Model)

Reviewer: Claude Code (claude-sonnet-4-6) | Tier: Local model review
Scope: paymaster-manager.ts + transfer-manager.ts diffs


🔴 Critical

None.


🟠 High

H1 — ALG_ID constant exists but not used (transfer-manager.ts:203)

packages/airaccount/src/server/constants/entrypoint.ts:264 already exports:

export const ALG_ID = {
  BLS: 0x01,
  ECDSA: 0x02,   // ← this exists
  P256: 0x03,
  ...
} as const;

But transfer-manager.ts only imports EntryPointVersion from that file, then inlines the magic byte "0x02". A future algId renumbering would silently produce wrong-routed signatures with no compile error. Same issue on the BLS path (line 203 uses "0x01" literal).

Fix: Import and use ALG_ID.ECDSA and ALG_ID.BLS.

H2 — No automated regression test for the algId prefix

The signature format change is critical (wrong byte → AA24 on every ECDSA op) but has no unit test. A test asserting userOp.signature.slice(0, 4) === "0x02" for the ECDSA path would prevent silent regressions when this code is touched next.


🟡 Medium

M1 — PR description file list discrepancy

PR body lists entrypoint.ts under "Files Changed" for the algId fix, but the diff only touches transfer-manager.ts. Either the description predates a refactor decision, or the constant extraction was planned and dropped. Needs updating for accurate history.

M2 — Inconsistent algId prefix encoding style

ECDSA path (new): ethers.concat(["0x02", ecdsaSig])
BLS path (line 203, existing): "0x01" + packedBls.slice(2)

Both produce identical bytes but two different patterns in the same file creates maintenance confusion. Should standardize on one approach.

M3 — postGas = 300_000 magic number without safety margin comment

300k is ~2.56× the observed ~117k. The number is defensible but should explain itself:

// recordXPNTsDebt + event emit observed ~117k; 300k gives safe headroom
const postGas = BigInt(300_000);

Also note: V4 path (same file, line 187) uses BigInt(0x30000) ≈ 196k for its paymasterPostOpGasLimit. Having a SuperPaymaster-specific higher value is fine but worth calling out.


🔵 Low

L1 — verGas = 80_000 not re-examined

postGas was bumped due to gas growth. Was validatePaymasterUserOp (verGas = 80k) also checked for potential OOG with v5 role checks? A brief comment confirming this was measured would close the question.

L2 — Milestone tag in comment will rot

"M7 ECDSA path" in the log string and code comment is a milestone reference that will be meaningless post-M7 ship. Prefer behavioral description: "compositeValidator ECDSA routing".


✅ Verdict

Approve with requests. Both fixes are mechanically correct and verified on Sepolia (0xd6eb1c22...). The algId prefix mechanics (ethers.concat(["0x02", sig])) produce the correct 66-byte signature. The 300k postGas resolves the documented AA50.

Primary asks before merge:

  1. Use ALG_ID.ECDSA / ALG_ID.BLS constants instead of inline magic bytes
  2. Add a unit test asserting the algId prefix is applied on the ECDSA signing path

Round 2 (Codex review) will follow in a separate comment.

@jhfnetboy
Copy link
Copy Markdown
Member

🤖 Round 2 — Codex Review

Reviewer: Codex (via codex:rescue subagent) | Tier: Codex CLI plugin
Scope: paymaster-manager.ts + transfer-manager.ts diffs


🔴 Critical

None.


🟠 High

H1 — 0x02 algId prefix applied to non-compositeValidator fallback paths

useECDSA = true is triggered by three conditions in transfer-manager.ts:

  1. accountCode === "0x" — undeployed/no-code sender
  2. validator() returns ZeroAddress — no validator set
  3. catch { useECDSA = true } — validator read failure (network error, ABI mismatch)

The fix prepends 0x02 to all three. Conditions 1 and 3 are not confirmed compositeValidator accounts — sending 0x02 || sig to an account expecting raw 65-byte ECDSA is a correctness regression that silently breaks those users (likely AA24).

Suggested fix: Only prepend 0x02 after positively confirming a compositeValidator account. Keep raw ECDSA (or fail closed) for the error-catch and undeployed fallback paths.


🟡 Medium

M1 — postGas = 300_000 is over-provisioned without a bounded rationale

Observed ~117k gas; 300k is 2.56× headroom. This increases maxCost / prefund / deposit requirements for every SuperPaymaster UserOperation by ~183k gas, causing avoidable sponsorship failures under high fee conditions.

Suggested fix: Use a named constant with a comment (e.g. POST_GAS_LIMIT = 150_000n) or make it configurable. Add regression coverage for the recordXPNTsDebt + event emit path.

M2 — No regression tests for the new ECDSA wire format

The diff changes ECDSA UserOperation signatures from 65 to 66 bytes with no test asserting:

  • compositeValidator accepts 0x02 || ecdsaSig
  • no-code / zero-validator / validator-read-error fallback paths still work correctly

🔵 Low

L1 — PR description does not match actual diff

PR body claims entrypoint.ts was changed for the algId fix. Actual diff: only paymaster-manager.ts and transfer-manager.ts. Creates ambiguity about whether off-chain algId routing was updated alongside on-chain integration.


❌ Verdict

Request Changes. The useECDSA fallback scope is too broad — prefixing 0x02 on undeployed accounts and validator-read-error fallback paths is a potential correctness regression. This must be resolved (scope the prefix to confirmed compositeValidator accounts only) before merge.


Round 1 (Claude review) is in the preceding comment.

@jhfnetboy
Copy link
Copy Markdown
Member

⚔️ PK 对比 — Claude vs Codex

维度 Claude Codex
总体裁决 ✅ Approve with suggestions ❌ Request Changes
Critical
最重要发现 ALG_ID.ECDSA 常量已存在却未用,用了魔法字面量 "0x02" useECDSA=true 的三条触发路径中,有两条(未部署账户 + catch fallback)不应加 0x02 前缀
一致意见 ✅ 均发现:缺少单测 ✅ 均发现:PR 描述 entrypoint.ts 与 diff 不符
分歧点 Claude 未注意到 fallback 路径的错误覆盖 Codex 未发现 ALG_ID 常量已存在
postGas 300k 需要注释说明安全裕度 建议降到 150k,认为 300k 会增加 prefund 风险

🏆 PK 结论

Codex 的 H1 更关键useECDSA=true 由三条路径触发,其中"validator() 读取异常 catch → useECDSA"这条路径会给非 compositeValidator 账户加上 0x02 前缀,导致 AA24 静默失败。这是 Claude 漏掉的正确性回归风险。

Claude 的 H1 互补ALG_ID.ECDSA 常量已存在是代码健壮性问题,Codex 未提及。

综合建议(须在 merge 前解决)

  1. [阻塞] 明确区分 compositeValidator 账户路径 vs. 纯 ECDSA fallback 路径——仅对前者加 0x02 前缀
  2. [阻塞] 添加单测覆盖三条 useECDSA 触发条件
  3. [建议] 改用 ALG_ID.ECDSA 常量替代字面量 "0x02" / "0x01"
  4. [建议] postGas 添加注释说明 117k 实测值和安全裕度依据

…LG_ID constants + tests

Codex H1 (blocking): useECDSA=true was applied to three paths but algId=0x02 prefix
was unconditionally added to all three. The catch{} path (validator() call failure)
cannot confirm compositeValidator — it now uses raw ECDSA to prevent AA24 on
non-compositeValidator accounts.

Fix: introduce detectSignatureStrategy() that tracks isCompositeValidator separately.
Only apply ethers.toBeHex(ALG_ID.ECDSA, 1) prefix when isCompositeValidator=true.

Claude H1: replace magic bytes "0x02"/"0x01" with ALG_ID.ECDSA / ALG_ID.BLS constants.

M2 (consistency): standardize BLS signature encoding to ethers.concat() (was string concat).

M3 (postGas): add comment explaining observed ~117k gas and 300k safety headroom.

Tests: 4 new unit tests for detectSignatureStrategy covering all three code paths;
1 new test for V4 paymasterData byte layout.
@fanhousanbu
Copy link
Copy Markdown
Contributor Author

回应评审意见

感谢 Claude/Codex 两轮评审!本次提交(7fc8f7d)处理了所有阻塞和主要建议:

🔴 阻塞问题(已修复)

Codex H1 — useECDSA 覆盖范围过宽

引入 detectSignatureStrategy(provider, address) 独立函数,用 isCompositeValidator 单独追踪:

触发条件 useECDSA isCompositeValidator 签名格式
accountCode === "0x" (未部署) true true algId || ecdsaSig
validator() === ZeroAddress (已部署) true true algId || ecdsaSig
catch {} (validator() 调用失败) true false 原始 ECDSA(无前缀)

🟠 High(已修复)

Claude H1 — 魔法字节 → ALG_ID 常量

// Before
userOp.signature = ethers.concat(["0x02", ecdsaSig]);
userOp.signature = "0x01" + packedBls.slice(2);

// After
userOp.signature = ethers.concat([ethers.toBeHex(ALG_ID.ECDSA, 1), ecdsaSig]);
userOp.signature = ethers.concat([ethers.toBeHex(ALG_ID.BLS, 1), packedBls]);

Claude H2 / Codex M2 — 添加单元测试

  • detectSignatureStrategy 4 个测试覆盖所有三条路径(包括 catch 分支)
  • paymaster-manager.test.ts 新增 V4 paymasterData 字节布局校验

🟡 Medium(已修复)

  • M2: BLS 编码统一改为 ethers.concat
  • M3: postGas = BigInt(300_000) 加注释说明 ~117k 实测值和安全裕度

未处理

  • M1 (PR 描述文件列表): 已在此评论说明实际变更文件
  • L2 (M7 里程碑注释): 已在本次提交中移除 "M7" 引用,改为行为描述

284 个单元测试全部通过。

@jhfnetboy
Copy link
Copy Markdown
Member

🤖 Round 1 (Second Pass) — Claude Code Review

Reviewer: Claude Code (claude-sonnet-4-6) | Pass: Post-fix re-review
Commit reviewed: 7fc8f7d (address PR #21 review — compositeValidator scope + ALG_ID constants + tests)


✅ Resolved from Round 1

Round 1 Issue Status
Codex H1: useECDSA fallback scope too broad ✅ Fixed — detectSignatureStrategy() now tracks isCompositeValidator separately; prefix only applied when true
Claude H1: inline magic bytes "0x02" / "0x01" ✅ Fixed — uses ALG_ID.ECDSA / ALG_ID.BLS constants
Both: no unit tests ✅ Fixed — 4 tests for detectSignatureStrategy, 1 for paymaster byte layout
Inconsistent encoding style (string concat vs ethers.concat) ✅ Fixed — BLS path now uses ethers.concat consistently
PR description entrypoint.ts discrepancy ✅ Fixed in commit message
postGas magic number lacks comment ✅ Fixed — comment added explaining ~117k observed + 300k headroom

🔴 Critical

None.


🟠 High

None.


🟡 Medium

M1 — SuperPaymaster postGas=300_000 branch still has no unit test

The new paymaster-manager.test.ts test explicitly states in its comment: "Without a live RPC the SuperPaymaster/token() calls fail and fall through to the PaymasterV4 branch." It tests V4 gas limits (0x30000), not the SuperPaymaster 300_000. The key change (100k→300k) is still only integration-verified via Sepolia TX.

A minimal fix: mock the operators() RPC call to return a valid SuperPaymaster response, then assert postGas = 300_000 in the packed data at the correct byte offset.

M2 — getCode() failure and validator() failure share the same catch block

} catch {
  // validator() call failed: account may not be compositeValidator.
  return { useECDSA: true, isCompositeValidator: false };
}

The comment says "validator() call failed" but this catch also triggers if provider.getCode() fails (RPC timeout, node unavailable). A compositeValidator account during a transient RPC failure will silently receive a raw ECDSA signature → AA24. This is the same behavior as pre-PR but the comment is now misleading.

Suggest splitting into two try-catch blocks or adding // Note: also catches getCode() failures.


🔵 Low

L1 — Undeployed account assumption is an undocumented protocol invariant

if (accountCode === "0x") {
  // Counterfactual AirAccount: will deploy as compositeValidator.
  return { useECDSA: true, isCompositeValidator: true };
}

The comment says "will deploy as compositeValidator" but this is enforced by the AirAccount factory — not verifiable here at runtime. If detectSignatureStrategy is ever called with a non-AirAccount address (plain EOA, non-compositeValidator factory), this returns wrong results silently.

Suggest: // AirAccount factory invariant: all counterfactual addresses are compositeValidator deployments.

L2 — BLS path does not check isCompositeValidator

The ECDSA path now guards on isCompositeValidator before adding the prefix. The BLS path adds ALG_ID.BLS unconditionally. This asymmetry is intentional (BLS accounts are always compositeValidator by design) but not commented, and the behavior differs from the ECDSA path.


✅ Verdict

Approve. All blocking issues from Round 1 are correctly resolved. detectSignatureStrategy() cleanly separates the compositeValidator scope, the constants are in use, and the tests cover the three meaningful code paths. M1 (missing SuperPaymaster unit test) is the only item worth picking up before the next merge window.


Round 2 (Codex re-review) will follow in a separate comment.

@jhfnetboy
Copy link
Copy Markdown
Member

🤖 Round 2 (Second Pass) — Codex Review

Reviewer: Codex (via codex:rescue subagent)
⚠️ Meta-note: Codex read commit e602529 (second commit — postGas only), NOT the latest 7fc8f7d (which contains the detectSignatureStrategy + ALG_ID fix). Critical/High findings below reflect that older state. Medium/Low are evaluated against the full diff and remain valid.


🔴 Critical — STALE (already fixed in 7fc8f7d)

detectSignatureStrategy() not implemented — useECDSA still a single flag, 0x02 unconditionally applied

Status: Fixed in 7fc8f7d. detectSignatureStrategy() correctly separates isCompositeValidator.


🟠 High — STALE (already fixed in 7fc8f7d)

ALG_ID constants not imported, magic bytes "0x02" / "0x01" still used

Status: Fixed in 7fc8f7d. ALG_ID.ECDSA / ALG_ID.BLS are now imported and used.


🟡 Medium — VALID (applies to latest diff)

M1 — SuperPaymaster 300k postGas is not asserted in any test

The new paymaster-manager.test.ts test decodes verGas/postGas byte positions, but only for the V4 fallback path (value 0x30000 ≈ 196k). The SuperPaymaster branch (postGas = BigInt(300_000)) requires a live operators() RPC call to detect, so it cannot be reached in unit tests without mocking. The 300_000 constant is still integration-only verified (Sepolia TX hash).

M2 — detectSignatureStrategy tests exist but packSignature double-prefix contract is untested

packSignature() is assumed to return a hex string without algId prefix. If this changes, the BLS path silently gets a double prefix. No byte-layout test protects this invariant.


🔵 Low — VALID

L1 — Both getCode() failure and validator() failure fall into the same catch block

The catch comment says "validator() call failed" but it also catches getCode() network errors. A transient RPC failure for a confirmed compositeValidator account produces raw ECDSA (no prefix) → potential AA24. Comment is misleading.


Verdict (re-evaluated against latest commit 7fc8f7d)

Approve — Codex's Round 1 blocking issue is properly fixed. Remaining items are Medium/Low quality improvements. The SuperPaymaster 300k unit test gap (M1) is the only item worth a follow-up ticket.


Note on Codex tooling: Codex read commit e602529 rather than the branch HEAD 7fc8f7d. When using Codex for re-review, explicitly pin the commit SHA to avoid reading stale state.

@jhfnetboy
Copy link
Copy Markdown
Member

⚔️ PK 对比 Round 2 — Claude vs Codex

维度 Claude (Round 2) Codex (Round 2)
读取的 commit 7fc8f7d (最新) ✅ e602529 (旧,第 2 个 commit) ⚠️
裁决 ✅ Approve ✅ Approve(重新评估后)
原 Codex H1 修复验证 ✅ 已确认正确修复 ⚠️ 读到旧代码,误报为未修复
一致发现 SuperPaymaster 300k 无单测 SuperPaymaster 300k 无单测
独家发现 (Claude) getCode() / validator() 共用 catch 导致注释误导
独家发现 (Codex) packSignature() 不带前缀是隐式契约,缺字节布局测试保护

🏆 Round 2 PK 结论

这轮 Claude 更可靠 — 读到了最新的 7fc8f7d,能正确评估 detectSignatureStrategy() 和 ALG_ID 修复。Codex 读到了旧 commit,Critical/High 全部是误报(均已在最新 commit 修复)。

但 Codex 的 Low 发现有价值packSignature() 不携带前缀是隐式契约,没有字节布局测试保护,这是 Claude 漏掉的。

📋 综合行动项(两轮 PK 共识)

优先级 行动 来源
Merge OK PR 可合并 两家均 Approve
跟进 ticket 为 SuperPaymaster 300k 分支补写单测(mock operators() RPC) Claude M1 + Codex M1
跟进 ticket packSignature() 字节布局添加保护测试 Codex L1
代码注释 catch 块注释改为"covers both getCode() and validator() failures" Claude M2

🔧 Codex 工具使用备注

Codex 在 re-review 时读到了旧 commit e602529 而非 HEAD 7fc8f7d。后续使用 Codex 做二次 review 时,建议在 prompt 中显式指定 commit SHA,避免读到 PR 的中间状态。

…review

- Undeployed path: 'AirAccount factory invariant' instead of 'will deploy as'
- catch block: explicitly covers both getCode() and validator() failures
- BLS path: document asymmetry — BLS always compositeValidator by design
@fanhousanbu
Copy link
Copy Markdown
Contributor Author

Round 2 评审回应(commit fab6174

两家均 Approve,感谢!本次处理剩余注释问题:

已修复

Claude L1 — 未部署账户注释

// Before: Counterfactual AirAccount: will deploy as compositeValidator.
// After:  AirAccount factory invariant: all counterfactual addresses are compositeValidator deployments.

Codex L1 / 两家共识 — catch 块注释

// Before: validator() call failed: account may not be compositeValidator.
// After:  Covers both getCode() and validator() failures (network error or non-compositeValidator account).

Claude L2 — BLS 路径非对称性

// Before: BLS triple signature with algId prefix for compositeValidator routing
// After:  BLS accounts are always compositeValidator by design — algId prefix applied unconditionally.

后续 ticket(不阻塞 merge)

  • M1: 为 SuperPaymaster 300k postGas 分支补写单测(需 mock operators() RPC)
  • M2 (Codex): 为 packSignature() 字节布局添加保护性测试,防止 double-prefix 静默回归

284 个测试全部通过,可以 merge。

Copy link
Copy Markdown
Member

@jhfnetboy jhfnetboy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved

All review items addressed across 4 commits:

Commit Content
27af31c Original algId=0x02 fix
e602529 postGas 100k→300k
7fc8f7d detectSignatureStrategy() + ALG_ID constants + tests (Codex H1 + Claude H1)
fab6174 Comment clarifications (Claude L1/L2/M2, Codex L1)

284 tests passing. Good to merge.

Follow-up tickets (non-blocking):

  • Unit test for SuperPaymaster postGas=300_000 branch (mock operators() RPC)
  • Byte-layout test for packSignature() to guard against silent double-prefix regression

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants