Skip to content

tbc: tx index intermittently loses entries during IBD #1050

Description

@marcopeereboom

Summary

The tx index (TransactionsDB) intermittently loses 't' entries during IBD. The ordinal indexer is the first consumer that reads every tx index entry during wind and surfaces this as a crash.

Evidence

Three different txids missing across three independent fresh-sync runs on testnet4:

  1. f621f096... at block 65569 — "tx not in block" (tx index pointed to wrong block, later resolved itself)
  2. da220b73... at block 81528 — "tx not found" (0 entries in raw LevelDB prefix scan, 1 missing out of 413 txs in the block, 412 are present)
  3. 2a2b5ede... at block 114125 — "tx not found" (entry missing despite all verify checks passing)

Investigation performed

  • Synthetic processTxs simulation: Replayed block 81528's 413 txs through processTxs logic. All 413 entries correctly added to the Go map cache, including the missing txid. No skip, no collision, no duplicate.
  • Key collision check: No TxKey collision between da220b73... and any other entry. 't' prefix (65 bytes) and 's' prefix (69 bytes) cannot collide.
  • Duplicate txid check: No duplicate txids within block 81528.
  • Reorg check: Single block header at each affected height. No reorg evidence. Fresh sync.
  • Post-commit verify: Added a verify step that reads back every 't' txid immediately after BlockTxUpdate commits. The verify PASSES — the entry is readable right after commit. But the entry is missing when the ordinal indexer reads it later (minutes to hours after).
  • Slice vs copy test: Replaced key = k[0:65] slice aliasing in BlockTxUpdate with explicit make + copy. The original da220b73... entry survived with copies, but a DIFFERENT entry (2a2b5ede...) went missing on the same run. The copy fix is inconclusive.
  • Wide-range audit: Scanned blocks 60000-82000 via direct LevelDB read. Found exactly 1 missing tx out of ~22000 blocks.
  • Cross-indexer dependency: The ordinal indexer is the only indexer that reads BlockHashByTxId during wind. The UTXO indexer reads its own OutputsDB via ScriptHashByOutpoint during wind. The tx index data loss may have existed unnoticed before the ordinal indexer was added.

Key observation

The post-commit verify reads back from the same LevelDB handle immediately after BlockTxUpdate returns. This reads from the LevelDB memtable (in-memory), NOT from persisted SSTables. The verify proves the write reached the memtable. It does NOT prove the data survives memtable flush and compaction to disk.

Reproduction

Testnet4 IBD with TBC_ORDINAL_INDEX=true. Hits within the first 120K blocks. Different txid each run. Approximately 1 in 1M+ entries affected.

Batch sizes

The tx indexer cache holds up to 960K entries (96% of 1M capacity) before flushing. Each flush produces a single LevelDB batch with ~960K operations (mix of 't' and 's' entries) committed inside a LevelDB transaction.

Affected code

  • database/tbcd/level/level.goBlockTxUpdate: batch write path
  • service/tbc/txindex.goprocessTxs: cache population

Potential fix

Storing the tx byte offset (TxLoc) in the currently-nil 't' value may surface the issue differently or eliminate it — the current 't' value is nil, and a non-nil value changes the LevelDB write path characteristics. This is speculative and being built as a separate PR regardless (for performance).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions