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

Code before:
            {"txid": tx2["txid"], "wtxid": tx2["wtxid"], "package-error": "conflict-in-package"}
        ])

    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"])

Review comment (bug):
i.e. if we relaxed the package-not-child-with-unconfirmed-parents rule, we should still fail because
- package parent is conflicting with ancestor of child (what the rule would be)
- the child spends 2 conflicting transactions, which makes no sense (what's actually wrong)

Code after:
        tx_child = self.wallet.create_self_transfer_multi(
            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"}