ChainResetGuard
Detects a chain RESET — a localnet docker down/up replaces the chain with a fresh genesis — and wipes the stale wallet caches so they re-sync from the new chain.
Why this exists: all three sync layers (shielded, dust, unshielded) decide "already synced" by comparing event/transaction-ID COUNTS. After a reset the fresh chain restarts IDs at 0 and, once the wallet is re-funded, re-climbs to the SAME counts — so every layer concludes "at tip" and keeps the OLD balance (the 30,001-NIGHT ghost). Left unfixed, the stale DUST also fails the next spend with node error 171 (OutOfDustValidityWindow).
Why a CHECKPOINT block, not the genesis hash (the #36 fix): the obvious discriminator is the genesis (height-0) hash — but a localnet booting from a FIXED chain spec reproduces the same genesis hash byte-for-byte across a reset (verified against a live localnet: a full volume-wiping down/up returned the identical height-0 hash). So genesis can't tell a reset from a healthy chain. Instead the guard pins a CHECKPOINT block ABOVE genesis — (height, hash) — and re-looks-up that height each sync. Two independent reset signals fall out of one lookup:
the pinned height is BlockHashLookup.NotOnChain — the fresh chain is shorter (proven live: an old tip height returned
block: nullon the fresh chain), orthe block at that height carries a DIFFERENT hash — the fresh chain re-climbed past the pin (blocks above genesis carry a per-instance timestamp, so their hashes are instance-specific).
Reliability contract (this path is credibility-critical: a false positive nukes a healthy wallet on PreProd; a false negative leaves the dev staring at a ghost balance / error 171):
checkpoint un-queryable (BlockHashLookup.Unavailable) → ChainStatus.UNKNOWN → SKIP.
first sync for this address → ChainStatus.FIRST_SYNC → pin, no wipe.
checkpoint present, same hash → ChainStatus.SAME → untouched fast path.
checkpoint missing OR different hash, AND wipeOnResetEnabled → ChainStatus.RESET → wipe shielded + dust + unshielded (state, cursors, UTXO rows) + re-pin.
reset detected but NOT wipeOnResetEnabled (remote chain) → LOG only, no wipe, no re-pin. Real chains don't reset; a remote indexer that pruned or briefly rolled back the checkpoint must never nuke a healthy wallet.
Concurrency + crash safety: a Mutex serializes the decision so concurrent syncs can't double-wipe (and the checkpoint lookup runs OUTSIDE the lock so it doesn't serialize every sync on one round-trip). Because the lookup is un-locked, the pin is re-read under the lock and the decision is dropped if another sync already re-pinned in the meantime. The re-pin happens LAST, after the wipes — so a crash mid-wipe leaves the OLD pin in place and the next sync re-detects the reset and re-wipes (the wipes are idempotent), never a half-wiped wallet pinned to the new chain.
Call ensureFreshChain at the START of every wallet sync entry point (shielded, dust, unshielded). Whichever runs first triggers the wipe; the others then find their state gone and run a full re-sync.
Constructors
Functions
Ensure the caches for address belong to the CURRENT chain.