Skip to content

consensus: skip future duplicate disconnected txids on reorg#167

Draft
l0rinc wants to merge 2 commits into
masterfrom
l0rinc/disconnect-pool-duplicate-txids
Draft

consensus: skip future duplicate disconnected txids on reorg#167
l0rinc wants to merge 2 commits into
masterfrom
l0rinc/disconnect-pool-duplicate-txids

Conversation

@l0rinc
Copy link
Copy Markdown
Owner

@l0rinc l0rinc commented May 10, 2026

Problem

DisconnectedBlockTransactions keeps disconnected transactions in two separate structures, queuedTx and iters_by_txid, which must describe the same txid set (header). Before this change, AddTransactionsFromBlock() inserted into queuedTx before inserting into iters_by_txid, then asserted that the map insertion succeeded (implementation).

That assumption is too strong for historical duplicate coinbase txids. Core already documents the BIP30/BIP34 duplicate-coinbase edge case and the future-consensus-cleanup need around it (validation comments). DisconnectTip() passes the full block transaction vector into the disconnect pool (call site); coinbases are skipped only later when MaybeUpdateMempoolForReorg() drains the pool (mempool resurrection loop). During a chain switch, one disconnect pool is reused while disconnecting all blocks back to the fork point before mempool resurrection (chain switch loop).

Fix

Make duplicate txids no-ops for the disconnect pool by inserting into iters_by_txid first with try_emplace() and appending to queuedTx only for newly queued txids. This keeps the list, map, and cached memory usage synchronized.

The duplicate branch remains a runtime check rather than an assert because historical duplicate coinbase txids are still reachable. BIP54 proposes coinbase nLockTime/nSequence commitments that make future duplicate coinbases impossible after activation (BIP54 specification, rationale), at which point this no-op can be reconsidered as an assert.

Related Implementations

Knots currently has the same list-plus-map shape and insertion-before-map assertion as Core (Knots implementation, storage members/comment).

Bitcoin Cash Node and Bitcoin ABC use a different disconnect-pool container with a boost::multi_index::hashed_unique txid index, so they do not have Core’s list/map divergence failure mode (BCHN current container/accounting, ABC current container). That unique container shape was introduced by f18009a4d63e (BCHN/ABC commit); the explicit duplicate skip in addForBlock() was introduced by 904931e89007 (commit, ABC current skip). BCHN later made memory accounting defensive on successful insert in fa0d374801ad (commit).

It's not obvious what libbitcoin does, but the reorg code I found is chaser/archive based (confirm reorg, set reorganized, organize path), and I did not find a Core-style DisconnectedBlockTransactions analogue to cite as prior art.

l0rinc added 2 commits May 10, 2026 00:16
feature_block.py already builds a valid duplicate coinbase at height 120 after spending the earlier output at height 119.

Extend that path with an equal-work fork from genesis so the test documents that the duplicate coinbase branch remains active until a stronger branch forces a disconnect.

This prepares the reorg reproducer without changing behavior yet.
DisconnectedBlockTransactions stores transactions in a list and indexes the same entries by txid.

A duplicate txid encountered while disconnecting a stronger genesis fork after a spent BIP30 duplicate coinbase is inserted into the list before the map detects the duplicate, so debug builds abort at the inserted assertion and non-assert builds can leave the list and map out of sync.

Use the txid index insertion result to detect duplicates before appending to the list, so the second copy is ignored and the list and map stay in sync.

Add direct unit coverage for the list/map invariant, then update feature_block.py to turn the existing BIP30 duplicate coinbase setup into a stronger genesis-fork reorg.

The functional test invalidates that fork afterward so the rest of the test continues from the duplicate branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant