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

Code before:

    def test_simple_doublespend(self):
        """Simple doublespend"""
        # we use MiniWallet to create a transaction template with inputs correctly set,
        # and modify the output (amount, scriptPubKey) according to our needs
        tx = self.wallet.create_self_transfer()["tx"]
        tx1a_txid = self.nodes[0].sendrawtransaction(tx.serialize().hex())

        # Should fail because we haven't changed the fee
        tx.vout[0].scriptPubKey[-1] ^= 1

        # This will raise an exception due to insufficient fee
        assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex(), 0)

        # Extra 0.1 BTC fee
        tx.vout[0].nValue -= int(0.1 * COIN)
        tx1b_hex = tx.serialize().hex()
        # Works when enabled
        tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0)

        mempool = self.nodes[0].getrawmempool()

        assert tx1a_txid not in mempool
        assert tx1b_txid in mempool

        assert_equal(tx1b_hex, self.nodes[0].getrawtransaction(tx1b_txid))

    def test_doublespend_chain(self):
        """Doublespend of a long chain"""

        initial_nValue = 5 * COIN
        tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue)

        prevout = tx0_outpoint
        remaining_value = initial_nValue
        chain_txids = []
        while remaining_value > 1 * COIN:
            remaining_value -= int(0.1 * COIN)
            prevout = self.wallet.send_self_transfer(
                from_node=self.nodes[0],
                utxo_to_spend=prevout,
                sequence=0,
                fee=Decimal("0.1"),
            )["new_utxo"]
            chain_txids.append(prevout["txid"])

        # Whether the double-spend is allowed is evaluated by including all
        # child fees - 4 BTC - so this attempt is rejected.
        dbl_tx = self.wallet.create_self_transfer(
            utxo_to_spend=tx0_outpoint,
            sequence=0,

Review comment (bug):
In commit bafd175ca898c3ff74ae7927a3e9e3607ea3c8cd, can make a helper for this.

<details><summary>diff</summary><p>

```diff
--- a/test/functional/feature_rbf.py
+++ b/test/functional/feature_rbf.py
@@ -107,6 +107,11 @@ class ReplaceByFeeTest(BitcoinTestFramework):
 
         return self.wallet.get_utxo(txid=tx["txid"], vout=tx["sent_vout"])
 
+    def assert_rejected_by_testmempoolaccept(self, tx_hex, msg):
+        result = self.nodes[0].testmempoolaccept(rawtxs=[tx_hex])[0]
+        assert not result["allowed"]
+        assert msg in result["reject-reason"]
+
     def test_simple_doublespend(self):
         """Simple doublespend"""
         # we use MiniWallet to create a transaction template with inputs correctly set,
@@ -121,9 +126,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception due to insufficient fee
         debug_message = f"insufficient fee, rejecting replacement {tx.hash}; new feerate"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx.serialize().hex(), 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[tx.serialize().hex()])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(tx.serialize().hex(), debug_message)
 
         # Extra 0.1 BTC fee
         tx.vout[0].nValue -= int(0.1 * COIN)
@@ -168,9 +171,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception due to insufficient fee
         debug_message = f"insufficient fee, rejecting replacement {dbl_tx['txid']}; less fees than conflicting txs"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, dbl_tx["hex"], 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[dbl_tx["hex"]])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(dbl_tx["hex"], debug_message)
 
         # Accepted with sufficient fee
         dbl_tx["tx"].vout[0].nValue = int(0.1 * COIN)
@@ -307,9 +308,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception
         debug_message = f"bad-txns-spends-conflicting-tx, {tx2['txid']} spends conflicting transaction {tx1a['txid']}"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx2["hex"], 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[tx2["hex"]])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(tx2["hex"], debug_message)
 
         # Spend tx1a's output to test the indirect case.
         tx1b_utxo = self.wallet.send_self_transfer(
@@ -349,9 +348,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception
         debug_message = f"replacement-adds-unconfirmed, replacement {tx2['txid']} adds unconfirmed input, idx"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx2["hex"], 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[tx2["hex"]])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(tx2["hex"], debug_message)
 
     def test_too_many_replacements(self):
         """Replacements that evict too many transactions are rejected"""
@@ -394,9 +391,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception
         debug_message = f"too many potential replacements, rejecting replacement {double_tx['txid']}; too many potential replacements"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, double_tx["hex"], 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[double_tx["hex"]])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(double_tx["hex"], debug_message)
 
         # If we remove an input, it should pass
         double_tx["tx"].vin.pop()
@@ -720,9 +715,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         tx.rehash()
         debug_message = f"insufficient fee, rejecting replacement {tx.hash}; not enough additional fees to relay"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx.serialize().hex())
-        res = self.nodes[0].testmempoolaccept(rawtxs=[tx.serialize().hex()])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(tx.serialize().hex(), debug_message)
 
     def test_fullrbf(self):
```
</p></details>


Code after:

    def test_simple_doublespend(self):
        """Simple doublespend"""
        # we use MiniWallet to create a transaction template with inputs correctly set,
        # and modify the output (amount, scriptPubKey) according to our needs
        tx = self.wallet.create_self_transfer(fee_rate=Decimal("0.003"))["tx"]
        tx1a_txid = self.nodes[0].sendrawtransaction(tx.serialize().hex())

        # Should fail because we haven't changed the fee
        tx.vout[0].scriptPubKey[-1] ^= 1
        tx.rehash()
        tx_hex = tx.serialize().hex()

        # This will raise an exception due to insufficient fee
        reject_reason = "insufficient fee"
        reject_details = f"{reject_reason}, rejecting replacement {tx.hash}; new feerate 0.00300000 BTC/kvB <= old feerate 0.00300000 BTC/kvB"
        res = self.nodes[0].testmempoolaccept(rawtxs=[tx_hex])[0]
        assert_equal(res["reject-reason"], reject_reason)
        assert_equal(res["reject-details"], reject_details)
        assert_raises_rpc_error(-26, f"{reject_details}", self.nodes[0].sendrawtransaction, tx_hex, 0)


        # Extra 0.1 BTC fee
        tx.vout[0].nValue -= int(0.1 * COIN)
        tx1b_hex = tx.serialize().hex()
        # Works when enabled
        tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0)

        mempool = self.nodes[0].getrawmempool()

        assert tx1a_txid not in mempool
        assert tx1b_txid in mempool

        assert_equal(tx1b_hex, self.nodes[0].getrawtransaction(tx1b_txid))

    def test_doublespend_chain(self):
        """Doublespend of a long chain"""

        initial_nValue = 5 * COIN
        tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue)

        prevout = tx0_outpoint
        remaining_value = initial_nValue
        chain_txids = []
        while remaining_value > 1 * COIN:
            remaining_value -= int(0.1 * COIN)
            prevout = self.wallet.send_self_transfer(
                from_node=self.nodes[0],
                utxo_to_spend=prevout,
                sequence=0,
                fee=Decimal("0.1"),