rate-limit every passphrase check, and stop address issuance outrunning the scan
two holes found auditing the wallet sidecar after the frozen-utxo fix. the brute-force backoff lived inside UnlockSession.unlock alone, so /unlock capped at five guesses a minute while export-seed — the one endpoint that returns the words in the clear — took unlimited ones. every passphrase check now goes through the same guard. verifyPassphrase rethrows LOCKED_OUT rather than folding it into `false`, so a caller can tell "wrong" from "stop". nextUnused advanced its mark on every issuance, paid or not, so a run of unpaid addresses walked it past the end of the window the next scan covers; a payment there would never be found again, and esplora has no rescan to go looking. sources now declare how far past a scan's last index they can still see, and issuance clamps to it — re-offering a virgin address rather than handing out one that could lose money. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -719,6 +719,17 @@ export class OnchainBackend extends BaseBackend {
|
||||
// Anything past the scanned window is unused by definition — the gap limit is what ended the scan.
|
||||
while (index <= highest && used.has(index)) index++;
|
||||
|
||||
// ...but "unused" is not the same as "findable". The mark advances on every issuance, paid or not,
|
||||
// so twenty unpaid addresses walk it past the end of the window the next scan covers, and a payment
|
||||
// to the twenty-first would never be seen again. Rather than hand out an address the wallet could
|
||||
// lose money on, stop and re-offer the last safe one: every index in that run is still virgin, so
|
||||
// repeating one is not address reuse in the sense that matters — nothing was ever received on it.
|
||||
// The floor of 0 covers a wallet the chain has never seen: Esplora short-circuits an account with no
|
||||
// history and returns no addresses at all, leaving `highest` at -1, and index 0 is then the only one
|
||||
// it will look at. Handing that out until it is paid is exactly right.
|
||||
const ceiling = Math.max(0, highest + this.chain.issueAhead);
|
||||
if (index > ceiling) index = ceiling;
|
||||
|
||||
if (!peek) this.issued.set(key, index + 1);
|
||||
return this.derive(type, chain, index);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user