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

Code before:
        ])

    def test_rbf(self):
        node = self.nodes[0]

        coin = self.wallet.get_utxo()
        fee = Decimal("0.00125000")
        replaceable_tx = self.wallet.create_self_transfer(utxo_to_spend=coin, sequence=MAX_BIP125_RBF_SEQUENCE, fee = fee)
        testres_replaceable = node.testmempoolaccept([replaceable_tx["hex"]])[0]
        assert_equal(testres_replaceable["txid"], replaceable_tx["txid"])
        assert_equal(testres_replaceable["wtxid"], replaceable_tx["wtxid"])
        assert testres_replaceable["allowed"]
        assert_equal(testres_replaceable["vsize"], replaceable_tx["tx"].get_vsize())
        assert_equal(testres_replaceable["fees"]["base"], fee)
        assert_fee_amount(fee, replaceable_tx["tx"].get_vsize(), testres_replaceable["fees"]["effective-feerate"])
        assert_equal(testres_replaceable["fees"]["effective-includes"], [replaceable_tx["wtxid"]])

        # Replacement transaction is identical except has double the fee
        replacement_tx = self.wallet.create_self_transfer(utxo_to_spend=coin, sequence=MAX_BIP125_RBF_SEQUENCE, fee = 2 * fee)
        testres_rbf_conflicting = node.testmempoolaccept([replaceable_tx["hex"], replacement_tx["hex"]])
        assert_equal(testres_rbf_conflicting, [
            {"txid": replaceable_tx["txid"], "wtxid": replaceable_tx["wtxid"], "package-error": "conflict-in-package"},
            {"txid": replacement_tx["txid"], "wtxid": replacement_tx["wtxid"], "package-error": "conflict-in-package"}
        ])

        self.log.info("Test that packages cannot conflict with mempool transactions, even if a valid BIP125 RBF")
        # This transaction is a valid BIP125 replace-by-fee
        self.wallet.sendrawtransaction(from_node=node, tx_hex=replaceable_tx["hex"])
        testres_rbf_single = node.testmempoolaccept([replacement_tx["hex"]])
        assert testres_rbf_single[0]["allowed"]
        testres_rbf_package = self.independent_txns_testres_blank + [{
            "txid": replacement_tx["txid"], "wtxid": replacement_tx["wtxid"], "allowed": False,
            "reject-reason": "bip125-replacement-disallowed"
        }]
        self.assert_testres_equal(self.independent_txns_hex + [replacement_tx["hex"]], testres_rbf_package)

    def assert_equal_package_results(self, node, testmempoolaccept_result, submitpackage_result):
        """Assert that a successful submitpackage result is consistent with testmempoolaccept
        results and getmempoolentry info. Note that the result structs are different and, due to
        policy differences between testmempoolaccept and submitpackage (i.e. package feerate),
        some information may be different.
        """
        for testres_tx in testmempoolaccept_result:
            # Grab this result from the submitpackage_result
            submitres_tx = submitpackage_result["tx-results"][testres_tx["wtxid"]]
            assert_equal(submitres_tx["txid"], testres_tx["txid"])
            # No "allowed" if the tx was already in the mempool
            if "allowed" in testres_tx and testres_tx["allowed"]:
                assert_equal(submitres_tx["vsize"], testres_tx["vsize"])
                assert_equal(submitres_tx["fees"]["base"], testres_tx["fees"]["base"])
            entry_info = node.getmempoolentry(submitres_tx["txid"])

Review comment (bug):
Perhaps add an `assert tx2["txid"] not in node.getrawmempool()`. Depending on where the rule is enforced (or how we received them if it's p2p), tx2 could replace tx1 before we fail :laughing: either way it'd be good for the test to show us what happens.

Code after:
            fee_per_output=int(DEFAULT_FEE * 5 * COIN),
            utxos_to_spend=[tx1["new_utxo"], tx2["new_utxo"]],
        )

        testres = node.testmempoolaccept([tx1["hex"], tx2["hex"], tx_child["hex"]])

        assert_equal(testres, [
            {"txid": tx1["txid"], "wtxid": tx1["wtxid"], "package-error": "conflict-in-package"},
            {"txid": tx2["txid"], "wtxid": tx2["wtxid"], "package-error": "conflict-in-package"},
            {"txid": tx_child["txid"], "wtxid": tx_child["wtxid"], "package-error": "conflict-in-package"}
        ])

        submitres = node.submitpackage([tx1["hex"], tx2["hex"], tx_child["hex"]])
        assert_equal(submitres, {'package_msg': 'conflict-in-package', 'tx-results': {}, 'replaced-transactions': []})

        # Submit tx1 to mempool, then try the same package again
        node.sendrawtransaction(tx1["hex"])

        submitres = node.submitpackage([tx1["hex"], tx2["hex"], tx_child["hex"]])
        assert_equal(submitres, {'package_msg': 'conflict-in-package', 'tx-results': {}, 'replaced-transactions': []})
        assert tx_child["txid"] not in node.getrawmempool()

        # ... and without the in-mempool ancestor tx1 included in the call
        submitres = node.submitpackage([tx2["hex"], tx_child["hex"]])
        assert_equal(submitres, {'package_msg': 'package-not-child-with-unconfirmed-parents', 'tx-results': {}, 'replaced-transactions': []})

        # Regardless of error type, the child can never enter the mempool
        assert tx_child["txid"] not in node.getrawmempool()

    def test_rbf(self):
        node = self.nodes[0]

        coin = self.wallet.get_utxo()
        fee = Decimal("0.00125000")
        replaceable_tx = self.wallet.create_self_transfer(utxo_to_spend=coin, sequence=MAX_BIP125_RBF_SEQUENCE, fee = fee)
        testres_replaceable = node.testmempoolaccept([replaceable_tx["hex"]])[0]
        assert_equal(testres_replaceable["txid"], replaceable_tx["txid"])
        assert_equal(testres_replaceable["wtxid"], replaceable_tx["wtxid"])
        assert testres_replaceable["allowed"]
        assert_equal(testres_replaceable["vsize"], replaceable_tx["tx"].get_vsize())
        assert_equal(testres_replaceable["fees"]["base"], fee)
        assert_fee_amount(fee, replaceable_tx["tx"].get_vsize(), testres_replaceable["fees"]["effective-feerate"])
        assert_equal(testres_replaceable["fees"]["effective-includes"], [replaceable_tx["wtxid"]])

        # Replacement transaction is identical except has double the fee
        replacement_tx = self.wallet.create_self_transfer(utxo_to_spend=coin, sequence=MAX_BIP125_RBF_SEQUENCE, fee = 2 * fee)
        testres_rbf_conflicting = node.testmempoolaccept([replaceable_tx["hex"], replacement_tx["hex"]])
        assert_equal(testres_rbf_conflicting, [
            {"txid": replaceable_tx["txid"], "wtxid": replaceable_tx["wtxid"], "package-error": "conflict-in-package"},
            {"txid": replacement_tx["txid"], "wtxid": replacement_tx["wtxid"], "package-error": "conflict-in-package"}
        ])