Skip to content

dash-spv: build a reorg/disconnect pipeline so key-wallet can retract non-chainlocked observed-spent entries #854

Description

@lklimek

Summary

Follow-up from PR #851 (fix for #649). ManagedWalletInfo::observed_spent_outpoints permanently and one-way records every on-chain-observed spend to prevent #649-style out-of-order resurrection. As of PR #851, entries below min(chainlock_height, synced_height) are safely pruned — but entries above that boundary (i.e. spends observed in a block that isn't chain-locked yet) are still permanent, with no retraction path if that block gets reorged out and its spend never re-confirms.

Current failure mode: a spend observed in a non-chain-locked block that is later reorged out, and never re-confirms, permanently hides the funding coin. Balance under-reports silently, forever. Worse: this is currently unrecoverable even by a full rescan — the funding record's history was already destructively compensated, the UTXO is persisted-absent, and once the funding tx (re)appears its redelivery is short-circuited by the wallet's own finalization machinery. The only recovery today is deleting the wallet and re-importing from seed.

On healthy mainnet the exposure window is small (ChainLocks land within ~1 block), but it's unbounded during a ChainLock outage.

Why this needs a dash-spv change first

A chainlock-gated "trust tier" for observed_spent_outpoints (only permanently trust entries at/below the last chain-locked height; retract entries above it on reorg) is architecturally coherent — the boundary, persistence, and promotion machinery already exist in key-wallet. But retraction requires the wallet to be told a reorg happened, and today:

  • WalletInterface (key-wallet-manager/src/wallet_interface.rs) has no disconnect/reorg method — the wallet layer is never notified of a reorg.
  • dash-spv has no operational reorg pipeline: a header-chain continuity break is a hard SyncError::Validation (sync/block_headers/manager.rs); ChainTipManager (chain/chain_tip.rs) is referenced only from test_utils; the storage-level truncate_above (storage/mod.rs) has no production caller.

Proposed follow-up stack (dependency order)

  1. dash-spv reorg pipeline (own design doc): fork detection in header sync, chainlock-gated fork acceptance (never reorg below the chain-locked boundary), truncate_above for headers/filter-headers/filters, rewind of per-wallet synced_height, replay of the replacement branch.
  2. WalletInterface::handle_chain_disconnect(fork_height) — new trait method (default no-op for compatibility), fanned out per wallet by key-wallet-manager.
  3. key-wallet retraction: ManagedWalletInfo::retract_observed_spends_above(fork_height) (debug-assert fork_height >= chainlock boundary — a chain-locked spend must never be retracted), plus reversible compensation (flag spent outputs instead of destructively deleting them, so retraction can undo it) and a provisional undo buffer for guard-removed UTXOs above the boundary.

Context

Full architecture analysis (call-site citations, safety proofs, why LRU/naive eviction was rejected for the related pruning fix) is in the PR #851 review trail — available on request if whoever picks this up wants the detailed writeup rather than re-deriving it.

🤖 Co-authored by Claudius the Magnificent AI Agent

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions