Resilience audit, vendored SDXL core (post-hoc wallet bookkeeping only — the send/build/broadcast path,
note/UTXO selection, fees, recipients, memo and the post-broadcast pending-marking are byte-identical):
- cleanup_mempool never cleared unconfirmed_spent, so a send that was dropped / never mined / reorged out
locked its notes and UTXOs until the app was reloaded ("insufficient funds" on a funded wallet). It now
releases the lock once the send is provably dead. A send is built with Builder::new(tip) (expiry = tip+20,
mineable through tip+20), so the lock is held until the chain reaches tip+21 (MEMPOOL_KEEP = DELTA+1) —
one block past the old mempool-drop point, which would have released the notes while the tx could still be
mined. A txid already in `txs` was mined (scan settled its inputs) and is skipped. mempool_txs is released
before `txs` is taken, so the two locks are never nested.
- invalidate_block reset nd.spent on reorged-out spenders but never walked wtx.utxos, so a transparent UTXO
whose spender was reorged out kept spent=Some(removed txid) and dropped out of the balance until a rescan.
It now resets utxo.spent the same way (unconfirmed_spent left as-is, as for notes — the send may be re-mined
on the new branch).
- Removed the stale "TODO remove unconfirmed_spent on expiry" in data.rs (now done).
Verified with the scratch-mirror rtest.sh (cargo 1.63, scratch HOME): cargo test --release --lib green,
129 passed / 0 failed / 12 ignored, including the oracle differential tests (updated in lockstep to clear
the sim's pending flag at the same expiry point) and test_z_mempool_expiry. The C-ABI wrapper and the
artifact rebuild / lite_smoke (needs a live server) were not run; they don't touch these functions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>