While testing v31.0rc2 cluster mempool changes (rc testing for #34840), I ran into unexpected transaction loss during multi-block reorgs using invalidateblock. After isolating the issue I found it also reproduces on v30.2, so it predates cluster mempool.
What happens
A transaction with nSequence=0 (BIP68 relative lock-time, value 0) and an unconfirmed parent gets silently removed by removeForReorg when the chain tip decreases via invalidateblock. The transaction is valid — sendrawtransaction accepts it immediately after removal.
Reproducer
Minimal test attached: https://gist.github.com/javierpmateos/c55d365973adbf488a852dc5e0b77dec python3 test/functional/mempool_reorg_bip68_bug.py –configfile build/test/config.ini
Two identical children spend mempool parents. The only difference is nSequence:
- child_A (0xFFFFFFFE, BIP68 disabled) → survives
- child_B (0x00000000, BIP68 enabled) → removed
- sendrawtransaction(child_B) → accepted (removal was wrong)
Tested on v31.0rc2 (f0e2cbc5) and v30.2.
Root cause
When child_B enters the mempool at tip H with a mempool parent, CalculatePrevHeights assigns nCoinHeight=H+1 (assumes parent confirms next block). With nSequence=0, CalculateSequenceLocks gives nMinHeight=H. This is cached as LockPoints{min_height=H, maxInputBlock=genesis}.
After invalidateblock drops the tip to H-1, removeForReorg calls filter_final_and_mature which calls TestLockPointValidity. Since maxInputBlock=genesis (always on chain), it returns true and the stale cached lockpoints are reused without recalculation. CheckSequenceLocksAtTip then evaluates min_height(H) >= next_block(H) → true → lock fails → tx removed with all descendants.
A fresh computation at the new tip would give min_height=H-1 < H → lock satisfied → tx should stay.
Scope
- Only nSequence=0 exactly is affected (seq=1+ is rejected as non-BIP68-final at mempool entry with mempool parents)
- Bitcoin Core wallet uses 0xFFFFFFFD/0xFFFFFFFE → not affected
- createrawtransaction defaults to 0xFFFFFFFD → not affected
- Protocols using relative-locktime=0 with unconfirmed inputs could be affected (e.g. some DLC constructions)
- Natural P2P reorgs are not affected because removeForReorg runs at the new (higher) tip after all blocks are connected
- reconsiderblock does not restore the removed transactions
Possible fix
In TestLockPointValidity, detect the genesis sentinel that indicates mempool-parent inputs and force recalculation:
0if (lp.maxInputBlock->nHeight == 0 && lp.height > 0) return false;
This is the same class of issue fixed for TRUC in #33504.