0xMiden / 0xMiden/wallet

Guardian seed recovery resurrects already-spent private notes as claimable — auto-consume retries them forever

Aperta
#742 1 commento 0 reazioni 0 assegnatari Vedi su GitHub
upstream
Lingua principale
TypeScript
Stelle
5
Fork
28
Merge medio
9h 50m
PR unite (30g)
123

Descrizione

@WiktorStarczewski I think we have to address this ASAP; the wallet that is being recovered isn't usable due to this issue. It's impossible to initiate a new transaction because of this issue.

## Summary

After a guardian seed recovery, the wallet re-imports the account's full private-note history from the Note Transport Service, which re-serves every in-retention note with no consumption filter. The client never checks whether those notes' nullifiers are already spent — so notes consumed or sender-reclaimed *before* recovery reappear as pending/claimable, and auto-consume queues them all. Each submit is rejected by the node with `nullifiers already exist`, the failure never marks the note consumed, and it re-queues on the retry backoff — permanently, on every recovered install.

Observed on wallet extension v1.15.21 and the Android app (`@miden-sdk/miden-sdk` 0.15.9, testnet): after recovering the same account on both devices, 37 pending notes appeared whose nullifiers were all provably spent on-chain days-to-weeks before recovery. Result: 148 consume rows / 123 Failed on the extension alone, retrying daily forever, plus queue-expiry failures (`Transaction expired after being queued too long`) because 37 simultaneous doomed consumes can't drain within the 30-minute queue window.

## Reproduction

1. On a guardian account, receive private notes; let some be consumed and/or sender-reclaimed.
2. Wipe the install (or use a second device) and recover the account from seed.
3. Let sync and auto-consume run.

**Expected:** already-spent notes are recognized as consumed (or filtered before auto-consume); nothing pending.
**Actual:** all in-retention notes reappear as claimable; consumes of spent notes fail `nullifiers already exist` and retry forever; the pending list never shrinks. Deterministic — 2/2 installs.

## Root cause

- The NTS fetch path imports notes as `NoteFile::NoteDetails` → `import_note_records_by_details`, which authenticates them to `Committed` via a backward note-tree scan and never queries nullifiers (miden-client v0.15.5, `note_transport/mod.rs:401-412`, `import.rs:344-457`). The correct backward spent-check (`get_nullifier_commit_heights`, `import.rs:252-294`) exists but is wired only to the proof-import path, which NTS never uses.
- The only later consumption trigger is forward-sync `sync_nullifiers(prefixes, checkpoint+1, tip)` (`state_sync.rs:1005-1024`); the checkpoint is monotonic with no rewind, and by the time a recovered account's notes are imported it already sits at chain tip — so pre-recovery spends are invisible forever. Confirmed on-device: the one note spent two minutes after recovery transitioned correctly to consumed-external; the 37 spent before recovery stayed `Committed`.
- Not a node limitation: `/rpc.Api/SyncNullifiers` returns the historical spends when asked with a low `block_from`.

Related, distinct: #215, #615, #617, #453.

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.