Skip to content

Fix fee options stub signature for pending config updates - #1031

Merged
taylanpince merged 2 commits into
masterfrom
fix/fee-options-stub-pending-updates
Oct 13, 2026
Merged

taylanpince merged 2 commits into
masterfrom
fix/fee-options-stub-pending-updates

Conversation

@tolgahan-arikan

Copy link
Copy Markdown
Contributor

Problem

Passkey wallets with pending config updates can't get relayer fee options. The relayer rejects FeeOptions with a 400:

unable to get fee options: gas usage simulation failed: Reverted(..., InvalidPasskeySignature(...))

Google/email login wallets in the same situation work fine.

Root cause

buildFeeOptionsStubSignature (added in #999) replaces the current configuration's signatures with a stub ECDSA signature, but chains status.pendingUpdates with their real signatures.

On-chain, each suffix link of a chained signature is verified against a config-update payload whose imageHash is recovered from the previous link. With the stub, that recovered imageHash is garbage, so every real signature in the suffix is checked against the wrong digest:

  • ECDSA (identity instrument / device signers): recovers a wrong address, no revert, so the simulation succeeds.
  • Sapient (passkeys) and ERC-1271: the signer reverts (InvalidPasskeySignature) and aborts the whole simulation.

It affects wallets that still have pending updates on the target chain, e.g. undeployed there after adding a device or login method. Fresh passkey wallets aren't affected.

I verified this against a real prod wallet on Base: the passkey signature in the suffix is a valid P256 signature, and its challenge equals Payload.hash(wallet, 0, { type: 'config-update', imageHash }) for the correct update. Only the on-chain digest differs, because of the stubbed previous link.

Fix

Also stub every signed leaf (isRawSignerLeaf, any signature type) in the pending-update suffix topologies, keeping each leaf's weight. Unsigned leaves are left as they are. Real transaction building (buildTransaction, 4337, message signing) is unchanged.

Replaying the failing Base FeeOptions request with the suffix stubbed returns 200 with fee options.

Tests

  • Added a wallet-fee-options test with a pending update signed by a passkey (sapient_compact) signer, asserting the suffix is stubbed. It fails without the fix.
  • packages/wallet/core: 402/402 tests pass with an anvil arbitrum fork. Typecheck, eslint and prettier are clean.
  • packages/wallet/wdk: the same 2 transactions.test.ts failures as on master locally; nothing new.

buildFeeOptionsTransaction stubbed the current configuration's signatures
but chained pending config updates with their real signatures. On-chain,
each suffix link is verified against a config-update payload built from
the imageHash recovered from the previous link, which the stub makes
meaningless. ECDSA signatures just recover a wrong address, but sapient
signers (passkeys) and ERC-1271 signers revert, so the relayer rejected
FeeOptions with InvalidPasskeySignature for passkey wallets with pending
updates (e.g. undeployed on the target chain after adding a device).

Stub every signed leaf in the suffix as well, keeping its weight.
@tolgahan-arikan
tolgahan-arikan requested a review from a team October 13, 2026 11:21
@tolgahan-arikan
tolgahan-arikan requested a review from a team as a code owner October 13, 2026 11:21
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T11:24:41.904976Z 05e8840 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

patrislav
patrislav previously approved these changes Oct 13, 2026

@patrislav patrislav left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! One comment from Claude:

One gap in the test. getConfigurationUpdates never returns unrecovered-signer
leaves, which is what pendingSignature mocks. All three providers return
recovered leaves:

  • Sequence: recover()s the config before returning it → {type:'sapient-signer', address, imageHash, weight, signed:true, signature}
  • Local: Signature.fillLeaves → {...leaf, signature}
  • Arweave: toRecoveredLikeTopology

Prod is still fixed, because isRawSignerLeaf is a duck-type ('weight' in cand && 'signature' in cand), so signed leaves match it too — I confirmed that
directly. But nothing pins it down: tightening isRawSignerLeaf to check
type === 'unrecovered-signer', or rewriting stubFeeOptionsSignedTopology as a
switch (topology.type), would keep this test green and bring back
InvalidPasskeySignature for exactly the wallets the PR fixes.

Worth a second case with the recovered shape:

{
  type: 'sapient-signer',
  address: PASSKEYS_MODULE,
  imageHash: '0x…',
  weight: 1n,
  signed: true,
  signature: { type: 'sapient_compact', address: PASSKEYS_MODULE, data: PASSKEY_SIGNATURE_DATA },
}

Stubs to the same eth_sign leaf, so expect(signature).not.toContain(PASSKEY_SIGNATURE_DATA.slice(2))
alone covers it — no need to repeat the full encoded-equality assertion.

State providers return recovered leaves (e.g. sapient-signer with
signed: true) for pending updates, not unrecovered-signer leaves. Pin
that shape so the stub can't regress if isRawSignerLeaf is tightened.
@taylanpince
taylanpince merged commit 9817c9a into master Oct 13, 2026
7 checks passed
@taylanpince
taylanpince deleted the fix/fee-options-stub-pending-updates branch October 13, 2026 13:39
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.

3 participants