Repository navigation
Fix fee options stub signature for pending config updates - #1031
Conversation
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.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
patrislav
left a comment
There was a problem hiding this comment.
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.
Problem
Passkey wallets with pending config updates can't get relayer fee options. The relayer rejects
FeeOptionswith a 400: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 chainsstatus.pendingUpdateswith their real signatures.On-chain, each suffix link of a chained signature is verified against a
config-updatepayload whoseimageHashis 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: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
FeeOptionsrequest with the suffix stubbed returns 200 with fee options.Tests
wallet-fee-optionstest 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 2transactions.test.tsfailures as onmasterlocally; nothing new.