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>