perf(bridge-core): trim + overlap the return claim's preflight, tighten its finality poll - #55
Yoni-Starkware wants to merge 1 commit into
Conversation
…en its finality poll
The claim ("Adding to your private balance") is the LAST leg of a return, and all of
its preflight runs serially after the CCTP attestation lands, so every read is time
the user spends watching a transfer that is already funded. Three changes, no change
to what the proof commits to:
1. Skip fetchPoolFeeAmount() under the AVNU paymaster. approvePoolFee() already
returns undefined immediately when config.paymaster is set (poolFee.ts) — the pool's
STRK fee is fronted by the relayer/forwarder and repaid via the proof's fee withdraw
— so the get_fee_amount() read could only ever be discarded. The manager path is
unchanged: it still reads, approves, and seeds the aging anchor from that block.
2. Start the AVNU fee quote CONCURRENTLY with the quiescence gate. paymasterBuildLeg's
request for this leg is constant ({type:'apply_action', pool_address} + fee mode), so
it depends on nothing the gate produces, yet today it runs strictly behind it. The
quote is deliberately not prefetched across any real wait — it is a live
STRK -> pool-fee-token conversion the proof must bake in exactly, so it is dropped and
re-quoted on the non-quiescent (aging) path, the manager fee-approve path, and every
rebuild/rebuildFresh. Its age stays bounded by the gate, ~one discovery round trip.
3. Track the claim on a 700ms -> 2.5s poll grid instead of submitAndTrack's 1s -> 8s
default. That default was tuned for attestation-scale waits; here an 8s step can sit
seconds past an acceptance that already happened, and nothing follows the claim. The
hard timeoutMs is unchanged.
Not done, and why: carrying a warm discovery registry into the claim build looks like
the bigger win but is unsafe against the current SDK. compiler.ts REPLACES
registry.notes per token with whatever the cursored discovery returns, and a cursored
/v1/sync/incoming_state returns only notes past last_note_index — so one note arriving
between the warm scan and the build wipes the earlier notes from the registry and the
fee withdraw can fail note selection. Removals are invisible incrementally too (spent
notes are filtered out, not reported), which is exactly what the quiescence gate must
detect, so the gate's own scans cannot go incremental either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR SummaryMedium Risk Overview Paymaster path: Skips AVNU fee quote: Starts Claim tracking: Tests: New Reviewed by Cursor Bugbot for commit bed3ea2. Bugbot is set up for automated code reviews on this repo. Configure here. |
The claim leg — "Adding to your private balance" — measured 21.4s on a real mainnet redeem (claim tx
0x400c77a0…d22e, block 12,816,489). It is the last leg of a return and all of its preflight runs serially after the CCTP attestation lands, so every read is time the user spends watching a transfer that is already funded.Three changes. None of them alter what the proof commits to.
1. Skip
fetchPoolFeeAmount()under the AVNU paymasterapprovePoolFee()already returnsundefinedimmediately whenconfig.paymasteris set (poolFee.ts:39) — the pool's STRK fee is fronted by the relayer/forwarder and repaid through the proof's fee withdraw. So theget_fee_amount()read that precedes it can only ever be discarded. One RPC round trip off the critical path.The manager path is untouched: it still reads, approves, and seeds the aging anchor from that block (covered by a new test).
2. Start the AVNU fee quote concurrently with the quiescence gate
paymasterBuildLeg's request for this leg is constant —{type:'apply_action', pool_address}plus the configured fee mode — so it depends on nothing the gate produces, yet today it runs strictly behind it insidebuildOnce.It is deliberately not prefetched across any real wait. The quote is a live STRK→pool-fee-token conversion the proof must bake in exactly, so it is dropped and re-quoted on every path that adds delay before the build: the non-quiescent (aging) path, the manager fee-approve path, and every
rebuild/rebuildFresh. Its age stays bounded by the gate itself — roughly one discovery round trip.3. Tighter finality poll for the claim: 700ms → 2.5s (was 1s → 8s)
submitAndTrack's default backoff was tuned for attestation-scale waits, where an 8s ceiling costs nothing. Here nothing follows the claim, and against Starknet block times an 8s step can sit seconds past an acceptance that already happened. The hardtimeoutMsis unchanged.Not done, and why
Carrying a warm discovery registry into the claim build looks like the bigger win — it would make the build's notes scan incremental — but it is unsafe against the current SDK:
compiler.tsreplacesregistry.notesper token with whatever the cursored discovery returns, and a cursored/v1/sync/incoming_statereturns only notes pastlast_note_index. One note arriving between the warm scan and the build therefore wipes the earlier notes from the registry, and the fee withdraw can fail note selection ("Insufficient balance").Collapsing the claim's three note scans (gate ×2 + build ×1) plus the channels scan into one needs a discovery/SDK change, not a bridge-core change.
Testing
npx vitest runinpackages/bridge-core— 1103 passed (110 files), including 6 new cases inbridgeBack.test.ts: paymaster skips the fee read, manager still reads/approves/anchors, quote overlaps a held-open gate, non-quiescent path re-quotes after aging, rebuild re-quotes, and the poll grid is tighter than the default.npx tsc --noEmitclean,eslintclean.🤖 Generated with Claude Code
This change is