removeForReorg incorrectly evicts valid BIP68 transactions with mempool parents after invalidateblock #35007

issue javierpmateos openend this issue on April 5, 2026
  1. javierpmateos commented at 1:13 pm on April 5, 2026: none

    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.

  2. javierpmateos referenced this in commit 920c6c0d56 on Apr 7, 2026
  3. javierpmateos referenced this in commit 85d744a099 on Apr 7, 2026
  4. javierpmateos referenced this in commit d7fec00f17 on Apr 7, 2026
  5. javierpmateos referenced this in commit c67f4de9b4 on Apr 7, 2026
  6. javierpmateos referenced this in commit a1f29608cc on Apr 7, 2026
  7. javierpmateos referenced this in commit d3ba673a2b on Apr 8, 2026
  8. javierpmateos referenced this in commit 5e23b1a9b1 on Apr 8, 2026
  9. javierpmateos referenced this in commit 9dc0642ac5 on Apr 8, 2026
  10. instagibbs commented at 1:14 pm on April 8, 2026: member

    Didn’t check closely but I think this is related/same: #32838 (comment)

    thanks for filing a proper issue

  11. javierpmateos commented at 3:29 pm on April 8, 2026: none

    Thanks @instagibbs for the pointer! #32838 is indeed related — @sdaftuar’s comment from November describes the same root cause: cached LockPoints with maxInputBlock=genesis (the “tip height + 1” sentinel for mempool parents) become stale after a reorg to a lower chain.

    The reproducer in this issue demonstrates the specific case with invalidateblock, and the fix in #35026 detects this genesis sentinel to force recalculation via CalculateLockPointsAtTip.

    Happy to adjust the approach if there’s a preferred direction from #32838.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-12 09:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me