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:
f621f096... at block 65569 — "tx not in block" (tx index pointed to wrong block, later resolved itself)
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)
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.go — BlockTxUpdate: batch write path
service/tbc/txindex.go — processTxs: 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).
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:
f621f096...at block 65569 — "tx not in block" (tx index pointed to wrong block, later resolved itself)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)2a2b5ede...at block 114125 — "tx not found" (entry missing despite all verify checks passing)Investigation performed
processTxssimulation: Replayed block 81528's 413 txs throughprocessTxslogic. All 413 entries correctly added to the Go map cache, including the missing txid. No skip, no collision, no duplicate.TxKeycollision betweenda220b73...and any other entry. 't' prefix (65 bytes) and 's' prefix (69 bytes) cannot collide.BlockTxUpdatecommits. 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).key = k[0:65]slice aliasing inBlockTxUpdatewith explicitmake + copy. The originalda220b73...entry survived with copies, but a DIFFERENT entry (2a2b5ede...) went missing on the same run. The copy fix is inconclusive.BlockHashByTxIdduring wind. The UTXO indexer reads its ownOutputsDBviaScriptHashByOutpointduring 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
BlockTxUpdatereturns. 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.go—BlockTxUpdate: batch write pathservice/tbc/txindex.go—processTxs: cache populationPotential 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 isnil, and a non-nil value changes the LevelDB write path characteristics. This is speculative and being built as a separate PR regardless (for performance).