Guardian seed recovery resurrects already-spent private notes as claimable — auto-consume retries them forever
- 主要语言
- TypeScript
- 星标
- 5
- 派生
- 28
- 平均合并
- 9 小时 50 分钟
- 30 天内合并 PR
- 123
描述
@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.
贡献指南
调研方向
The issue is in the note import logic. Start by examining the NTS fetch path in `note_transport/mod.rs` lines 401-412 and `import.rs` lines 344-457. Compare it with the proof-import path that uses `get_nullifier_commit_heights` (lines 252-294). The fix likely involves wiring the spent-check into the NTS import flow. Check `state_sync.rs` lines 1005-1024 for the forward sync logic. Testing requires setting up a guardian account, creating spent notes, and performing a seed recovery to verify the fix.
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- rust, typescript
- 领域
- backend, databases
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 冷清
- 描述清晰度
- 描述清楚
- 新手友好度
- 45/100