Skip to content

bug: out-of-order block processing causes SPV wallet to miss UTXO spends #649

@lklimek

Description

@lklimek

Summary

When the SPV block processing pipeline processes blocks out of height order, UTXO spends are silently missed. If a UTXO is created in block H1 and spent in block H2 (H2 > H1), but H2 is processed before H1, the wallet never removes the spent UTXO — it appears as permanently spendable.

Impact

  • Stale UTXOs: The wallet retains UTXOs that were already spent on-chain, showing an inflated balance.
  • Failed broadcasts: Transactions built from stale UTXOs are silently rejected by peers as double-spends. The wallet reports "broadcast successfully" but the tx never enters mempools or gets mined.
  • Cascading failures: Any flow that depends on spendable balance after a payment hangs indefinitely.

Root Cause

Compact block filter matching (check_compact_filters_for_addresses in key-wallet-manager/src/matching.rs) correctly identifies blocks containing wallet-relevant transactions. However, blocks are downloaded and processed in non-deterministic order — the download pipeline doesn't enforce ascending height ordering.

Observed in trace logs

Block 1458841 creates a UTXO for address yYTQPB4A... (received 2.39 DASH):

10:30:50.117596 New wallet transaction detected txid=706a9619... context=block 1458841
                net_change=-30000226 received=239274892 sent=269275118

Block 1458904 spends that UTXO (sends to external address yV8Bj8Td...):

10:30:50.069353 New wallet transaction detected txid=c8f671e5... context=block 1458904
                net_change=0 received=0 sent=0

Block 1458904 was processed 48ms BEFORE block 1458841. When the spending tx was checked against the wallet, the UTXO didn't exist yet (sent=0). The creating block was processed later and added the UTXO, but the spend was already missed.

Secondary concern: filter matching completeness

The compact filter matching function only queries GCS filters with output scriptPubKeys. Per BIP158, the basic filter also contains prevout scriptPubKeys for non-coinbase inputs. If Dash Core follows this (which it appears to, since the spending block was downloaded), the filter matching works. However, check_compact_filters_for_addresses should ideally also include watched_outpoints() serialized as bytes for defense-in-depth, similar to how the mempool bloom filter (dash-spv/src/sync/mempool/filter.rs) already does.

Steps to Reproduce

  1. Create a wallet and receive funds (UTXO created at height H1)
  2. Spend the UTXO from a different environment — confirmed at height H2 > H1
  3. Restart the SPV wallet and let it sync past H2
  4. Both H1 and H2 match the compact filter (our address appears in both blocks)
  5. If H2 is downloaded/processed before H1, the spend is missed
  6. The wallet still shows the UTXO as spendable

Verified on Dash testnet:

  • UTXO 706a9619...22bd6a45:1 — created at block 1458841
  • Spent by c8f671e5...1225c5bb — confirmed at block 1458904 (528 confirmations)
  • SPV processed block 1458904 at 10:30:50.069 and block 1458841 at 10:30:50.117 — 48ms out of order

Proposed Fix

Option A (block pipeline): Enforce ascending height order in the block download/processing pipeline. Blocks should be processed strictly in height order so that UTXOs are always created before they can be spent.

Option B (wallet checker): After processing all matched blocks in a batch, do a second pass to reconcile: for any UTXO that was created, check if a later block in the same batch spent it. The spent_outpoints tracking in ManagedAccount already handles some out-of-order cases (it skips UTXO creation if the outpoint is in spent_outpoints), but this relies on the spending tx being processed first, which sets sent=0 when the UTXO doesn't exist yet.

Option C (retroactive detection): When a creating transaction is processed and adds a UTXO, check if any already-processed transaction in spent_outpoints references this outpoint. If so, immediately remove the UTXO.

Option A is the cleanest but may have performance implications. Option C is the most targeted fix.

🤖 Co-authored by Claudius the Magnificent AI Agent

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions