Repository: bitcoin/bitcoin
File: test/functional/mempool_reorg.py (Python)

Code before:
        spend_2_1_id = self.nodes[0].sendrawtransaction(spend_2_1['hex'])

        assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, timelock_tx_id})
        self.sync_all()

        self.log.info("invalidate the last block")
        for node in self.nodes:
            node.invalidateblock(last_block[0])
        self.log.info("The time-locked transaction is now too immature and has been removed from the mempool")
        self.log.info("spend_3_1 has been re-orged out of the chain and is back in the mempool")
        assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, spend_3_1_id})

        self.log.info("Use invalidateblock to re-org back and make all those coinbase spends immature/invalid")
        b = self.nodes[0].getblockhash(first_block + 100)
        for node in self.nodes:
            node.invalidateblock(b)

        self.log.info("Check that the mempool is empty")
        assert_equal(set(self.nodes[0].getrawmempool()), set())
        self.sync_all()

        self.test_reorg_relay()


if __name__ == '__main__':
    MempoolCoinbaseTest(__file__).main()


Review comment (bug):
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 https://github.com/bitcoin/bitcoin/issues/32838

Let me think about how to best do this test case and enhancing it possibly

Code after:
        assert_raises_rpc_error(-26, 'non-final', self.nodes[0].sendrawtransaction, timelock_tx)

        self.log.info("Create spend_2_1 and spend_3_1")
        spend_2_1 = wallet.create_self_transfer(utxo_to_spend=spend_2["new_utxo"])
        spend_3_1 = wallet.create_self_transfer(utxo_to_spend=spend_3["new_utxo"])

        self.log.info("Broadcast and mine spend_3_1")
        spend_3_1_id = self.nodes[0].sendrawtransaction(spend_3_1['hex'])
        self.log.info("Generate a block")

        # Prep for fork, only go FORK_LENGTH  seconds into the MTP future max
        fork_blocks = create_empty_fork(self.nodes[0], fork_length=FORK_LENGTH)

        # Jump node and MTP 300 seconds and generate a slightly weaker chain than reorg one
        self.nodes[0].setmocktime(future)
        self.generate(self.nodes[0], FORK_LENGTH - 1)
        block_time = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['time']
        assert(block_time >= now + 300)

        # generate() implicitly syncs blocks, so that peer 1 gets the block before timelock_tx
        # Otherwise, peer 1 would put the timelock_tx in m_lazy_recent_rejects
        self.log.info("The time-locked transaction can now be spent")
        timelock_tx_id = self.nodes[0].sendrawtransaction(timelock_tx)

        self.log.info("Add spend_1 and spend_2_1 to the mempool")
        spend_1_id = self.nodes[0].sendrawtransaction(spend_1['hex'])
        spend_2_1_id = self.nodes[0].sendrawtransaction(spend_2_1['hex'])

        assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, timelock_tx_id})
        self.sync_all()

        self.trigger_reorg(fork_blocks, self.nodes[0])
        self.sync_blocks()

        # We went backwards in time to boot timelock_tx_id
        fork_block_time = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['time']
        assert fork_block_time < block_time

        self.log.info("The time-locked transaction is now too immature and has been removed from the mempool")
        self.log.info("spend_3_1 has been re-orged out of the chain and is back in the mempool")
        assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, spend_3_1_id})

        self.log.info("Use invalidateblock to re-org back and make all those coinbase spends immature/invalid")
        b = self.nodes[0].getblockhash(first_block + 100)

        # Use invalidateblock to go backwards in MTP time.
        # invalidateblock actually moves MTP backwards, making timelock_tx_id valid again.
        for node in self.nodes:
            node.invalidateblock(b)

        self.log.info("Check that the mempool is empty")