Files
amethyst/amethyst
Claude 954560666a perf: stop the wallet paying per-bundle signer round-trips and per-mint rescans
Audit of the paths the proof backfill makes hot, plus two bugs it makes
reachable.

/v1/checkstate went out unchunked. scrubStaleProofs checks every proof
held at a mint in one call and that set is unbounded — it grows with the
wallet's history, and a client that pages its whole proof set back off
the relays reaches four figures in one sweep. Mints run the same Pydantic
list caps there that they do on /v1/restore, which this file already caps
at 500 for exactly that reason, so the sweep failed with a validation
error at the moment the wallet had the most to reconcile. Chunked, and
the hash-to-curve derivation now happens once per proof instead of twice
(it was computed separately for the request list and the response
lookup — a discarded EC operation per proof, every sweep).

The auto-redeem sweep paid two NIP-44 decrypts of kind:17375 before
checking whether it had anything to redeem, and p2pkPubkeyHex re-decrypts
the same event walletPrivkeyHex just read. That sweep fires from every
relevant cache bundle, so a wallet whose nutzaps were all redeemed months
ago still paid two out-of-process round-trips per bundle on a NIP-46
bunker or a NIP-55 external signer. The candidate filter needs no key, so
it now runs first, and the pubkey is derived from the privkey in hand.

A kind:7375 we cannot decrypt hides money exactly as effectively as one a
relay never delivered, and looked identical to an empty wallet.
recomputeUnspent caches only successes, so failures are retried — but
only when something else marks tokens dirty, which in a quiet wallet may
be never. Failures are now counted and logged, and a forced resync
retries them even when the relay walk found nothing new.

Two quadratic scans that were invisible while truncation kept the entry
list tiny: peekNutzapFunding filtered the whole entry list once per
shared mint, allocating a list each time, from inside a composable
remember (so per rendered note); and cleanupDuplicateProofs compared all
pairs before every Resync. Both are single-pass/indexed now — a superset
of B must share all of B's secrets, so only entries indexed under B's
first secret can cover it.

Finally, scanning every keyset made Resync N times slower by
construction: each keyset costs at least three /v1/restore round-trips
with 500-item bodies, so a mint that has rotated ten times turned a
three-request scan into thirty run end to end. The walks are independent
and read-only, so they run three at a time — bounded to stay polite to
the mint's rate limiter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaZ8RprmKC3sidsq6W8dKY
2026-08-17 00:09:40 +00:00
..
2024-06-24 14:13:55 -04:00