fix(lite-backend): keep witnesses current for notes in a pending send

Notes spent by a send still in the mempool stopped getting witness
updates. After a restart (pending flags are not saved), or a reorg
that un-mined the send, those notes were spendable again but with
stale witnesses, so the next send from them used an anchor that is not
on chain and failed until a rescan. Deterministic note selection made
this sticky: the stale note was picked every time it was the best fit.

Witnesses now advance for every note that is not spent on chain,
pending or not. Witness clearing still keys on the mined spend only,
so pending notes are never cleared. The wallet file format is
unchanged; an old build reading a file written while a send was
pending can now spend from it (verified offline).

The oracle reloads and reorgs inside the pending window and compares
every unspent note's witnesses byte for byte; reverting the fix fails
9 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 06:05:06 -05:00
co-authored by Claude Opus 5.5
parent db8d5359a4
commit 24a90ae532
3 changed files with 237 additions and 116 deletions
@@ -2323,12 +2323,14 @@ pub fn scan_full_mempool_tx(&self, tx: &Transaction, height: i32, _datetime: u64
// Create a single mutable slice of all the newly-added witnesses.
// A block without shielded outputs appends nothing to any witness: don't un-share (deep-clone) them.
// Advance every note whose nullifier is still tracked (spent.is_none(), the `nfs` set), pending
// ones included: a reload, or a reorg of the block that mined the send, makes them spendable again.
let block_has_outputs = block.vtx.iter().any(|t| !t.outputs.is_empty());
let mut witness_refs: Vec<&mut IncrementalWitness<Node>> = if block_has_outputs {
txs
.values_mut()
.map(|tx| tx.notes.iter_mut().filter_map(
|nd| if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { nd.witnesses.last_mut().map(|w| Arc::make_mut(w)) } else { None }))
|nd| if nd.spent.is_none() { nd.witnesses.last_mut().map(|w| Arc::make_mut(w)) } else { None }))
.flatten()
.collect()
} else {