test: Fix reorg patterns in tests to use proper fork-based approach #32587

pull yuvicc wants to merge 2 commits into bitcoin:master from yuvicc:2025-05-update_test_reorg_behaviour changing 7 files +171 −62
  1. yuvicc commented at 1:26 pm on May 22, 2025: contributor

    Updated functional tests to replace direct use of invalidateblock with proper fork-based reorg behaviour. The direct invalidation approach bypasses important validation checks and has depth limitations(10 block) that don’t match real-world reorg scenarios. For more details see #32531.

    Fixes #32531

  2. DrahtBot commented at 1:26 pm on May 22, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32587.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK Prabhat1308, BrandonOdiwuor
    Stale ACK instagibbs

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #32868 (test: refactor: overhaul block hash determination for CBlock{,Header} objects by theStack)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • slighty -> slightly [correct spelling in comment “slighty weaker chain”]

    drahtbot_id_4_m

  3. instagibbs commented at 1:27 pm on May 22, 2025: member
    feel free to ping me when it’s ready
  4. yuvicc commented at 1:30 pm on May 22, 2025: contributor

    feel free to ping me when it’s ready

    Sure!

  5. yuvicc force-pushed on May 23, 2025
  6. yuvicc force-pushed on May 30, 2025
  7. yuvicc force-pushed on May 30, 2025
  8. DrahtBot added the label CI failed on May 30, 2025
  9. DrahtBot commented at 6:35 am on May 30, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/runs/43164565056 LLM reason (✨ experimental): The CI failure is primarily due to errors from the lint check, specifically from ruff, which identified and flagged Python code issues such as unused imports and trailing whitespace.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  10. yuvicc force-pushed on Jun 2, 2025
  11. DrahtBot removed the label CI failed on Jun 2, 2025
  12. yuvicc marked this as ready for review on Jun 3, 2025
  13. yuvicc commented at 7:10 am on June 3, 2025: contributor
    @instagibbs can you review this, will add other tests once this one gets ACK’ed
  14. in test/functional/test_framework/blocktools.py:121 in b70c907bc2 outdated
    116+        Creates a fork using first node's chaintip as the starting point.
    117+        Returns a list of blocks to submit in order.
    118+    '''
    119+    tip = int(self.nodes[0].getbestblockhash(), 16)
    120+    height = self.nodes[0].getblockcount()
    121+    block_time = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['time'] + 1
    


    maflcko commented at 7:44 am on June 3, 2025:
    passing self into a standalone function doesn’t really make sense. This should just be the node

    yuvicc commented at 8:10 am on June 3, 2025:
    got it!
  15. yuvicc renamed this:
    [WIP] test: Fix reorg patterns in mempool tests to use proper fork-based approach
    test: Fix reorg patterns in mempool tests to use proper fork-based approach
    on Jun 3, 2025
  16. DrahtBot added the label Tests on Jun 3, 2025
  17. yuvicc force-pushed on Jun 4, 2025
  18. in test/functional/mempool_ephemeral_dust.py:335 in 9565415592 outdated
    330@@ -323,49 +331,68 @@ def test_reorgs(self):
    331 
    332         # Get dusty tx mined, then check that it makes it back into mempool on reorg
    333         # due to bypass_limits allowing 0-fee individually
    334+
    335+        # Prep for fork with empty blocks to not use invalidateblock directly
    


    Prabhat1308 commented at 12:04 pm on June 9, 2025:
    0        # Prep for fork with empty blocks
    

    I think this should be included in the PR description or commit description rather than in the code. We would not like to refer to a old function we used here which anyway could be easily found if we backtracked to the PR.


    yuvicc commented at 10:33 am on June 10, 2025:
    Agree, I had the same thought! I will remove that for now!
  19. in test/functional/mempool_ephemeral_dust.py:350 in 9565415592 outdated
    347         # Create a sweep that has dust of its own and leaves dusty_tx's dust unspent
    348         sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)
    349         self.add_output_to_create_multi_result(sweep_tx)
    350         assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, sweep_tx["hex"])
    351 
    352+        # Prep for fork with empty blocks to not use invalidateblock directly
    


    Prabhat1308 commented at 12:04 pm on June 9, 2025:
    same here
  20. Prabhat1308 commented at 12:04 pm on June 9, 2025: contributor
    ACK with the approach . Left some nits
  21. in test/functional/test_framework/blocktools.py:119 in 3e5f125968 outdated
    113@@ -114,6 +114,26 @@ def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl
    114     block.calc_sha256()
    115     return block
    116 
    117+def create_empty_fork(node, fork_length):
    118+    '''
    119+        Creates a fork using first node's chaintip as the starting point.
    


    instagibbs commented at 1:07 pm on June 9, 2025:
    not “first” node anymore
  22. in test/functional/mempool_ephemeral_dust.py:339 in 9565415592 outdated
    330@@ -323,49 +331,68 @@ def test_reorgs(self):
    331 
    332         # Get dusty tx mined, then check that it makes it back into mempool on reorg
    333         # due to bypass_limits allowing 0-fee individually
    334+
    335+        # Prep for fork with empty blocks to not use invalidateblock directly
    336+        fork_blocks = create_empty_fork(self.nodes[0], fork_length=10)
    


    instagibbs commented at 1:09 pm on June 9, 2025:
    if fork length is always 10 because it’s sufficient and easy, just make a constant like FORK_LENGTH with a quick comment to describe what it’s for

    yuvicc commented at 10:34 am on June 10, 2025:
    Correct!

    yuvicc commented at 10:34 am on June 10, 2025:
    Correct!
  23. yuvicc force-pushed on Jun 10, 2025
  24. yuvicc commented at 1:39 pm on June 10, 2025: contributor
    Addressed some nits and rebased with master!
  25. yuvicc force-pushed on Jun 10, 2025
  26. yuvicc force-pushed on Jun 10, 2025
  27. in test/functional/mempool_ephemeral_dust.py:76 in c98f3615e7 outdated
    69@@ -64,6 +70,11 @@ def create_ephemeral_dust_package(self, *, tx_version, dust_tx_fee=0, dust_value
    70 
    71         return dusty_tx, sweep_tx
    72 
    73+    def trigger_reorg(self, fork_blocks):
    74+        """Trigger reorg of the fork blocks."""
    75+        for block in fork_blocks:
    76+            self.nodes[0].submitblock(block.serialize().hex())
    


    instagibbs commented at 12:47 pm on June 11, 2025:
    could assert that the node’s new chaintip indeed is the last submitted block to avoid regressions in the test

    yuvicc commented at 5:12 pm on June 15, 2025:
    Done
  28. instagibbs commented at 12:48 pm on June 11, 2025: member

    LGTM c98f3615e7eb6774b8f49fe7b2aeed8c056622c7

    feel free to add more cases

  29. yuvicc force-pushed on Jun 15, 2025
  30. yuvicc renamed this:
    test: Fix reorg patterns in mempool tests to use proper fork-based approach
    test: Fix reorg patterns in tests to use proper fork-based approach
    on Jun 16, 2025
  31. yuvicc force-pushed on Jun 18, 2025
  32. yuvicc force-pushed on Jun 18, 2025
  33. DrahtBot added the label CI failed on Jun 18, 2025
  34. DrahtBot commented at 7:26 am on June 18, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/runs/44317225556 LLM reason (✨ experimental): Lint errors caused the CI failure.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  35. yuvicc force-pushed on Jun 18, 2025
  36. DrahtBot removed the label CI failed on Jun 18, 2025
  37. yuvicc force-pushed on Jun 19, 2025
  38. BrandonOdiwuor commented at 2:04 pm on June 20, 2025: contributor
    Concept ACK in favor of replacing direct invalidateblock usage with a proper fork-based approach for simulating reorgs in tests, as this better reflects realistic network behavior
  39. yuvicc force-pushed on Jun 22, 2025
  40. yuvicc force-pushed on Jun 23, 2025
  41. DrahtBot added the label CI failed on Jun 23, 2025
  42. DrahtBot commented at 8:59 am on June 23, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/runs/44585514502 LLM reason (✨ experimental): Lint check failed due to style and whitespace errors in the Python code.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  43. yuvicc force-pushed on Jun 23, 2025
  44. yuvicc commented at 9:13 am on June 23, 2025: contributor

    While fixing the reorg pattern in mempool_truc.py test, I observed an interesting behavior. During reorg the test_2child_rbf test fails when using fork blocks(10) pattern, which later found was due to 1-child policy during mempool re-entry after a reorg. Basically the parent txn was mined in a block, later in next block 2 child txns were mined and after a reorg, the 1-child policy holds true here, but when using invalidateblock() this doesn’t hold true.

    For now, I have removed the assertion for ancestor_tx to exceed the 1-child limit as it doesn’t happened during re-org testing using fork block pattern.

    Not sure if I am correct here, it would be helpful for me if someone could review the commit https://github.com/bitcoin/bitcoin/pull/32587/commits/33e9f3fd1bcdfc79eecfe377a608113f1bda8bae. If this is the correct behavior then adding a check for 1-child limit during reorg would be better IMO.

    cc @glozow

  45. yuvicc requested review from instagibbs on Jun 23, 2025
  46. DrahtBot removed the label CI failed on Jun 23, 2025
  47. glozow commented at 9:36 pm on June 23, 2025: member

    Basically the parent txn was mined in a block, later in next block 2 child txns were mined and after a reorg, the 1-child policy holds true here, but when using invalidateblock() this doesn’t hold true.

    I haven’t reviewed in depth, but I don’t think the children are supposed to be mined and then reorged, only the parent is. The children should already be in mempool when the disconnectpool is being resubmitted. When the parent comes in, we don’t check it for TRUC children limits (the implementation assumes no in-mempool children which is true in all cases except reorgs), which is why we end up with 1p2c.

  48. yuvicc commented at 4:16 pm on June 24, 2025: contributor

    I haven’t reviewed in depth, but I don’t think the children are supposed to be mined and then reorged, only the parent is.

    Agree! I shall change the test to match this!

  49. move the method create_empty_fork to test_framework (blocktools.py) so that it can be used by other tests as well 8fa8fa751a
  50. yuvicc referenced this in commit 89f3866830 on Jun 26, 2025
  51. yuvicc force-pushed on Jun 26, 2025
  52. yuvicc referenced this in commit 3b0727ba71 on Jun 26, 2025
  53. yuvicc force-pushed on Jun 26, 2025
  54. DrahtBot added the label CI failed on Jun 26, 2025
  55. DrahtBot commented at 2:49 pm on June 26, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/runs/44857399027 LLM reason (✨ experimental): The CI failure is caused by errors detected during the Python linting step, specifically unused imports and an unused variable, which are treated as errors.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  56. yuvicc referenced this in commit 0feecd78e5 on Jun 26, 2025
  57. yuvicc force-pushed on Jun 26, 2025
  58. yuvicc commented at 3:13 pm on June 26, 2025: contributor
    Ready for review!
  59. yuvicc requested review from BrandonOdiwuor on Jun 26, 2025
  60. yuvicc requested review from maflcko on Jun 26, 2025
  61. in test/functional/mempool_reorg.py:196 in b40447e911 outdated
    195-            node.invalidateblock(last_block[0])
    196-        self.log.info("The time-locked transaction is now too immature and has been removed from the mempool")
    197+        self.trigger_reorg(fork_blocks, self.nodes[0])
    198+        self.sync_blocks()
    199+
    200+        # self.log.info("The time-locked transaction is now too immature and has been removed from the mempool")
    


    instagibbs commented at 4:31 pm on June 26, 2025:
    accidental comment?
  62. in test/functional/mempool_truc.py:490 in 0feecd78e5 outdated
    490-        self.generate(node, 1)
    491-        self.check_mempool([])
    492-
    493         # Create a reorg, causing ancestor_tx to exceed the 1-child limit
    494-        node.invalidateblock(block)
    495+        self.trigger_reorg(fork_blocks)
    


    instagibbs commented at 4:42 pm on June 26, 2025:
    this changes behavior a bit but should give the coverage we want still, as all we care about is getting 1p2c into mempool with TRUC

    instagibbs commented at 1:34 pm on June 27, 2025:
    @yuvicc can you un-resolve this? I’d like other reviewers to see it and yell at me if I’m wrong :)
  63. instagibbs approved
  64. instagibbs commented at 4:43 pm on June 26, 2025: member

    ACK 0feecd78e52aede26c07391b9f361410e6a8a4ad

    though I think the last two commits can be squashed

  65. DrahtBot requested review from Prabhat1308 on Jun 26, 2025
  66. DrahtBot removed the label CI failed on Jun 26, 2025
  67. yuvicc referenced this in commit eb330b58d8 on Jun 27, 2025
  68. yuvicc force-pushed on Jun 27, 2025
  69. yuvicc commented at 5:43 am on June 27, 2025: contributor

    though I think the last two commits can be squashed

    Done!

  70. instagibbs approved
  71. instagibbs commented at 1:35 pm on June 27, 2025: member

    reACK eb330b58d8d20bb4a5002906cb48bb03c5fa1a10

    squashes and a couple errant comments deleted

  72. in test/functional/mempool_reorg.py:197 in eb330b58d8 outdated
    196+        self.sync_blocks()
    197+
    198         self.log.info("The time-locked transaction is now too immature and has been removed from the mempool")
    199         self.log.info("spend_3_1 has been re-orged out of the chain and is back in the mempool")
    200-        assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, spend_3_1_id})
    201+        assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, spend_3_1_id, timelock_tx_id})
    


    instagibbs commented at 12:39 pm on July 1, 2025:

    actually this is wrong, the test is checking that timelock_tx_id is not in mempool. If we don’t use invalidateblock it’s essentially impossible to trigger this behavior since the chain should not move backwards in height ever.

    I re-read this PR after writing up #32838

    Let me think about how to best do this test case and enhancing it possibly


    instagibbs commented at 8:53 pm on July 1, 2025:
    It can move backwards in height if reorg goes across difficulty adustments (impossible on regtest) or if we mine blocks with varying MTP and use time-based timelock on the tx

    instagibbs commented at 2:18 pm on July 2, 2025:
    Given that the coinbase maturity tests also use invalidateblock, let’s just keep this particular test untouched for now. This test setup is going to have to be a bit more advanced and can be done in a follow-up

    instagibbs commented at 2:48 pm on July 2, 2025:

    actually ended up being pretty easy to remove for a time-based timelock

     0diff --git a/test/functional/mempool_reorg.py b/test/functional/mempool_reorg.py
     1index 3906117e8b..0f6124f9ab 100755
     2--- a/test/functional/mempool_reorg.py
     3+++ b/test/functional/mempool_reorg.py
     4@@ -28,5 +28,6 @@ from test_framework.blocktools import (
     5 
     6 # Number of blocks to create in temporary blockchain branch for reorg testing
     7-FORK_LENGTH = 10
     8+# Needs to be long enough to allow MTP to move arbitrarily forward
     9+FORK_LENGTH = 20
    10 
    11 class MempoolCoinbaseTest(BitcoinTestFramework):
    12@@ -125,4 +126,8 @@ class MempoolCoinbaseTest(BitcoinTestFramework):
    13         wallet = self.wallet
    14 
    15+        # Prevent clock from moving blocks further forward in time
    16+        now = int(time.time())
    17+        self.nodes[0].setmocktime(now)
    18+
    19         # Start with a 200 block chain
    20         assert_equal(self.nodes[0].getblockcount(), 200)
    21@@ -148,9 +153,11 @@ class MempoolCoinbaseTest(BitcoinTestFramework):
    22         spend_3 = wallet.create_self_transfer(utxo_to_spend=utxo_3)
    23 
    24-        self.log.info("Create another transaction which is time-locked to two blocks in the future")
    25+        future = now + 300
    26+
    27+        self.log.info("Create another transaction which is time-locked to 300 seconds in the future")
    28         utxo = wallet.get_utxo(txid=coinbase_txids[0])
    29         timelock_tx = wallet.create_self_transfer(
    30             utxo_to_spend=utxo,
    31-            locktime=self.nodes[0].getblockcount() + 2,
    32+            locktime=future,
    33         )['hex']
    34 
    35@@ -174,7 +181,13 @@ class MempoolCoinbaseTest(BitcoinTestFramework):
    36         self.log.info("Generate a block")
    37 
    38-        # Prep for fork
    39+        # Prep for fork, only go FORK_LENGTH  seconds into the MTP future max
    40         fork_blocks = create_empty_fork(self.nodes[0], fork_length=FORK_LENGTH)
    41-        self.generate(self.nodes[0], 1)
    42+
    43+        # Jump node and MTP 300 seconds and generate a slighty weaker chain than reorg one
    44+        self.nodes[0].setmocktime(future)
    45+        self.generate(self.nodes[0], FORK_LENGTH - 1)
    46+        block_time = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['time']
    47+        assert(block_time >= now + 300)
    48+
    49         # generate() implicitly syncs blocks, so that peer 1 gets the block before timelock_tx
    50         # Otherwise, peer 1 would put the timelock_tx in m_lazy_recent_rejects
    51@@ -193,7 +206,11 @@ class MempoolCoinbaseTest(BitcoinTestFramework):
    52         self.sync_blocks()
    53 
    54+        # We went backwards in time to boot timelock_tx_id
    55+        fork_block_time = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['time']
    56+        assert fork_block_time < block_time
    57+
    58         self.log.info("The time-locked transaction is now too immature and has been removed from the mempool")
    59         self.log.info("spend_3_1 has been re-orged out of the chain and is back in the mempool")
    60-        assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, spend_3_1_id, timelock_tx_id})
    61+        assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, spend_3_1_id})
    62 
    63         self.log.info("Use invalidateblock to re-org back and make all those coinbase spends immature/invalid")
    
  73. yuvicc force-pushed on Jul 4, 2025
  74. yuvicc requested review from instagibbs on Jul 4, 2025
  75. yuvicc force-pushed on Jul 4, 2025
  76. DrahtBot added the label CI failed on Jul 4, 2025
  77. DrahtBot commented at 5:07 am on July 4, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/runs/45340830150 LLM reason (✨ experimental): The CI failure is caused by errors detected during the lint check, specifically a whitespace issue on a blank line.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  78. yuvicc commented at 5:23 am on July 4, 2025: contributor
    Addressed @instagibbs suggestion to use time-based timelock instead of n block method.
  79. DrahtBot removed the label CI failed on Jul 4, 2025
  80. fix: reorg behaviour in mempool tests to match real one 4ddc577953
  81. in test/functional/mempool_reorg.py:185 in 01b98dc05c outdated
    177@@ -160,10 +178,18 @@ def run_test(self):
    178         self.log.info("Broadcast and mine spend_3_1")
    179         spend_3_1_id = self.nodes[0].sendrawtransaction(spend_3_1['hex'])
    180         self.log.info("Generate a block")
    181-        last_block = self.generate(self.nodes[0], 1)
    182+
    183+        # Prep for fork, only go FORK_LENGTH  seconds into the MTP future max
    184+        fork_blocks = create_empty_fork(self.nodes[0], fork_length=FORK_LENGTH)
    185+
    186+        # Jump node and MTP 300 seconds and generate a slighty weaker chain than reorg one
    


    DrahtBot commented at 7:46 am on July 4, 2025:
    in mempool_reorg.py: “slighty” -> “slightly” [correct spelling]
    
  82. yuvicc force-pushed on Jul 5, 2025

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: 2025-07-05 21:12 UTC

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