fix(lite-backend): serialize clear and rescan with a running sync
clear_state_from wiped the wallet's blocks, txs and mempool maps without taking sync_lock. The console `clear` and `rescan` commands run concurrently with the refresh worker's sync (litelib_execute drops the client mutex before a command runs), so a clear could land after a sync had already taken its nullifier snapshot. scan_block then unwrapped a note that was no longer in the wallet and panicked under the txs write guard, which poisons the wallet until restart. - clear_state_from now takes sync_lock first (lock order sync_lock -> wallet, as in do_save and do_sync). No caller holds sync_lock, so it cannot self-deadlock; do_rescan holds no lock when it clears. - clear_state reads the birthday into a local before calling clear_state_from. Before, the wallet read guard was a temporary that lived across the call, which read-locks the wallet again: with a writer (a save, an import, a new address) queued in between, the writer-preferring RwLock on Linux and Windows could deadlock. Holding it while waiting for sync_lock would also have inverted the lock order against do_save. - scan_block resolves each spent note with and_then instead of three unwraps. A spend of a note that is no longer in the wallet logs a warning and is skipped. Flagged behaviour changes: - console clear/rescan and the GUI rescan wait for an in-flight sync or save to finish instead of racing it; - the recursive wallet read lock that could deadlock a concurrent save is gone; - a spend of a vanished note now warns and is skipped instead of panicking; it is counted after the next rescan. No ABI, symbol or wallet format change. Tests: clear_state_from waits for sync_lock, clear_state holds no wallet guard while it waits, neither leaks the lock, and a 200x clear/write loop finishes within a 20 s watchdog (a regression guard only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+13
-10
@@ -2371,18 +2371,21 @@ pub fn scan_full_mempool_tx(&self, tx: &Transaction, height: i32, _datetime: u64
|
||||
//info!("Txid {} belongs to wallet", tx.txid);
|
||||
|
||||
for spend in &tx.shielded_spends {
|
||||
let txid = nfs
|
||||
// The note can be gone if the wallet was cleared or reorged after `nfs` was taken. Never panic
|
||||
// here (it would poison `txs`): skip the spend with a warning; the next rescan counts it.
|
||||
let spent_note = match nfs
|
||||
.iter()
|
||||
.find(|(nf, _, _)| &nf[..] == &spend.nf[..])
|
||||
.unwrap()
|
||||
.2;
|
||||
let mut spent_note = txs
|
||||
.get_mut(&txid)
|
||||
.unwrap()
|
||||
.notes
|
||||
.iter_mut()
|
||||
.find(|nd| &nd.nullifier[..] == &spend.nf[..])
|
||||
.unwrap();
|
||||
.and_then(|(_, _, txid)| txs.get_mut(txid))
|
||||
.and_then(|wtx| wtx.notes.iter_mut().find(|nd| &nd.nullifier[..] == &spend.nf[..]))
|
||||
{
|
||||
Some(nd) => nd,
|
||||
None => {
|
||||
warn!("Block {}: tx {} spends a note that is no longer in the wallet; skipping it (a rescan counts it)",
|
||||
block_data.height, tx.txid);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Mark the note as spent, and remove the unconfirmed part of it
|
||||
info!("Marked a note as spent");
|
||||
|
||||
Reference in New Issue
Block a user