#!/usr/bin/env python3
# Copyright (c) 2024-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from decimal import Decimal

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
    assert_not_equal,
    assert_equal,
    assert_greater_than,
    assert_greater_than_or_equal,
    assert_raises_rpc_error,
    get_fee,
)
from test_framework.wallet import (
    COIN,
    DEFAULT_FEE,
    MiniWallet,
)
from test_framework.blocktools import (
    create_empty_fork,
)

MAX_REPLACEMENT_CANDIDATES = 100
TRUC_MAX_VSIZE = 10000
TRUC_CHILD_MAX_VSIZE = 1000

def cleanup(extra_args=None):
    def decorator(func):
        def wrapper(self):
            try:
                if extra_args is not None:
                    self.restart_node(0, extra_args=extra_args)
                func(self)
            finally:
                # Clear mempool again after test
                self.generate(self.nodes[0], 1)
                if extra_args is not None:
                    self.restart_node(0)
        return wrapper
    return decorator

class MempoolTRUC(BitcoinTestFramework):
    def set_test_params(self):
        self.num_nodes = 1
        self.extra_args = [[]]
        self.setup_clean_chain = True

    def check_mempool(self, txids):
        """Assert exact contents of the node's mempool (by txid)."""
        mempool_contents = self.nodes[0].getrawmempool()
        assert_equal(len(txids), len(mempool_contents))
        assert all([txid in txids for txid in mempool_contents])

    def trigger_reorg(self, fork_blocks):
        """Trigger reorg of the fork blocks."""
        for block in fork_blocks:
            self.nodes[0].submitblock(block.serialize().hex())
        assert_equal(self.nodes[0].getbestblockhash(), fork_blocks[-1].hash_hex)

    @cleanup()
    def test_truc_max_vsize(self):
        node = self.nodes[0]
        self.log.info("Test TRUC-specific maximum transaction vsize")
        tx_v3_heavy = self.wallet.create_self_transfer(target_vsize=TRUC_MAX_VSIZE + 1, version=3)
        assert_greater_than_or_equal(tx_v3_heavy["tx"].get_vsize(), TRUC_MAX_VSIZE)
        expected_error_heavy = f"TRUC-violation, version=3 tx {tx_v3_heavy['txid']} (wtxid={tx_v3_heavy['wtxid']}) is too big"
        assert_raises_rpc_error(-26, expected_error_heavy, node.sendrawtransaction, tx_v3_heavy["hex"])
        self.check_mempool([])

        # Ensure we are hitting the TRUC-specific limit and not something else
        tx_v2_heavy = self.wallet.send_self_transfer(from_node=node, target_vsize=TRUC_MAX_VSIZE + 1, version=2)
        self.check_mempool([tx_v2_heavy["txid"]])

    @cleanup()
    def test_truc_acceptance(self):
        node = self.nodes[0]
        self.log.info("Test a child of a TRUC transaction cannot be more than 1000vB")
        tx_v3_parent_normal = self.wallet.send_self_transfer(from_node=node, version=3)
        self.check_mempool([tx_v3_parent_normal["txid"]])
        tx_v3_child_heavy = self.wallet.create_self_transfer(
            utxo_to_spend=tx_v3_parent_normal["new_utxo"],
            target_vsize=TRUC_CHILD_MAX_VSIZE + 1,
            version=3
        )
        assert_greater_than_or_equal(tx_v3_child_heavy["tx"].get_vsize(), TRUC_CHILD_MAX_VSIZE)
        expected_error_child_heavy = f"TRUC-violation, version=3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big"
        assert_raises_rpc_error(-26, expected_error_child_heavy, node.sendrawtransaction, tx_v3_child_heavy["hex"])
        self.check_mempool([tx_v3_parent_normal["txid"]])
        # tx has no descendants
        assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 1)

        self.log.info("Test that, during replacements, only the new transaction counts for TRUC descendant limit")
        tx_v3_child_almost_heavy = self.wallet.send_self_transfer(
            from_node=node,
            fee_rate=DEFAULT_FEE,
            utxo_to_spend=tx_v3_parent_normal["new_utxo"],
            target_vsize=TRUC_CHILD_MAX_VSIZE - 3,
            version=3
        )
        assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_almost_heavy["tx"].get_vsize())
        self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy["txid"]])
        assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2)
        tx_v3_child_almost_heavy_rbf = self.wallet.send_self_transfer(
            from_node=node,
            fee_rate=DEFAULT_FEE * 2,
            utxo_to_spend=tx_v3_parent_normal["new_utxo"],
            target_vsize=875,
            version=3
        )
        assert_greater_than_or_equal(tx_v3_child_almost_heavy["tx"].get_vsize() + tx_v3_child_almost_heavy_rbf["tx"].get_vsize(),
                                     TRUC_CHILD_MAX_VSIZE)
        self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy_rbf["txid"]])
        assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2)

    @cleanup(extra_args=None)
    def test_truc_replacement(self):
        node = self.nodes[0]
        self.log.info("Test TRUC transactions may be replaced by TRUC transactions")
        utxo_v3_bip125 = self.wallet.get_utxo()
        tx_v3_bip125 = self.wallet.send_self_transfer(
            from_node=node,
            fee_rate=DEFAULT_FEE,
            utxo_to_spend=utxo_v3_bip125,
            version=3
        )
        self.check_mempool([tx_v3_bip125["txid"]])

        tx_v3_bip125_rbf = self.wallet.send_self_transfer(
            from_node=node,
            fee_rate=DEFAULT_FEE * 2,
            utxo_to_spend=utxo_v3_bip125,
            version=3
        )
        self.check_mempool([tx_v3_bip125_rbf["txid"]])

        self.log.info("Test TRUC transactions may be replaced by non-TRUC (BIP125) transactions")
        tx_v3_bip125_rbf_v2 = self.wallet.send_self_transfer(
            from_node=node,
            fee_rate=DEFAULT_FEE * 3,
            utxo_to_spend=utxo_v3_bip125,
            version=2
        )
        self.check_mempool([tx_v3_bip125_rbf_v2["txid"]])

        self.log.info("Test that replacements cannot cause violation of inherited TRUC")
        utxo_v3_parent = self.wallet.get_utxo()
        tx_v3_parent = self.wallet.send_self_transfer(
            from_node=node,
            fee_rate=DEFAULT_FEE,
            utxo_to_spend=utxo_v3_parent,
            version=3
        )
        tx_v3_child = self.wallet.send_self_transfer(
            from_node=node,
            fee_rate=DEFAULT_FEE,
            utxo_to_spend=tx_v3_parent["new_utxo"],
            version=3
        )
        self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]])

        tx_v3_child_rbf_v2 = self.wallet.create_self_transfer(
            fee_rate=DEFAULT_FEE * 2,
            utxo_to_spend=tx_v3_parent["new_utxo"],
            version=2
        )
        expected_error_v2_v3 = f"TRUC-violation, non-version=3 tx {tx_v3_child_rbf_v2['txid']} (wtxid={tx_v3_child_rbf_v2['wtxid']}) cannot spend from version=3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})"
        assert_raises_rpc_error(-26, expected_error_v2_v3, node.sendrawtransaction, tx_v3_child_rbf_v2["hex"])
        self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]])


    @cleanup()
    def test_truc_reorg(self):
        node = self.nodes[0]

        # Prep for fork
        fork_blocks = create_empty_fork(node)
        self.log.info("Test that, during a reorg, TRUC rules are not enforced")
        self.check_mempool([])

        # Testing 2<-3 versions allowed
        tx_v2_block = self.wallet.create_self_transfer(version=2)

        # Testing 3<-2 versions allowed
        tx_v3_block = self.wallet.create_self_transfer(version=3)

        # Testing overly-large child size
        tx_v3_block2 = self.wallet.create_self_transfer(version=3)

        # Also create a linear chain of 3 TRUC transactions that will be directly mined, followed by one v2 in-mempool after block is made
        tx_chain_1 = self.wallet.create_self_transfer(version=3)
        tx_chain_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_1["new_utxo"], version=3)
        tx_chain_3 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_2["new_utxo"], version=3)

        tx_to_mine = [tx_v3_block["hex"], tx_v2_block["hex"], tx_v3_block2["hex"], tx_chain_1["hex"], tx_chain_2["hex"], tx_chain_3["hex"]]
        self.generateblock(node, output="raw(42)", transactions=tx_to_mine)

        self.check_mempool([])

        tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2)
        tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3)
        tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_vsize=1250, version=3)
        assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], TRUC_CHILD_MAX_VSIZE)
        tx_chain_4 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_chain_3["new_utxo"], version=2)
        self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_4["txid"]])

        # Reorg should have all block transactions re-accepted, ignoring TRUC enforcement
        self.trigger_reorg(fork_blocks)
        self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_1["txid"], tx_chain_2["txid"], tx_chain_3["txid"], tx_chain_4["txid"]])

    @cleanup(extra_args=["-limitclustercount=1"])
    def test_nondefault_package_limits(self):
        """
        Max standard tx size + TRUC rules imply the cluster rules (at their default
        values), but those checks must not be skipped. Ensure both sets of checks are done by
        changing the cluster limit configurations.
        """
        node = self.nodes[0]
        self.log.info("Test that a decreased cluster count limit also applies to TRUC child")
        parent_target_vsize = 9990
        child_target_vsize = 500
        tx_v3_parent_large1 = self.wallet.send_self_transfer(
            from_node=node,
            target_vsize=parent_target_vsize,
            version=3
        )
        tx_v3_child_large1 = self.wallet.create_self_transfer(
            utxo_to_spend=tx_v3_parent_large1["new_utxo"],
            target_vsize=child_target_vsize,
            version=3
        )

        # Parent and child are within v3 limits, but cluster count limit is exceeded.
        assert_greater_than_or_equal(TRUC_MAX_VSIZE, tx_v3_parent_large1["tx"].get_vsize())
        assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_large1["tx"].get_vsize())

        assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_v3_child_large1["hex"])
        self.check_mempool([tx_v3_parent_large1["txid"]])
        assert_equal(node.getmempoolentry(tx_v3_parent_large1["txid"])["descendantcount"], 1)
        self.generate(node, 1)

        self.log.info("Test that a decreased limitclustersize also applies to TRUC child")
        self.restart_node(0, extra_args=["-limitclustersize=10", "-acceptnonstdtxn=1"])
        tx_v3_parent_large2 = self.wallet.send_self_transfer(from_node=node, target_vsize=parent_target_vsize, version=3)
        tx_v3_child_large2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large2["new_utxo"], target_vsize=child_target_vsize, version=3)
        # Parent and child are within TRUC limits
        assert_greater_than_or_equal(TRUC_MAX_VSIZE, tx_v3_parent_large2["tx"].get_vsize())
        assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_large2["tx"].get_vsize())
        assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_v3_child_large2["hex"])
        self.log.info("Test that a decreased limitclustercount also applies to TRUC transactions")
        self.restart_node(0, extra_args=["-limitclustercount=1", "-acceptnonstdtxn=1"])
        assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_v3_child_large2["hex"])
        self.check_mempool([tx_v3_parent_large2["txid"]])

    @cleanup()
    def test_truc_ancestors_package(self):
        self.log.info("Test that TRUC ancestor limits are checked within the package")
        node = self.nodes[0]
        tx_v3_parent_normal = self.wallet.create_self_transfer(
            fee_rate=0,
            target_vsize=1001,
            version=3
        )
        tx_v3_parent_2_normal = self.wallet.create_self_transfer(
            fee_rate=0,
            target_vsize=1001,
            version=3
        )
        tx_v3_child_multiparent = self.wallet.create_self_transfer_multi(
            utxos_to_spend=[tx_v3_parent_normal["new_utxo"], tx_v3_parent_2_normal["new_utxo"]],
            fee_per_output=10000,
            version=3
        )
        tx_v3_child_heavy = self.wallet.create_self_transfer_multi(
            utxos_to_spend=[tx_v3_parent_normal["new_utxo"]],
            target_vsize=TRUC_CHILD_MAX_VSIZE + 1,
            fee_per_output=10000,
            version=3
        )

        self.check_mempool([])
        result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_parent_2_normal["hex"], tx_v3_child_multiparent["hex"]])
        assert_equal(result['package_msg'], f"TRUC-violation, tx {tx_v3_child_multiparent['txid']} (wtxid={tx_v3_child_multiparent['wtxid']}) would have too many ancestors")
        self.check_mempool([])

        self.check_mempool([])
        result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_child_heavy["hex"]])
        # tx_v3_child_heavy is heavy based on vsize, not sigops.
        assert_equal(result['package_msg'], f"TRUC-violation, version=3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big: {tx_v3_child_heavy['tx'].get_vsize()} > 1000 virtual bytes")
        self.check_mempool([])

        tx_v3_parent = self.wallet.create_self_transfer(version=3)
        tx_v3_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxo"], version=3)
        tx_v3_grandchild = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_child["new_utxo"], version=3)
        result = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child["hex"], tx_v3_grandchild["hex"]])
        for txresult in result:
            assert_equal(txresult["package-error"], f"TRUC-violation, tx {tx_v3_grandchild['txid']} (wtxid={tx_v3_grandchild['wtxid']}) would have too many ancestors")

    @cleanup(extra_args=None)
    def test_truc_ancestors_package_and_mempool(self):
        """
        A TRUC transaction in a package cannot have 2 TRUC parents.
        Test that if we have a transaction graph A -> B -> C, where A, B, C are
        all TRUC transactions, that we cannot use submitpackage to get the
        transactions all into the mempool.

        Verify, in particular, that if A is already in the mempool, then
        submitpackage(B, C) will fail.
        """
        node = self.nodes[0]
        self.log.info("Test that TRUC ancestor limits include transactions within the package and all in-mempool ancestors")
        # This is our transaction "A":
        tx_in_mempool = self.wallet.send_self_transfer(from_node=node, version=3)

        # Verify that A is in the mempool
        self.check_mempool([tx_in_mempool["txid"]])

        # tx_0fee_parent is our transaction "B"; just create it.
        tx_0fee_parent = self.wallet.create_self_transfer(utxo_to_spend=tx_in_mempool["new_utxo"], fee=0, fee_rate=0, version=3)

        # tx_child_violator is our transaction "C"; create it:
        tx_child_violator = self.wallet.create_self_transfer_multi(utxos_to_spend=[tx_0fee_parent["new_utxo"]], version=3)

        # submitpackage(B, C) should fail
        result = node.submitpackage([tx_0fee_parent["hex"], tx_child_violator["hex"]])
        assert_equal(result['package_msg'], f"TRUC-violation, tx {tx_child_violator['txid']} (wtxid={tx_child_violator['wtxid']}) would have too many ancestors")
        self.check_mempool([tx_in_mempool["txid"]])

    @cleanup(extra_args=None)
    def test_sibling_eviction_package(self):
        """
        When a transaction has a mempool sibling, it may be eligible for sibling eviction.
        However, this option is only available in single transaction acceptance. It doesn't work in
        a multi-testmempoolaccept (where RBF is disabled) or when doing package CPFP.
        """
        self.log.info("Test TRUC sibling eviction in submitpackage and multi-testmempoolaccept")
        node = self.nodes[0]
        # Add a parent + child to mempool
        tx_mempool_parent = self.wallet.send_self_transfer_multi(
            from_node=node,
            utxos_to_spend=[self.wallet.get_utxo()],
            num_outputs=2,
            version=3
        )
        tx_mempool_sibling = self.wallet.send_self_transfer(
            from_node=node,
            utxo_to_spend=tx_mempool_parent["new_utxos"][0],
            version=3
        )
        self.check_mempool([tx_mempool_parent["txid"], tx_mempool_sibling["txid"]])

        tx_sibling_1 = self.wallet.create_self_transfer(
            utxo_to_spend=tx_mempool_parent["new_utxos"][1],
            version=3,
            fee_rate=DEFAULT_FEE*100,
        )
        tx_has_mempool_uncle = self.wallet.create_self_transfer(utxo_to_spend=tx_sibling_1["new_utxo"], version=3)

        tx_sibling_2 = self.wallet.create_self_transfer(
            utxo_to_spend=tx_mempool_parent["new_utxos"][0],
            version=3,
            fee_rate=DEFAULT_FEE*200,
        )

        tx_sibling_3 = self.wallet.create_self_transfer(
            utxo_to_spend=tx_mempool_parent["new_utxos"][1],
            version=3,
            fee_rate=0,
        )
        tx_bumps_parent_with_sibling = self.wallet.create_self_transfer(
            utxo_to_spend=tx_sibling_3["new_utxo"],
            version=3,
            fee_rate=DEFAULT_FEE*300,
        )

        # Fails with another non-related transaction via testmempoolaccept
        tx_unrelated = self.wallet.create_self_transfer(version=3)
        result_test_unrelated = node.testmempoolaccept([tx_sibling_1["hex"], tx_unrelated["hex"]])
        assert_equal(result_test_unrelated[0]["reject-reason"], "TRUC-violation")

        # Fails in a package via testmempoolaccept
        result_test_1p1c = node.testmempoolaccept([tx_sibling_1["hex"], tx_has_mempool_uncle["hex"]])
        assert_equal(result_test_1p1c[0]["reject-reason"], "TRUC-violation")

        # Allowed when tx is submitted in a package and evaluated individually.
        # Note that the child failed since it would be the 3rd generation.
        result_package_indiv = node.submitpackage([tx_sibling_1["hex"], tx_has_mempool_uncle["hex"]])
        self.check_mempool([tx_mempool_parent["txid"], tx_sibling_1["txid"]])
        expected_error_gen3 = f"TRUC-violation, tx {tx_has_mempool_uncle['txid']} (wtxid={tx_has_mempool_uncle['wtxid']}) would have too many ancestors"

        assert_equal(result_package_indiv["tx-results"][tx_has_mempool_uncle['wtxid']]['error'], expected_error_gen3)

        # Allowed when tx is submitted in a package with in-mempool parent (which is deduplicated).
        node.submitpackage([tx_mempool_parent["hex"], tx_sibling_2["hex"]])
        self.check_mempool([tx_mempool_parent["txid"], tx_sibling_2["txid"]])

        # Child cannot pay for sibling eviction for parent, as it violates TRUC topology limits
        result_package_cpfp = node.submitpackage([tx_sibling_3["hex"], tx_bumps_parent_with_sibling["hex"]])
        self.check_mempool([tx_mempool_parent["txid"], tx_sibling_2["txid"]])
        expected_error_cpfp = f"TRUC-violation, tx {tx_mempool_parent['txid']} (wtxid={tx_mempool_parent['wtxid']}) would exceed descendant count limit"

        assert_equal(result_package_cpfp["tx-results"][tx_sibling_3['wtxid']]['error'], expected_error_cpfp)


    @cleanup()
    def test_truc_package_inheritance(self):
        self.log.info("Test that TRUC inheritance is checked within package")
        node = self.nodes[0]
        tx_v3_parent = self.wallet.create_self_transfer(
            fee_rate=0,
            target_vsize=1001,
            version=3
        )
        tx_v2_child = self.wallet.create_self_transfer_multi(
            utxos_to_spend=[tx_v3_parent["new_utxo"]],
            fee_per_output=10000,
            version=2
        )
        self.check_mempool([])
        result = node.submitpackage([tx_v3_parent["hex"], tx_v2_child["hex"]])
        assert_equal(result['package_msg'], f"TRUC-violation, non-version=3 tx {tx_v2_child['txid']} (wtxid={tx_v2_child['wtxid']}) cannot spend from version=3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})")
        self.check_mempool([])

    @cleanup(extra_args=None)
    def test_truc_in_testmempoolaccept(self):
        node = self.nodes[0]

        self.log.info("Test that TRUC inheritance is accurately assessed in testmempoolaccept")
        tx_v2 = self.wallet.create_self_transfer(version=2)
        tx_v2_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=2)
        tx_v3_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=3)
        tx_v3 = self.wallet.create_self_transfer(version=3)
        tx_v2_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=2)
        tx_v3_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=3)

        # testmempoolaccept paths don't require child-with-parents topology. Ensure that topology
        # assumptions aren't made in inheritance checks.
        test_accept_v2_and_v3 = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"]])
        assert all([result["allowed"] for result in test_accept_v2_and_v3])

        test_accept_v3_from_v2 = node.testmempoolaccept([tx_v2["hex"], tx_v3_from_v2["hex"]])
        expected_error_v3_from_v2 = f"TRUC-violation, version=3 tx {tx_v3_from_v2['txid']} (wtxid={tx_v3_from_v2['wtxid']}) cannot spend from non-version=3 tx {tx_v2['txid']} (wtxid={tx_v2['wtxid']})"
        for result in test_accept_v3_from_v2:
            assert_equal(result["package-error"], expected_error_v3_from_v2)

        test_accept_v2_from_v3 = node.testmempoolaccept([tx_v3["hex"], tx_v2_from_v3["hex"]])
        expected_error_v2_from_v3 = f"TRUC-violation, non-version=3 tx {tx_v2_from_v3['txid']} (wtxid={tx_v2_from_v3['wtxid']}) cannot spend from version=3 tx {tx_v3['txid']} (wtxid={tx_v3['wtxid']})"
        for result in test_accept_v2_from_v3:
            assert_equal(result["package-error"], expected_error_v2_from_v3)

        test_accept_pairs = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"], tx_v2_from_v2["hex"], tx_v3_from_v3["hex"]])
        assert all([result["allowed"] for result in test_accept_pairs])

        self.log.info("Test that descendant violations are caught in testmempoolaccept")
        tx_v3_independent = self.wallet.create_self_transfer(version=3)
        tx_v3_parent = self.wallet.create_self_transfer_multi(num_outputs=2, version=3)
        tx_v3_child_1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][0], version=3)
        tx_v3_child_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][1], version=3)
        test_accept_2children = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"]])
        expected_error_2children = f"TRUC-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
        for result in test_accept_2children:
            assert_equal(result["package-error"], expected_error_2children)

        # Extra TRUC transaction does not get incorrectly marked as extra descendant
        test_accept_1child_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_independent["hex"]])
        assert all([result["allowed"] for result in test_accept_1child_with_exra])

        # Extra TRUC transaction does not make us ignore the extra descendant
        test_accept_2children_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"], tx_v3_independent["hex"]])
        expected_error_extra = f"TRUC-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
        for result in test_accept_2children_with_exra:
            assert_equal(result["package-error"], expected_error_extra)
        # Same result if the parent is already in mempool
        node.sendrawtransaction(tx_v3_parent["hex"])
        test_accept_2children_with_in_mempool_parent = node.testmempoolaccept([tx_v3_child_1["hex"], tx_v3_child_2["hex"]])
        for result in test_accept_2children_with_in_mempool_parent:
            assert_equal(result["package-error"], expected_error_extra)

    @cleanup(extra_args=None)
    def test_reorg_2child_rbf(self):
        node = self.nodes[0]
        self.log.info("Test that children of a TRUC transaction can be replaced individually, even if there are multiple due to reorg")

        # Prep for fork
        fork_blocks = create_empty_fork(node)
        ancestor_tx = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2, version=3)
        self.check_mempool([ancestor_tx["txid"]])

        self.generate(node, 1)[0]
        self.check_mempool([])

        child_1 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][0])
        child_2 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][1])
        self.check_mempool([child_1["txid"], child_2["txid"]])

        # Create a reorg, causing ancestor_tx to exceed the 1-child limit
        self.trigger_reorg(fork_blocks)
        self.check_mempool([ancestor_tx["txid"], child_1["txid"], child_2["txid"]])
        assert_equal(node.getmempoolentry(ancestor_tx["txid"])["descendantcount"], 3)

        # Create a replacement of child_1. It does not conflict with child_2.
        child_1_conflict = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][0], fee_rate=Decimal("0.01"))

        # Ensure child_1 and child_1_conflict are different transactions
        assert_not_equal(child_1_conflict["txid"], child_1["txid"])
        self.check_mempool([ancestor_tx["txid"], child_1_conflict["txid"], child_2["txid"]])
        assert_equal(node.getmempoolentry(ancestor_tx["txid"])["descendantcount"], 3)

    @cleanup(extra_args=None)
    def test_truc_sibling_eviction(self):
        self.log.info("Test sibling eviction for TRUC")
        node = self.nodes[0]
        tx_v3_parent = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2, version=3)
        # This is the sibling to replace
        tx_v3_child_1 = self.wallet.send_self_transfer(
            from_node=node, utxo_to_spend=tx_v3_parent["new_utxos"][0], fee_rate=DEFAULT_FEE * 2, version=3
        )
        assert tx_v3_child_1["txid"] in node.getrawmempool()

        self.log.info("Test tx must be higher feerate than sibling to evict it")
        tx_v3_child_2_rule6 = self.wallet.create_self_transfer(
            utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=DEFAULT_FEE, version=3
        )
        rule6_str = f"insufficient fee (including sibling eviction), rejecting replacement {tx_v3_child_2_rule6['txid']}"
        assert_raises_rpc_error(-26, rule6_str, node.sendrawtransaction, tx_v3_child_2_rule6["hex"])
        self.check_mempool([tx_v3_parent['txid'], tx_v3_child_1['txid']])

        self.log.info("Test tx must meet absolute fee rules to evict sibling")
        tx_v3_child_2_rule4 = self.wallet.create_self_transfer(
            utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=2 * DEFAULT_FEE + Decimal("0.00000001"), version=3
        )
        rule4_str = f"insufficient fee (including sibling eviction), rejecting replacement {tx_v3_child_2_rule4['txid']}, not enough additional fees to relay"
        assert_raises_rpc_error(-26, rule4_str, node.sendrawtransaction, tx_v3_child_2_rule4["hex"])
        self.check_mempool([tx_v3_parent['txid'], tx_v3_child_1['txid']])

        self.log.info("Test tx cannot cause more than 100 evictions including RBF and sibling eviction")
        # First add 4 groups of 25 transactions.
        utxos_for_conflict = []
        txids_v2_100 = []
        for _ in range(4):
            confirmed_utxo = self.wallet.get_utxo(confirmed_only=True)
            utxos_for_conflict.append(confirmed_utxo)
            # 25 is within descendant limits
            chain_length = int(MAX_REPLACEMENT_CANDIDATES / 4)
            chain = self.wallet.create_self_transfer_chain(chain_length=chain_length, utxo_to_spend=confirmed_utxo)
            for item in chain:
                txids_v2_100.append(item["txid"])
                node.sendrawtransaction(item["hex"])
        self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_1["txid"]])

        # Replacing 100 transactions is fine
        tx_v3_replacement_only = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_for_conflict, fee_per_output=4000000)
        # Override maxfeerate - it costs a lot to replace these 100 transactions.
        assert node.testmempoolaccept([tx_v3_replacement_only["hex"]], maxfeerate=0)[0]["allowed"]
        self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_1["txid"]])

        self.log.info("Test sibling eviction is successful if it meets all RBF rules")
        tx_v3_child_2 = self.wallet.create_self_transfer(
            utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=DEFAULT_FEE*10, version=3
        )
        node.sendrawtransaction(tx_v3_child_2["hex"])
        self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_2["txid"]])

        self.log.info("Test that it's possible to do a sibling eviction and RBF at the same time")
        utxo_unrelated_conflict = self.wallet.get_utxo(confirmed_only=True)
        tx_unrelated_replacee = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_unrelated_conflict)
        assert tx_unrelated_replacee["txid"] in node.getrawmempool()

        fee_to_beat = max(int(tx_v3_child_2["fee"] * COIN), int(tx_unrelated_replacee["fee"]*COIN))

        tx_v3_child_3 = self.wallet.create_self_transfer_multi(
            utxos_to_spend=[tx_v3_parent["new_utxos"][0], utxo_unrelated_conflict], fee_per_output=fee_to_beat*4, version=3
        )
        node.sendrawtransaction(tx_v3_child_3["hex"])
        self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_3["txid"]])

    @cleanup(extra_args=None)
    def test_reorg_sibling_eviction_1p2c(self):
        node = self.nodes[0]
        self.log.info("Test that sibling eviction is not allowed when multiple siblings exist")

        # Prep for fork
        fork_blocks = create_empty_fork(node)
        tx_with_multi_children = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=3, version=3, confirmed_only=True)
        self.check_mempool([tx_with_multi_children["txid"]])

        self.generate(node, 1)
        self.check_mempool([])

        tx_with_sibling1 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=tx_with_multi_children["new_utxos"][0])
        tx_with_sibling2 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=tx_with_multi_children["new_utxos"][1])
        self.check_mempool([tx_with_sibling1["txid"], tx_with_sibling2["txid"]])

        # Create a reorg, bringing tx_with_multi_children back into the mempool with a descendant count of 3.
        self.trigger_reorg(fork_blocks)
        self.check_mempool([tx_with_multi_children["txid"], tx_with_sibling1["txid"], tx_with_sibling2["txid"]])
        assert_equal(node.getmempoolentry(tx_with_multi_children["txid"])["descendantcount"], 3)

        # Sibling eviction is not allowed because there are two siblings
        tx_with_sibling3 = self.wallet.create_self_transfer(
            version=3,
            utxo_to_spend=tx_with_multi_children["new_utxos"][2],
            fee_rate=DEFAULT_FEE*50
        )
        expected_error_2siblings = f"TRUC-violation, tx {tx_with_multi_children['txid']} (wtxid={tx_with_multi_children['wtxid']}) would exceed descendant count limit"
        assert_raises_rpc_error(-26, expected_error_2siblings, node.sendrawtransaction, tx_with_sibling3["hex"])

        # However, an RBF (with conflicting inputs) is possible even if the resulting cluster size exceeds 2
        tx_with_sibling3_rbf = self.wallet.send_self_transfer(
            from_node=node,
            version=3,
            utxo_to_spend=tx_with_multi_children["new_utxos"][0],
            fee_rate=DEFAULT_FEE*50
        )
        self.check_mempool([tx_with_multi_children["txid"], tx_with_sibling3_rbf["txid"], tx_with_sibling2["txid"]])

    @cleanup(extra_args=None)
    def test_minrelay_in_package_combos(self):
        node = self.nodes[0]
        self.log.info("Test that all transaction versions can be under minrelaytxfee for various settings...")

        for minrelay_setting in (0, 5, 10, 100, 500, 1000, 5000, 333333, 2500000):
            self.log.info(f"-> Test -minrelaytxfee={minrelay_setting}sat/kvB...")
            setting_decimal = minrelay_setting / Decimal(COIN)
            self.restart_node(0, extra_args=[f"-minrelaytxfee={setting_decimal:.8f}", "-persistmempool=0"])
            minrelayfeerate = node.getmempoolinfo()["minrelaytxfee"]
            high_feerate = minrelayfeerate * 50

            tx_v3_0fee_parent = self.wallet.create_self_transfer(fee=0, fee_rate=0, confirmed_only=True, version=3)
            tx_v3_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_0fee_parent["new_utxo"], fee_rate=high_feerate, version=3)
            total_v3_fee = tx_v3_child["fee"] + tx_v3_0fee_parent["fee"]
            total_v3_size = tx_v3_child["tx"].get_vsize() + tx_v3_0fee_parent["tx"].get_vsize()
            assert_greater_than_or_equal(total_v3_fee, get_fee(total_v3_size, minrelayfeerate))
            if minrelayfeerate > 0:
                assert_greater_than(get_fee(tx_v3_0fee_parent["tx"].get_vsize(), minrelayfeerate), 0)
                # Always need to pay at least 1 satoshi for entry, even if minimum feerate is very low
                assert_greater_than(total_v3_fee, 0)
                # Also create a version where the child is at minrelaytxfee
                tx_v3_child_minrelay = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_0fee_parent["new_utxo"], fee_rate=minrelayfeerate, version=3)
                result_truc_minrelay = node.submitpackage([tx_v3_0fee_parent["hex"], tx_v3_child_minrelay["hex"]])
                assert_equal(result_truc_minrelay["package_msg"], "transaction failed")

            tx_v2_0fee_parent = self.wallet.create_self_transfer(fee=0, fee_rate=0, confirmed_only=True, version=2)
            tx_v2_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v2_0fee_parent["new_utxo"], fee_rate=high_feerate, version=2)
            total_v2_fee = tx_v2_child["fee"] + tx_v2_0fee_parent["fee"]
            total_v2_size = tx_v2_child["tx"].get_vsize() + tx_v2_0fee_parent["tx"].get_vsize()
            assert_greater_than_or_equal(total_v2_fee, get_fee(total_v2_size, minrelayfeerate))
            if minrelayfeerate > 0:
                assert_greater_than(get_fee(tx_v2_0fee_parent["tx"].get_vsize(), minrelayfeerate), 0)
                # Always need to pay at least 1 satoshi for entry, even if minimum feerate is very low
                assert_greater_than(total_v2_fee, 0)

            result_truc = node.submitpackage([tx_v3_0fee_parent["hex"], tx_v3_child["hex"]], maxfeerate=0)
            assert_equal(result_truc["package_msg"], "success")

            result_non_truc = node.submitpackage([tx_v2_0fee_parent["hex"], tx_v2_child["hex"]], maxfeerate=0)
            assert_equal(result_non_truc["package_msg"], "success")
            self.check_mempool([tx_v2_0fee_parent["txid"], tx_v2_child["txid"], tx_v3_0fee_parent["txid"], tx_v3_child["txid"]])


    def run_test(self):
        self.log.info("Generate blocks to create UTXOs")
        node = self.nodes[0]
        self.wallet = MiniWallet(node)
        self.generate(self.wallet, 200)
        self.test_truc_max_vsize()
        self.test_truc_acceptance()
        self.test_truc_replacement()
        self.test_truc_reorg()
        self.test_nondefault_package_limits()
        self.test_truc_ancestors_package()
        self.test_truc_ancestors_package_and_mempool()
        self.test_sibling_eviction_package()
        self.test_truc_package_inheritance()
        self.test_truc_in_testmempoolaccept()
        self.test_reorg_2child_rbf()
        self.test_truc_sibling_eviction()
        self.test_reorg_sibling_eviction_1p2c()
        self.test_minrelay_in_package_combos()


if __name__ == "__main__":
    MempoolTRUC(__file__).main()
