From e779d59eca62dd20885158256cdbbfbe2ff3ac5c Mon Sep 17 00:00:00 2001 From: glozow Date: Thu, 31 Jul 2025 11:29:49 -0400 Subject: [PATCH 01/19] [test] check miner doesn't select 0fee transactions Github-Pull: #33106 Rebased-From: e5f896bb1f052fb8c7811c6024cb49143b427512 --- test/functional/mining_basic.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index aca71933ec5..02d3480dd35 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -28,6 +28,7 @@ from test_framework.p2p import P2PDataStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than, assert_greater_than_or_equal, assert_raises_rpc_error, get_fee, @@ -86,7 +87,7 @@ class MiningTest(BitcoinTestFramework): node = self.nodes[0] # test default (no parameter), zero and a bunch of arbitrary blockmintxfee rates [sat/kvB] - for blockmintxfee_sat_kvb in (DEFAULT_BLOCK_MIN_TX_FEE, 0, 50, 100, 500, 2500, 5000, 21000, 333333, 2500000): + for blockmintxfee_sat_kvb in (DEFAULT_BLOCK_MIN_TX_FEE, 0, 1, 5, 10, 50, 100, 500, 2500, 5000, 21000, 333333, 2500000): blockmintxfee_btc_kvb = blockmintxfee_sat_kvb / Decimal(COIN) if blockmintxfee_sat_kvb == DEFAULT_BLOCK_MIN_TX_FEE: self.log.info(f"-> Default -blockmintxfee setting ({blockmintxfee_sat_kvb} sat/kvB)...") @@ -97,19 +98,27 @@ class MiningTest(BitcoinTestFramework): self.wallet.rescan_utxos() # to avoid spending outputs of txs that are not in mempool anymore after restart # submit one tx with exactly the blockmintxfee rate, and one slightly below - tx_with_min_feerate = self.wallet.send_self_transfer(from_node=node, fee_rate=blockmintxfee_btc_kvb) + tx_with_min_feerate = self.wallet.send_self_transfer(from_node=node, fee_rate=blockmintxfee_btc_kvb, confirmed_only=True) assert_equal(tx_with_min_feerate["fee"], get_fee(tx_with_min_feerate["tx"].get_vsize(), blockmintxfee_btc_kvb)) - if blockmintxfee_btc_kvb > 0: + if blockmintxfee_sat_kvb > 5: lowerfee_btc_kvb = blockmintxfee_btc_kvb - Decimal(10)/COIN # 0.01 sat/vbyte lower - tx_below_min_feerate = self.wallet.send_self_transfer(from_node=node, fee_rate=lowerfee_btc_kvb) + tx_below_min_feerate = self.wallet.send_self_transfer(from_node=node, fee_rate=lowerfee_btc_kvb, confirmed_only=True) assert_equal(tx_below_min_feerate["fee"], get_fee(tx_below_min_feerate["tx"].get_vsize(), lowerfee_btc_kvb)) else: # go below zero fee by using modified fees - tx_below_min_feerate = self.wallet.send_self_transfer(from_node=node, fee_rate=blockmintxfee_btc_kvb) + tx_below_min_feerate = self.wallet.send_self_transfer(from_node=node, fee_rate=blockmintxfee_btc_kvb, confirmed_only=True) node.prioritisetransaction(tx_below_min_feerate["txid"], 0, -1) # check that tx below specified fee-rate is neither in template nor in the actual block block_template = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) block_template_txids = [tx['txid'] for tx in block_template['transactions']] + + # Unless blockmintxfee is 0, the template shouldn't contain free transactions. + # Note that the real block assembler uses package feerates, but we didn't create dependent transactions so it's ok to use base feerate. + if blockmintxfee_btc_kvb > 0: + for txid in block_template_txids: + tx = node.getmempoolentry(txid) + assert_greater_than(tx['fees']['base'], 0) + self.generate(self.wallet, 1, sync_fun=self.no_op) block = node.getblock(node.getbestblockhash(), verbosity=2) block_txids = [tx['txid'] for tx in block['tx']] From 308778b7b614c29bf8fd3e9edcf938d9f19cbba2 Mon Sep 17 00:00:00 2001 From: glozow Date: Thu, 31 Jul 2025 12:38:36 -0400 Subject: [PATCH 02/19] [test] check bypass of minrelay for various minrelaytxfee settings Github-Pull: #33106 Rebased-From: 85f498893f54ea7d84f2bdf12aa35d198edf8a72 --- test/functional/mempool_truc.py | 49 ++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/test/functional/mempool_truc.py b/test/functional/mempool_truc.py index 28f3256ef1b..774c9e7c1f2 100755 --- a/test/functional/mempool_truc.py +++ b/test/functional/mempool_truc.py @@ -14,6 +14,7 @@ from test_framework.util import ( assert_greater_than, assert_greater_than_or_equal, assert_raises_rpc_error, + get_fee, ) from test_framework.wallet import ( COIN, @@ -621,12 +622,57 @@ class MempoolTRUC(BitcoinTestFramework): ) 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 only TRUC transactions 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) + + 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) + if minrelayfeerate > 0: + assert_equal(result_non_truc["package_msg"], "transaction failed") + min_fee_parent = int(get_fee(tx_v2_0fee_parent["tx"].get_vsize(), minrelayfeerate) * COIN) + assert_equal(result_non_truc["tx-results"][tx_v2_0fee_parent["wtxid"]]["error"], f"min relay fee not met, 0 < {min_fee_parent}") + self.check_mempool([tx_v3_0fee_parent["txid"], tx_v3_child["txid"]]) + else: + 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, 120) + self.generate(self.wallet, 200) self.test_truc_max_vsize() self.test_truc_acceptance() self.test_truc_replacement() @@ -641,6 +687,7 @@ class MempoolTRUC(BitcoinTestFramework): 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__": From cf875f15596052d42abccecb81a96d8e343fd2cd Mon Sep 17 00:00:00 2001 From: glozow Date: Thu, 31 Jul 2025 13:53:57 -0400 Subject: [PATCH 03/19] [test] RBF rule 4 for various incrementalrelayfee settings Github-Pull: #33106 Rebased-From: 72dc18467dbfc16cdbda2dd109b087243b397799 --- test/functional/feature_rbf.py | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/functional/feature_rbf.py b/test/functional/feature_rbf.py index b660b96935d..f16be68fcc4 100755 --- a/test/functional/feature_rbf.py +++ b/test/functional/feature_rbf.py @@ -14,7 +14,10 @@ from test_framework.messages import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than, + assert_greater_than_or_equal, assert_raises_rpc_error, + get_fee, ) from test_framework.wallet import MiniWallet from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE @@ -87,6 +90,9 @@ class ReplaceByFeeTest(BitcoinTestFramework): self.log.info("Running test full replace by fee...") self.test_fullrbf() + self.log.info("Running test incremental relay feerates...") + self.test_incremental_relay_feerates() + self.log.info("Passed") def make_utxo(self, node, amount, *, confirmed=True, scriptPubKey=None): @@ -701,6 +707,38 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx.vout[0].nValue -= 1 assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex()) + def test_incremental_relay_feerates(self): + self.log.info("Test that incremental relay fee is applied correctly in RBF for various settings...") + node = self.nodes[0] + for incremental_setting in (0, 5, 10, 50, 100, 234, 1000, 5000, 21000): + incremental_setting_decimal = incremental_setting / Decimal(COIN) + self.log.info(f"-> Test -incrementalrelayfee={incremental_setting_decimal:.8f}sat/kvB...") + self.restart_node(0, extra_args=[f"-incrementalrelayfee={incremental_setting_decimal:.8f}", "-datacarriersize=5000", "-persistmempool=0"]) + + # When incremental relay feerate is higher than min relay feerate, min relay feerate is automatically increased. + min_relay_feerate = node.getmempoolinfo()["minrelaytxfee"] + assert_greater_than_or_equal(min_relay_feerate, incremental_setting_decimal) + + low_feerate = min_relay_feerate * 2 + confirmed_utxo = self.wallet.get_utxo(confirmed_only=True) + replacee_tx = self.wallet.create_self_transfer(utxo_to_spend=confirmed_utxo, fee_rate=low_feerate, target_weight=20000) + node.sendrawtransaction(replacee_tx['hex']) + + replacement_placeholder_tx = self.wallet.create_self_transfer(utxo_to_spend=confirmed_utxo) + replacement_expected_size = replacement_placeholder_tx['tx'].get_vsize() + replacement_required_fee = get_fee(replacement_expected_size, incremental_setting_decimal) + replacee_tx['fee'] + + # Should always be required to pay additional fees + if incremental_setting > 0: + assert_greater_than(replacement_required_fee, replacee_tx['fee']) + + # 1 satoshi shy of the required fee + failed_replacement_tx = self.wallet.create_self_transfer(utxo_to_spend=confirmed_utxo, fee=replacement_required_fee - Decimal("0.00000001")) + assert_raises_rpc_error(-26, "insufficient fee", node.sendrawtransaction, failed_replacement_tx['hex']) + + replacement_tx = self.wallet.create_self_transfer(utxo_to_spend=confirmed_utxo, fee=replacement_required_fee) + node.sendrawtransaction(replacement_tx['hex']) + def test_fullrbf(self): confirmed_utxo = self.make_utxo(self.nodes[0], int(2 * COIN)) From e3273e03b1aea0c3ee3aeca802c984ab007f91ed Mon Sep 17 00:00:00 2001 From: glozow Date: Mon, 11 Aug 2025 16:58:21 -0400 Subject: [PATCH 04/19] [test] explicitly check default -minrelaytxfee and -incrementalrelayfee Github-Pull: #33106 Rebased-From: 1fbee5d7b61b83e68e4230c8a97ca308de92c4c3 --- test/functional/mempool_accept.py | 9 +++++++++ test/functional/test_framework/mempool_util.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py index 4d08575255b..1380f4e57b6 100755 --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -9,6 +9,10 @@ from decimal import Decimal import math from test_framework.test_framework import BitcoinTestFramework +from test_framework.mempool_util import ( + DEFAULT_MIN_RELAY_TX_FEE, + DEFAULT_INCREMENTAL_RELAY_FEE, +) from test_framework.messages import ( MAX_BIP125_RBF_SEQUENCE, COIN, @@ -80,6 +84,11 @@ class MempoolAcceptanceTest(BitcoinTestFramework): assert_equal(node.getblockcount(), 200) assert_equal(node.getmempoolinfo()['size'], self.mempool_size) + self.log.info("Check default settings") + # Settings are listed in BTC/kvB + assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN) + assert_equal(node.getmempoolinfo()['incrementalrelayfee'], Decimal(DEFAULT_INCREMENTAL_RELAY_FEE) / COIN) + self.log.info('Should not accept garbage to testmempoolaccept') assert_raises_rpc_error(-3, 'JSON value of type string is not of expected type array', lambda: node.testmempoolaccept(rawtxs='ff00baar')) assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=['ff22']*26)) diff --git a/test/functional/test_framework/mempool_util.py b/test/functional/test_framework/mempool_util.py index 148cc935ed5..9c514a7d548 100644 --- a/test/functional/test_framework/mempool_util.py +++ b/test/functional/test_framework/mempool_util.py @@ -18,6 +18,10 @@ from .wallet import ( MiniWallet, ) +# Default for -minrelaytxfee in sat/kvB +DEFAULT_MIN_RELAY_TX_FEE = 1000 +# Default for -incrementalrelayfee in sat/kvB +DEFAULT_INCREMENTAL_RELAY_FEE = 1000 def fill_mempool(test_framework, node): """Fill mempool until eviction. From 27b775586ebe702c6ac7fe022e2a42216aefea27 Mon Sep 17 00:00:00 2001 From: glozow Date: Mon, 11 Aug 2025 16:50:58 -0400 Subject: [PATCH 05/19] [doc] assert that default min relay feerate and incremental are the same Github-Pull: #33106 Rebased-From: d6213d6aa114aeed6804a585491d741386fd2739 --- src/node/mempool_args.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/mempool_args.cpp b/src/node/mempool_args.cpp index a488c1b1498..eceff59301b 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -56,6 +56,7 @@ util::Result ApplyArgsManOptions(const ArgsManager& argsman, const CChainP } } + static_assert(DEFAULT_MIN_RELAY_TX_FEE == DEFAULT_INCREMENTAL_RELAY_FEE); if (argsman.IsArgSet("-minrelaytxfee")) { if (std::optional min_relay_feerate = ParseMoney(argsman.GetArg("-minrelaytxfee", ""))) { // High fee check is done afterward in CWallet::Create() From b7ba0167070c93e2c35b3cc1df2979c4a4a2464e Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Tue, 3 Sep 2024 21:55:02 +0200 Subject: [PATCH 06/19] test: add `BulkTransaction` helper to unit test transaction utils The padding method used matches the one used in MiniWallet, `MiniWallet._bulk_tx`. Github-Pull: #30784 Rebased-From: ed7d2246661ec1789b7db0f21668270f0681ea4a --- src/test/util/transaction_utils.cpp | 21 +++++++++++++++++++++ src/test/util/transaction_utils.h | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/src/test/util/transaction_utils.cpp b/src/test/util/transaction_utils.cpp index 300caa577ca..5727da44440 100644 --- a/src/test/util/transaction_utils.cpp +++ b/src/test/util/transaction_utils.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include