From e5f896bb1f052fb8c7811c6024cb49143b427512 Mon Sep 17 00:00:00 2001 From: glozow Date: Thu, 31 Jul 2025 11:29:49 -0400 Subject: [PATCH 01/12] [test] check miner doesn't select 0fee transactions --- 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 a4e482f6a01..6707f4b0b43 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -39,6 +39,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, @@ -143,7 +144,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)...") @@ -154,19 +155,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 85f498893f54ea7d84f2bdf12aa35d198edf8a72 Mon Sep 17 00:00:00 2001 From: glozow Date: Thu, 31 Jul 2025 12:38:36 -0400 Subject: [PATCH 02/12] [test] check bypass of minrelay for various minrelaytxfee settings --- 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 8a4f34e3669..428bfb0b807 100755 --- a/test/functional/mempool_truc.py +++ b/test/functional/mempool_truc.py @@ -11,6 +11,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, @@ -595,12 +596,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() @@ -614,6 +660,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 72dc18467dbfc16cdbda2dd109b087243b397799 Mon Sep 17 00:00:00 2001 From: glozow Date: Thu, 31 Jul 2025 13:53:57 -0400 Subject: [PATCH 03/12] [test] RBF rule 4 for various incrementalrelayfee settings --- 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 3c9b21ddcac..5f462512c7c 100755 --- a/test/functional/feature_rbf.py +++ b/test/functional/feature_rbf.py @@ -13,7 +13,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 @@ -74,6 +77,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): @@ -583,6 +589,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}", "-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_vsize=5000) + 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): # BIP125 signaling is not respected From 1fbee5d7b61b83e68e4230c8a97ca308de92c4c3 Mon Sep 17 00:00:00 2001 From: glozow Date: Mon, 11 Aug 2025 16:58:21 -0400 Subject: [PATCH 04/12] [test] explicitly check default -minrelaytxfee and -incrementalrelayfee --- test/functional/mempool_accept.py | 9 +++++++++ test/functional/test_framework/mempool_util.py | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py index e225b68cc10..dc565b1c80d 100755 --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -10,6 +10,10 @@ import math from test_framework.test_framework import BitcoinTestFramework from test_framework.blocktools import MAX_STANDARD_TX_WEIGHT +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, @@ -85,6 +89,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 b60c14cf7c1..a0b6f5d088f 100644 --- a/test/functional/test_framework/mempool_util.py +++ b/test/functional/test_framework/mempool_util.py @@ -30,6 +30,11 @@ 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 assert_mempool_contents(test_framework, node, expected=None, sync=True): """Assert that all transactions in expected are in the mempool, and no additional ones exist. 'expected' is an array of From d6213d6aa114aeed6804a585491d741386fd2739 Mon Sep 17 00:00:00 2001 From: glozow Date: Mon, 11 Aug 2025 16:50:58 -0400 Subject: [PATCH 05/12] [doc] assert that default min relay feerate and incremental are the same --- 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 11c77ff5610..abbe97d9221 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -65,6 +65,7 @@ util::Result ApplyArgsManOptions(const ArgsManager& argsman, const CChainP } } + static_assert(DEFAULT_MIN_RELAY_TX_FEE == DEFAULT_INCREMENTAL_RELAY_FEE); if (const auto arg{argsman.GetArg("-minrelaytxfee")}) { if (std::optional min_relay_feerate = ParseMoney(*arg)) { // High fee check is done afterward in CWallet::Create() From 5f2df0ef78be7b24798d0983c9b962740608f1f4 Mon Sep 17 00:00:00 2001 From: glozow Date: Tue, 29 Jul 2025 13:32:38 -0400 Subject: [PATCH 06/12] [miner] lower default -blockmintxfee to 1sat/kvB Back when we implemented coin age priority as a miner policy, miners mempools might admit transactions paying very low fees, but then want to set a higher fee for block inclusion. However, since coin age priority was removed in v0.15, the block assembly policy is solely based on fees, so we do not need to apply minimum feerate rules in multiple places. In fact, the block assembly policy ignoring transactions that are added to the mempool is likely undesirable as we waste resources accepting and storing this transaction. Instead, rely on mempool policy to enforce a minimum entry feerate to the mempool (minrelaytxfee). Set the minimum block feerate to the minimum non-zero amount (1sat/kvB) so it collects everything it finds in mempool into the block. --- src/policy/policy.h | 2 +- src/test/miner_tests.cpp | 4 ++++ test/functional/mining_basic.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/policy/policy.h b/src/policy/policy.h index ad787630a45..bccd0866a2e 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -29,7 +29,7 @@ static constexpr unsigned int DEFAULT_BLOCK_RESERVED_WEIGHT{8000}; * Setting a lower value is prevented at startup. */ static constexpr unsigned int MINIMUM_BLOCK_RESERVED_WEIGHT{2000}; /** Default for -blockmintxfee, which sets the minimum feerate for a transaction in blocks created by mining code **/ -static constexpr unsigned int DEFAULT_BLOCK_MIN_TX_FEE{1000}; +static constexpr unsigned int DEFAULT_BLOCK_MIN_TX_FEE{1}; /** The maximum weight for transactions we're willing to relay/mine */ static constexpr int32_t MAX_STANDARD_TX_WEIGHT{400000}; /** The minimum non-witness size for transactions we're willing to relay/mine: one larger than 64 */ diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index 9207f1bfc2c..cf0d103b3c7 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -216,6 +217,9 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const tx.vout.resize(2); tx.vout[0].nValue = 5000000000LL - 100000000; tx.vout[1].nValue = 100000000; // 1BTC output + // Increase size to avoid rounding errors: when the feerate is extremely small (i.e. 1sat/kvB), evaluating the fee + // at a smaller transaction size gives us a rounded value of 0. + BulkTransaction(tx, 4000); Txid hashFreeTx2 = tx.GetHash(); AddToMempool(tx_mempool, entry.Fee(0).SpendsCoinbase(true).FromTx(tx)); diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 6707f4b0b43..4683919d1cc 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -55,7 +55,7 @@ MAX_FUTURE_BLOCK_TIME = 2 * 3600 MAX_TIMEWARP = 600 VERSIONBITS_TOP_BITS = 0x20000000 VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT = 28 -DEFAULT_BLOCK_MIN_TX_FEE = 1000 # default `-blockmintxfee` setting [sat/kvB] +DEFAULT_BLOCK_MIN_TX_FEE = 1 # default `-blockmintxfee` setting [sat/kvB] class MiningTest(BitcoinTestFramework): def set_test_params(self): @@ -144,7 +144,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, 1, 5, 10, 50, 100, 500, 2500, 5000, 21000, 333333, 2500000): + for blockmintxfee_sat_kvb in (DEFAULT_BLOCK_MIN_TX_FEE, 0, 5, 10, 50, 100, 500, 1000, 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)...") From 3eab8b724044dc321f70e5eed66b149713158a04 Mon Sep 17 00:00:00 2001 From: glozow Date: Tue, 29 Jul 2025 14:08:06 -0400 Subject: [PATCH 07/12] [prep/test] replace magic number 1000 with respective feerate vars --- src/test/mempool_tests.cpp | 16 ++++++++-------- test/functional/mempool_limit.py | 9 +++------ test/functional/test_framework/mempool_util.py | 15 ++++++--------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/test/mempool_tests.cpp b/src/test/mempool_tests.cpp index 5992d2a41eb..b75a1cc77ad 100644 --- a/src/test/mempool_tests.cpp +++ b/src/test/mempool_tests.cpp @@ -482,7 +482,7 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) BOOST_CHECK(!pool.exists(tx3.GetHash())); CFeeRate maxFeeRateRemoved(25000, GetVirtualTransactionSize(CTransaction(tx3)) + GetVirtualTransactionSize(CTransaction(tx2))); - BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), maxFeeRateRemoved.GetFeePerK() + 1000); + BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), maxFeeRateRemoved.GetFeePerK() + DEFAULT_INCREMENTAL_RELAY_FEE); CMutableTransaction tx4 = CMutableTransaction(); tx4.vin.resize(2); @@ -559,28 +559,28 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) std::vector vtx; SetMockTime(42); SetMockTime(42 + CTxMemPool::ROLLING_FEE_HALFLIFE); - BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), maxFeeRateRemoved.GetFeePerK() + 1000); + BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), maxFeeRateRemoved.GetFeePerK() + DEFAULT_INCREMENTAL_RELAY_FEE); // ... we should keep the same min fee until we get a block pool.removeForBlock(vtx, 1); SetMockTime(42 + 2*CTxMemPool::ROLLING_FEE_HALFLIFE); - BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), llround((maxFeeRateRemoved.GetFeePerK() + 1000)/2.0)); + BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), llround((maxFeeRateRemoved.GetFeePerK() + DEFAULT_INCREMENTAL_RELAY_FEE)/2.0)); // ... then feerate should drop 1/2 each halflife SetMockTime(42 + 2*CTxMemPool::ROLLING_FEE_HALFLIFE + CTxMemPool::ROLLING_FEE_HALFLIFE/2); - BOOST_CHECK_EQUAL(pool.GetMinFee(pool.DynamicMemoryUsage() * 5 / 2).GetFeePerK(), llround((maxFeeRateRemoved.GetFeePerK() + 1000)/4.0)); + BOOST_CHECK_EQUAL(pool.GetMinFee(pool.DynamicMemoryUsage() * 5 / 2).GetFeePerK(), llround((maxFeeRateRemoved.GetFeePerK() + DEFAULT_INCREMENTAL_RELAY_FEE)/4.0)); // ... with a 1/2 halflife when mempool is < 1/2 its target size SetMockTime(42 + 2*CTxMemPool::ROLLING_FEE_HALFLIFE + CTxMemPool::ROLLING_FEE_HALFLIFE/2 + CTxMemPool::ROLLING_FEE_HALFLIFE/4); - BOOST_CHECK_EQUAL(pool.GetMinFee(pool.DynamicMemoryUsage() * 9 / 2).GetFeePerK(), llround((maxFeeRateRemoved.GetFeePerK() + 1000)/8.0)); + BOOST_CHECK_EQUAL(pool.GetMinFee(pool.DynamicMemoryUsage() * 9 / 2).GetFeePerK(), llround((maxFeeRateRemoved.GetFeePerK() + DEFAULT_INCREMENTAL_RELAY_FEE)/8.0)); // ... with a 1/4 halflife when mempool is < 1/4 its target size SetMockTime(42 + 7*CTxMemPool::ROLLING_FEE_HALFLIFE + CTxMemPool::ROLLING_FEE_HALFLIFE/2 + CTxMemPool::ROLLING_FEE_HALFLIFE/4); - BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), 1000); - // ... but feerate should never drop below 1000 + BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), DEFAULT_INCREMENTAL_RELAY_FEE); + // ... but feerate should never drop below DEFAULT_INCREMENTAL_RELAY_FEE SetMockTime(42 + 8*CTxMemPool::ROLLING_FEE_HALFLIFE + CTxMemPool::ROLLING_FEE_HALFLIFE/2 + CTxMemPool::ROLLING_FEE_HALFLIFE/4); BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), 0); - // ... unless it has gone all the way to 0 (after getting past 1000/2) + // ... unless it has gone all the way to 0 (after getting past DEFAULT_INCREMENTAL_RELAY_FEE/2) } inline CTransactionRef make_tx(std::vector&& output_values, std::vector&& inputs=std::vector(), std::vector&& input_indices=std::vector()) diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py index c9a200cfd9b..2a127007a41 100755 --- a/test/functional/mempool_limit.py +++ b/test/functional/mempool_limit.py @@ -92,8 +92,7 @@ class MempoolLimitTest(BitcoinTestFramework): assert_equal(node.getrawmempool(), []) # Restarting the node resets mempool minimum feerate - assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) - assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + assert_equal(node.getmempoolinfo()['minrelaytxfee'], node.getmempoolinfo()["mempoolminfee"]) fill_mempool(self, node) current_info = node.getmempoolinfo() @@ -184,8 +183,7 @@ class MempoolLimitTest(BitcoinTestFramework): self.restart_node(0, extra_args=self.extra_args[0]) # Restarting the node resets mempool minimum feerate - assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) - assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + assert_equal(node.getmempoolinfo()['minrelaytxfee'], node.getmempoolinfo()["mempoolminfee"]) fill_mempool(self, node) current_info = node.getmempoolinfo() @@ -256,8 +254,7 @@ class MempoolLimitTest(BitcoinTestFramework): relayfee = node.getnetworkinfo()['relayfee'] self.log.info('Check that mempoolminfee is minrelaytxfee') - assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) - assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + assert_equal(node.getmempoolinfo()['minrelaytxfee'], node.getmempoolinfo()["mempoolminfee"]) fill_mempool(self, node) diff --git a/test/functional/test_framework/mempool_util.py b/test/functional/test_framework/mempool_util.py index a0b6f5d088f..78e5f06021b 100644 --- a/test/functional/test_framework/mempool_util.py +++ b/test/functional/test_framework/mempool_util.py @@ -3,7 +3,6 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Helpful routines for mempool testing.""" -from decimal import Decimal import random from .blocktools import ( @@ -62,9 +61,7 @@ def fill_mempool(test_framework, node, *, tx_sync_fun=None): """ test_framework.log.info("Fill the mempool until eviction is triggered and the mempoolminfee rises") txouts = gen_return_txouts() - relayfee = node.getnetworkinfo()['relayfee'] - - assert_equal(relayfee, Decimal('0.00001000')) + minrelayfee = node.getnetworkinfo()['relayfee'] tx_batch_size = 1 num_of_batches = 75 @@ -84,7 +81,7 @@ def fill_mempool(test_framework, node, *, tx_sync_fun=None): test_framework.log.debug("Create a mempool tx that will be evicted") tx_to_be_evicted_id = ephemeral_miniwallet.send_self_transfer( - from_node=node, utxo_to_spend=confirmed_utxos.pop(0), fee_rate=relayfee)["txid"] + from_node=node, utxo_to_spend=confirmed_utxos.pop(0), fee_rate=minrelayfee)["txid"] def send_batch(fee): utxos = confirmed_utxos[:tx_batch_size] @@ -94,14 +91,14 @@ def fill_mempool(test_framework, node, *, tx_sync_fun=None): # Increase the tx fee rate to give the subsequent transactions a higher priority in the mempool # The tx has an approx. vsize of 65k, i.e. multiplying the previous fee rate (in sats/kvB) # by 130 should result in a fee that corresponds to 2x of that fee rate - base_fee = relayfee * 130 + base_fee = minrelayfee * 130 batch_fees = [(i + 1) * base_fee for i in range(num_of_batches)] test_framework.log.debug("Fill up the mempool with txs with higher fee rate") for fee in batch_fees[:-3]: send_batch(fee) tx_sync_fun() if tx_sync_fun else test_framework.sync_mempools() # sync before any eviction - assert_equal(node.getmempoolinfo()["mempoolminfee"], Decimal("0.00001000")) + assert_equal(node.getmempoolinfo()["mempoolminfee"], minrelayfee) for fee in batch_fees[-3:]: send_batch(fee) tx_sync_fun() if tx_sync_fun else test_framework.sync_mempools() # sync after all evictions @@ -113,8 +110,8 @@ def fill_mempool(test_framework, node, *, tx_sync_fun=None): assert tx_to_be_evicted_id not in node.getrawmempool() test_framework.log.debug("Check that mempoolminfee is larger than minrelaytxfee") - assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) - assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + assert_equal(node.getmempoolinfo()['minrelaytxfee'], minrelayfee) + assert_greater_than(node.getmempoolinfo()['mempoolminfee'], minrelayfee) def tx_in_orphanage(node, tx: CTransaction) -> bool: """Returns true if the transaction is in the orphanage.""" From 457cfb61b5323a13218b3cfb5a6a6d8b3a7c5f7f Mon Sep 17 00:00:00 2001 From: glozow Date: Wed, 30 Jul 2025 15:48:34 -0400 Subject: [PATCH 08/12] [prep/util] help MockMempoolMinFee handle more precise feerates Use a virtual size of 1000 to keep precision when using a feerate (which is rounded to the nearest satoshi per kvb) that isn't just an integer. --- src/test/util/setup_common.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 76a42d19ea2..9c9bdc70ccd 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -586,6 +587,9 @@ void TestChain100Setup::MockMempoolMinFee(const CFeeRate& target_feerate) CMutableTransaction mtx = CMutableTransaction(); mtx.vin.emplace_back(COutPoint{Txid::FromUint256(m_rng.rand256()), 0}); mtx.vout.emplace_back(1 * COIN, GetScriptForDestination(WitnessV0ScriptHash(CScript() << OP_TRUE))); + // Set a large size so that the fee evaluated at target_feerate (which is usually in sats/kvB) is an integer. + // Otherwise, GetMinFee() may end up slightly different from target_feerate. + BulkTransaction(mtx, 4000); const auto tx{MakeTransactionRef(mtx)}; LockPoints lp; // The new mempool min feerate is equal to the removed package's feerate + incremental feerate. From 2e515d2897eaa5a9b012eb78aef105e1cf80d42b Mon Sep 17 00:00:00 2001 From: glozow Date: Wed, 30 Jul 2025 15:55:07 -0400 Subject: [PATCH 09/12] [prep/test] make wallet_fundrawtransaction's minrelaytxfee assumption explicit --- test/functional/wallet_fundrawtransaction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py index 2d5c5a2d993..7cf92876b3f 100755 --- a/test/functional/wallet_fundrawtransaction.py +++ b/test/functional/wallet_fundrawtransaction.py @@ -43,7 +43,8 @@ class RawTransactionsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 self.extra_args = [[ - "-deprecatedrpc=settxfee" + "-deprecatedrpc=settxfee", + "-minrelaytxfee=0.00001000", ] for i in range(self.num_nodes)] self.setup_clean_chain = True # whitelist peers to speed up tx relay / mempool sync From 6da5de58cabc4133c379baa50845e30e5bc6b3e4 Mon Sep 17 00:00:00 2001 From: glozow Date: Tue, 29 Jul 2025 14:37:16 -0400 Subject: [PATCH 10/12] [policy] lower default minrelaytxfee and incrementalrelayfee to 100sat/kvB Let's say an attacker wants to use/exhaust the network's bandwidth, and has the choice between renting resources from a commercial provider and getting the network to "spam" itself it by sending unconfirmed transactions. We'd like the latter to be more expensive than the former. The bandwidth for relaying a transaction across the network is roughly its serialized size (plus relay overhead) x number of nodes. A 1000vB transaction is 1000-4000B serialized. With 100k nodes, that's 0.1-0.4GB If the going rate for commercial services is 10c/GB, that's like 1-4c per kvB of transaction data, so a 1000vB transaction should pay at least $0.04. At a price of 120k USD/BTC, 100sat is about $0.12. This price allows us to tolerate a large decrease in the conversion rate or increase in the number of nodes. --- src/policy/policy.h | 4 ++-- src/test/mempool_tests.cpp | 24 +++++++++---------- src/test/rbf_tests.cpp | 8 +++---- test/functional/feature_rbf.py | 2 +- test/functional/mempool_ephemeral_dust.py | 2 +- test/functional/mempool_limit.py | 6 ++--- test/functional/mempool_package_rbf.py | 9 +++---- test/functional/p2p_1p1c_network.py | 17 +++++++------ test/functional/p2p_ibd_txrelay.py | 8 +++---- test/functional/p2p_opportunistic_1p1c.py | 8 ++++--- .../functional/test_framework/mempool_util.py | 4 ++-- test/functional/wallet_bumpfee.py | 4 ++-- 12 files changed, 49 insertions(+), 47 deletions(-) diff --git a/src/policy/policy.h b/src/policy/policy.h index bccd0866a2e..ce8bfc6aef9 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -41,7 +41,7 @@ static constexpr unsigned int MAX_STANDARD_TX_SIGOPS_COST{MAX_BLOCK_SIGOPS_COST/ /** The maximum number of potentially executed legacy signature operations in a single standard tx */ static constexpr unsigned int MAX_TX_LEGACY_SIGOPS{2'500}; /** Default for -incrementalrelayfee, which sets the minimum feerate increase for mempool limiting or replacement **/ -static constexpr unsigned int DEFAULT_INCREMENTAL_RELAY_FEE{1000}; +static constexpr unsigned int DEFAULT_INCREMENTAL_RELAY_FEE{100}; /** Default for -bytespersigop */ static constexpr unsigned int DEFAULT_BYTES_PER_SIGOP{20}; /** Default for -permitbaremultisig */ @@ -63,7 +63,7 @@ static constexpr unsigned int MAX_STANDARD_SCRIPTSIG_SIZE{1650}; * outputs below the new threshold */ static constexpr unsigned int DUST_RELAY_TX_FEE{3000}; /** Default for -minrelaytxfee, minimum relay fee for transactions */ -static constexpr unsigned int DEFAULT_MIN_RELAY_TX_FEE{1000}; +static constexpr unsigned int DEFAULT_MIN_RELAY_TX_FEE{100}; /** Default for -limitancestorcount, max number of in-mempool ancestors */ static constexpr unsigned int DEFAULT_ANCESTOR_LIMIT{25}; /** Default for -limitancestorsize, maximum kilobytes of tx + all in-mempool ancestors */ diff --git a/src/test/mempool_tests.cpp b/src/test/mempool_tests.cpp index b75a1cc77ad..5f8b63dbdd7 100644 --- a/src/test/mempool_tests.cpp +++ b/src/test/mempool_tests.cpp @@ -443,7 +443,7 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) tx1.vout.resize(1); tx1.vout[0].scriptPubKey = CScript() << OP_1 << OP_EQUAL; tx1.vout[0].nValue = 10 * COIN; - AddToMempool(pool, entry.Fee(10000LL).FromTx(tx1)); + AddToMempool(pool, entry.Fee(1000LL).FromTx(tx1)); CMutableTransaction tx2 = CMutableTransaction(); tx2.vin.resize(1); @@ -451,7 +451,7 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) tx2.vout.resize(1); tx2.vout[0].scriptPubKey = CScript() << OP_2 << OP_EQUAL; tx2.vout[0].nValue = 10 * COIN; - AddToMempool(pool, entry.Fee(5000LL).FromTx(tx2)); + AddToMempool(pool, entry.Fee(500LL).FromTx(tx2)); pool.TrimToSize(pool.DynamicMemoryUsage()); // should do nothing BOOST_CHECK(pool.exists(tx1.GetHash())); @@ -469,7 +469,7 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) tx3.vout.resize(1); tx3.vout[0].scriptPubKey = CScript() << OP_3 << OP_EQUAL; tx3.vout[0].nValue = 10 * COIN; - AddToMempool(pool, entry.Fee(20000LL).FromTx(tx3)); + AddToMempool(pool, entry.Fee(2000LL).FromTx(tx3)); pool.TrimToSize(pool.DynamicMemoryUsage() * 3 / 4); // tx3 should pay for tx2 (CPFP) BOOST_CHECK(!pool.exists(tx1.GetHash())); @@ -481,7 +481,7 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) BOOST_CHECK(!pool.exists(tx2.GetHash())); BOOST_CHECK(!pool.exists(tx3.GetHash())); - CFeeRate maxFeeRateRemoved(25000, GetVirtualTransactionSize(CTransaction(tx3)) + GetVirtualTransactionSize(CTransaction(tx2))); + CFeeRate maxFeeRateRemoved(2500, GetVirtualTransactionSize(CTransaction(tx3)) + GetVirtualTransactionSize(CTransaction(tx2))); BOOST_CHECK_EQUAL(pool.GetMinFee(1).GetFeePerK(), maxFeeRateRemoved.GetFeePerK() + DEFAULT_INCREMENTAL_RELAY_FEE); CMutableTransaction tx4 = CMutableTransaction(); @@ -532,10 +532,10 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) tx7.vout[1].scriptPubKey = CScript() << OP_7 << OP_EQUAL; tx7.vout[1].nValue = 10 * COIN; - AddToMempool(pool, entry.Fee(7000LL).FromTx(tx4)); - AddToMempool(pool, entry.Fee(1000LL).FromTx(tx5)); - AddToMempool(pool, entry.Fee(1100LL).FromTx(tx6)); - AddToMempool(pool, entry.Fee(9000LL).FromTx(tx7)); + AddToMempool(pool, entry.Fee(700LL).FromTx(tx4)); + AddToMempool(pool, entry.Fee(100LL).FromTx(tx5)); + AddToMempool(pool, entry.Fee(110LL).FromTx(tx6)); + AddToMempool(pool, entry.Fee(900LL).FromTx(tx7)); // we only require this to remove, at max, 2 txn, because it's not clear what we're really optimizing for aside from that pool.TrimToSize(pool.DynamicMemoryUsage() - 1); @@ -544,8 +544,8 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) BOOST_CHECK(!pool.exists(tx7.GetHash())); if (!pool.exists(tx5.GetHash())) - AddToMempool(pool, entry.Fee(1000LL).FromTx(tx5)); - AddToMempool(pool, entry.Fee(9000LL).FromTx(tx7)); + AddToMempool(pool, entry.Fee(100LL).FromTx(tx5)); + AddToMempool(pool, entry.Fee(900LL).FromTx(tx7)); pool.TrimToSize(pool.DynamicMemoryUsage() / 2); // should maximize mempool size by only removing 5/7 BOOST_CHECK(pool.exists(tx4.GetHash())); @@ -553,8 +553,8 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest) BOOST_CHECK(pool.exists(tx6.GetHash())); BOOST_CHECK(!pool.exists(tx7.GetHash())); - AddToMempool(pool, entry.Fee(1000LL).FromTx(tx5)); - AddToMempool(pool, entry.Fee(9000LL).FromTx(tx7)); + AddToMempool(pool, entry.Fee(100LL).FromTx(tx5)); + AddToMempool(pool, entry.Fee(900LL).FromTx(tx7)); std::vector vtx; SetMockTime(42); diff --git a/src/test/rbf_tests.cpp b/src/test/rbf_tests.cpp index 0052276eac4..ade698b017d 100644 --- a/src/test/rbf_tests.cpp +++ b/src/test/rbf_tests.cpp @@ -238,10 +238,10 @@ BOOST_FIXTURE_TEST_CASE(rbf_helper_functions, TestChain100Setup) BOOST_CHECK(PaysForRBF(high_fee, high_fee - 1, 1, CFeeRate(0), unused_txid).has_value()); BOOST_CHECK(PaysForRBF(high_fee + 1, high_fee, 1, CFeeRate(0), unused_txid).has_value()); // Additional fees must cover the replacement's vsize at incremental relay fee - BOOST_CHECK(PaysForRBF(high_fee, high_fee + 1, 2, incremental_relay_feerate, unused_txid).has_value()); - BOOST_CHECK(PaysForRBF(high_fee, high_fee + 2, 2, incremental_relay_feerate, unused_txid) == std::nullopt); - BOOST_CHECK(PaysForRBF(high_fee, high_fee + 2, 2, higher_relay_feerate, unused_txid).has_value()); - BOOST_CHECK(PaysForRBF(high_fee, high_fee + 4, 2, higher_relay_feerate, unused_txid) == std::nullopt); + BOOST_CHECK(PaysForRBF(high_fee, high_fee + 1, 11, incremental_relay_feerate, unused_txid).has_value()); + BOOST_CHECK(PaysForRBF(high_fee, high_fee + 1, 10, incremental_relay_feerate, unused_txid) == std::nullopt); + BOOST_CHECK(PaysForRBF(high_fee, high_fee + 2, 11, higher_relay_feerate, unused_txid).has_value()); + BOOST_CHECK(PaysForRBF(high_fee, high_fee + 4, 20, higher_relay_feerate, unused_txid) == std::nullopt); BOOST_CHECK(PaysForRBF(low_fee, high_fee, 99999999, incremental_relay_feerate, unused_txid).has_value()); BOOST_CHECK(PaysForRBF(low_fee, high_fee + 99999999, 99999999, incremental_relay_feerate, unused_txid) == std::nullopt); diff --git a/test/functional/feature_rbf.py b/test/functional/feature_rbf.py index 5f462512c7c..4117326b883 100755 --- a/test/functional/feature_rbf.py +++ b/test/functional/feature_rbf.py @@ -585,7 +585,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): # Higher fee, higher feerate, different txid, but the replacement does not provide a relay # fee conforming to node's `incrementalrelayfee` policy of 1000 sat per KB. - assert_equal(self.nodes[0].getmempoolinfo()["incrementalrelayfee"], Decimal("0.00001")) + assert_equal(self.nodes[0].getmempoolinfo()["incrementalrelayfee"], Decimal("0.000001")) tx.vout[0].nValue -= 1 assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex()) diff --git a/test/functional/mempool_ephemeral_dust.py b/test/functional/mempool_ephemeral_dust.py index fd77eac3e2b..a0308da72ac 100755 --- a/test/functional/mempool_ephemeral_dust.py +++ b/test/functional/mempool_ephemeral_dust.py @@ -216,7 +216,7 @@ class EphemeralDustTest(BitcoinTestFramework): res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]]) assert_equal(res["package_msg"], "transaction failed") - assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "min relay fee not met, 0 < 147") + assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "min relay fee not met, 0 < 15") assert_equal(self.nodes[0].getrawmempool(), []) diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py index 2a127007a41..72696062457 100755 --- a/test/functional/mempool_limit.py +++ b/test/functional/mempool_limit.py @@ -206,7 +206,7 @@ class MempoolLimitTest(BitcoinTestFramework): # coin is no longer available, but the cache could still contain the tx. cpfp_parent = self.wallet.create_self_transfer( utxo_to_spend=replaced_tx["new_utxo"], - fee_rate=mempoolmin_feerate - Decimal('0.00001'), + fee_rate=mempoolmin_feerate - Decimal('0.000001'), confirmed_only=True) self.wallet.rescan_utxos() @@ -312,9 +312,9 @@ class MempoolLimitTest(BitcoinTestFramework): target_vsize_each = 50000 assert_greater_than(target_vsize_each * 2 * 3, node.getmempoolinfo()["maxmempool"] - node.getmempoolinfo()["bytes"]) # Should be a true CPFP: parent's feerate is just below mempool min feerate - parent_feerate = mempoolmin_feerate - Decimal("0.000001") # 0.1 sats/vbyte below min feerate + parent_feerate = mempoolmin_feerate - Decimal("0.0000001") # 0.01 sats/vbyte below min feerate # Parent + child is above mempool minimum feerate - child_feerate = (worst_feerate_btcvb * 1000) - Decimal("0.000001") # 0.1 sats/vbyte below worst feerate + child_feerate = (worst_feerate_btcvb * 1000) - Decimal("0.0000001") # 0.01 sats/vbyte below worst feerate # However, when eviction is triggered, these transactions should be at the bottom. # This assertion assumes parent and child are the same size. miniwallet.rescan_utxos() diff --git a/test/functional/mempool_package_rbf.py b/test/functional/mempool_package_rbf.py index ca56bdf8c5f..54f3a90c0ac 100755 --- a/test/functional/mempool_package_rbf.py +++ b/test/functional/mempool_package_rbf.py @@ -162,13 +162,13 @@ class PackageRBFTest(BitcoinTestFramework): self.log.info("Check replacement pays for incremental bandwidth") _, placeholder_txns3 = self.create_simple_package(coin) package_3_size = sum([tx.get_vsize() for tx in placeholder_txns3]) - incremental_sats_required = Decimal(package_3_size) / COIN - incremental_sats_short = incremental_sats_required - Decimal("0.00000001") + incremental_sats_required = (Decimal(package_3_size * 0.1) / COIN).quantize(Decimal("0.00000001")) + incremental_sats_short = incremental_sats_required - Decimal("0.00000005") # Recreate the package with slightly higher fee once we know the size of the new package, but still short of required fee failure_package_hex3, failure_package_txns3 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE + incremental_sats_short) assert_equal(package_3_size, sum([tx.get_vsize() for tx in failure_package_txns3])) pkg_results3 = node.submitpackage(failure_package_hex3) - assert_equal(f"package RBF failed: insufficient anti-DoS fees, rejecting replacement {failure_package_txns3[1].txid_hex}, not enough additional fees to relay; {incremental_sats_short} < {incremental_sats_required}", pkg_results3["package_msg"]) + assert_equal(f"package RBF failed: insufficient anti-DoS fees, rejecting replacement {failure_package_txns3[1].txid_hex}, not enough additional fees to relay; {incremental_sats_short:8f} < {incremental_sats_required:8f}", pkg_results3["package_msg"]) self.assert_mempool_contents(expected=package_txns1) success_package_hex3, success_package_txns3 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE + incremental_sats_required) @@ -562,12 +562,13 @@ class PackageRBFTest(BitcoinTestFramework): ) node.sendrawtransaction(grandparent_result["hex"]) + minrelayfeerate = node.getnetworkinfo()["relayfee"] # Now make package of two descendants that looks # like a cpfp where the parent can't get in on its own self.ctr += 1 parent_result = self.wallet.create_self_transfer( - fee_rate=Decimal('0.00001000'), + fee_rate=minrelayfeerate, utxo_to_spend=grandparent_result["new_utxo"], sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr, ) diff --git a/test/functional/p2p_1p1c_network.py b/test/functional/p2p_1p1c_network.py index 4d0f11ea99b..e4d3b738c19 100755 --- a/test/functional/p2p_1p1c_network.py +++ b/test/functional/p2p_1p1c_network.py @@ -13,9 +13,11 @@ from decimal import Decimal from math import ceil from test_framework.mempool_util import ( + DEFAULT_MIN_RELAY_TX_FEE, fill_mempool, ) from test_framework.messages import ( + COIN, msg_tx, ) from test_framework.p2p import ( @@ -31,9 +33,6 @@ from test_framework.wallet import ( MiniWalletMode, ) -# 1sat/vB feerate denominated in BTC/KvB -FEERATE_1SAT_VB = Decimal("0.00001000") - class PackageRelayTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True @@ -49,12 +48,12 @@ class PackageRelayTest(BitcoinTestFramework): self.log.debug("Check that all nodes' mempool minimum feerates are above min relay feerate") for node in self.nodes: - assert_equal(node.getmempoolinfo()['minrelaytxfee'], FEERATE_1SAT_VB) - assert_greater_than(node.getmempoolinfo()['mempoolminfee'], FEERATE_1SAT_VB) + assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN) + assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN) def create_basic_1p1c(self, wallet): - low_fee_parent = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, confirmed_only=True) - high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB) + low_fee_parent = wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN, confirmed_only=True) + high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*Decimal(DEFAULT_MIN_RELAY_TX_FEE)/ COIN) package_hex_basic = [low_fee_parent["hex"], high_fee_child["hex"]] return package_hex_basic, low_fee_parent["tx"], high_fee_child["tx"] @@ -85,8 +84,8 @@ class PackageRelayTest(BitcoinTestFramework): return [low_fee_parent_2outs["hex"], high_fee_child_2outs["hex"]], low_fee_parent_2outs["tx"], high_fee_child_2outs["tx"] def create_package_2p1c(self, wallet): - parent1 = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True) - parent2 = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*20, confirmed_only=True) + parent1 = wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN * 10, confirmed_only=True) + parent2 = wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN * 20, confirmed_only=True) child = wallet.create_self_transfer_multi( utxos_to_spend=[parent1["new_utxo"], parent2["new_utxo"]], fee_per_output=999*parent1["tx"].get_vsize(), diff --git a/test/functional/p2p_ibd_txrelay.py b/test/functional/p2p_ibd_txrelay.py index 870324d76c3..0cd0ac05b2c 100755 --- a/test/functional/p2p_ibd_txrelay.py +++ b/test/functional/p2p_ibd_txrelay.py @@ -28,8 +28,8 @@ from test_framework.p2p import ( ) from test_framework.test_framework import BitcoinTestFramework -MAX_FEE_FILTER = Decimal(9170997) / COIN -NORMAL_FEE_FILTER = Decimal(100) / COIN +MAX_FEE_FILTER = Decimal(9936506) / COIN +NORMAL_FEE_FILTER = Decimal(10) / COIN class P2PIBDTxRelayTest(BitcoinTestFramework): @@ -37,8 +37,8 @@ class P2PIBDTxRelayTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [ - ["-minrelaytxfee={}".format(NORMAL_FEE_FILTER)], - ["-minrelaytxfee={}".format(NORMAL_FEE_FILTER)], + ["-minrelaytxfee={:.8f}".format(NORMAL_FEE_FILTER)], + ["-minrelaytxfee={:.8f}".format(NORMAL_FEE_FILTER)], ] def run_test(self): diff --git a/test/functional/p2p_opportunistic_1p1c.py b/test/functional/p2p_opportunistic_1p1c.py index ad42b7308ba..590d84999e5 100755 --- a/test/functional/p2p_opportunistic_1p1c.py +++ b/test/functional/p2p_opportunistic_1p1c.py @@ -13,10 +13,12 @@ import time from test_framework.blocktools import MAX_STANDARD_TX_WEIGHT from test_framework.mempool_util import ( create_large_orphan, + DEFAULT_MIN_RELAY_TX_FEE, fill_mempool, ) from test_framework.messages import ( CInv, + COIN, COutPoint, CTransaction, CTxIn, @@ -79,13 +81,13 @@ class PackageRelayTest(BitcoinTestFramework): ]] def create_tx_below_mempoolminfee(self, wallet, utxo_to_spend=None): - """Create a 1-input 1sat/vB transaction using a confirmed UTXO. Decrement and use + """Create a 1-input 0.1sat/vB transaction using a confirmed UTXO. Decrement and use self.sequence so that subsequent calls to this function result in unique transactions.""" self.sequence -= 1 - assert_greater_than(self.nodes[0].getmempoolinfo()["mempoolminfee"], FEERATE_1SAT_VB) + assert_greater_than(self.nodes[0].getmempoolinfo()["mempoolminfee"], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN) - return wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, sequence=self.sequence, utxo_to_spend=utxo_to_spend, confirmed_only=True) + return wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN, sequence=self.sequence, utxo_to_spend=utxo_to_spend, confirmed_only=True) @cleanup def test_basic_child_then_parent(self): diff --git a/test/functional/test_framework/mempool_util.py b/test/functional/test_framework/mempool_util.py index 78e5f06021b..901a4ed62e6 100644 --- a/test/functional/test_framework/mempool_util.py +++ b/test/functional/test_framework/mempool_util.py @@ -30,9 +30,9 @@ from .wallet import ( ) # Default for -minrelaytxfee in sat/kvB -DEFAULT_MIN_RELAY_TX_FEE = 1000 +DEFAULT_MIN_RELAY_TX_FEE = 100 # Default for -incrementalrelayfee in sat/kvB -DEFAULT_INCREMENTAL_RELAY_FEE = 1000 +DEFAULT_INCREMENTAL_RELAY_FEE = 100 def assert_mempool_contents(test_framework, node, expected=None, sync=True): """Assert that all transactions in expected are in the mempool, diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 05965caacd3..097c39575a3 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -535,7 +535,7 @@ def test_dust_to_fee(self, rbf_node, dest_address): def test_settxfee(self, rbf_node, dest_address): self.log.info('Test settxfee') - assert_raises_rpc_error(-8, "txfee cannot be less than min relay tx fee", rbf_node.settxfee, Decimal('0.000005')) + assert_raises_rpc_error(-8, "txfee cannot be less than min relay tx fee", rbf_node.settxfee, Decimal('0.0000005')) assert_raises_rpc_error(-8, "txfee cannot be less than wallet min fee", rbf_node.settxfee, Decimal('0.000015')) # check that bumpfee reacts correctly to the use of settxfee (paytxfee) rbfid = spend_one_input(rbf_node, dest_address) @@ -850,7 +850,7 @@ def test_bumpfee_with_feerate_ignores_walletincrementalrelayfee(self, rbf_node, # Ensure you can not fee bump if the fee_rate is more than original fee_rate but the total fee from new fee_rate is # less than (original fee + incrementalrelayfee) - assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, tx["txid"], {"fee_rate": 2.8}) + assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, tx["txid"], {"fee_rate": 2.05}) # You can fee bump as long as the new fee set from fee_rate is at least (original fee + incrementalrelayfee) rbf_node.bumpfee(tx["txid"], {"fee_rate": 3}) From 18720bc5d5b4d3acf91060859180d72cbfdf59b7 Mon Sep 17 00:00:00 2001 From: glozow Date: Thu, 31 Jul 2025 09:45:00 -0400 Subject: [PATCH 11/12] [doc] release note for min feerate changes --- doc/release-notes-33106.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 doc/release-notes-33106.md diff --git a/doc/release-notes-33106.md b/doc/release-notes-33106.md new file mode 100644 index 00000000000..95750cedff9 --- /dev/null +++ b/doc/release-notes-33106.md @@ -0,0 +1,17 @@ +Mining and Transaction Relay Policy +========================= + +The minimum block feerate (`-blockmintxfee`) has been changed to 1 satoshi per kvB. It can still be changed using the +configuration option. + +The default minimum relay feerate (`-minrelaytxfee`) and incremental relay feerate (`-incrementalrelayfee`) have been +changed to 100 satoshis per kvB. They can still be changed using their respective configuration options, but it is +recommended to change both together if you decide to do so. + +Other minimum feerates (e.g. the dust feerate, the minimum returned by the fee estimator, and all feerates used by the +wallet) remain unchanged. The mempool minimum feerate still changes in response to high volume but more gradually, as a +result of the change to the incremental relay feerate. + +Note that unless these lower defaults are widely adopted across the network, transactions created with lower fee rates +are not guaranteed to propagate or confirm. The wallet feerates remain unchanged; `-mintxfee` must be changed before +attempting to create transactions with lower feerates using the wallet. From ba84a25deec0b3b9b94ee51b373e715fec995791 Mon Sep 17 00:00:00 2001 From: glozow Date: Mon, 11 Aug 2025 17:12:23 -0400 Subject: [PATCH 12/12] [doc] update mempool-replacements.md for incremental relay feerate change --- doc/policy/mempool-replacements.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/policy/mempool-replacements.md b/doc/policy/mempool-replacements.md index eb370672e40..73682e2ffb3 100644 --- a/doc/policy/mempool-replacements.md +++ b/doc/policy/mempool-replacements.md @@ -32,8 +32,8 @@ other consensus and policy rules, each of the following conditions are met: 4. The additional fees (difference between absolute fee paid by the replacement transaction and the sum paid by the original transactions) pays for the replacement transaction's bandwidth at or above the rate set by the node's incremental relay feerate. For example, if the incremental relay - feerate is 1 satoshi/vB and the replacement transaction is 500 virtual bytes total, then the - replacement pays a fee at least 500 satoshis higher than the sum of the original transactions. + feerate is 0.1 satoshi/vB and the replacement transaction is 500 virtual bytes total, then the + replacement pays a fee at least 50 satoshis higher than the sum of the original transactions. *Rationale*: Try to prevent DoS attacks where an attacker causes the network to repeatedly relay transactions each paying a tiny additional amount in fees, e.g. just 1 satoshi. @@ -77,3 +77,5 @@ This set of rules is similar but distinct from BIP125. * Full replace-by-fee is the default policy as of **v28.0** ([PR #30493](https://github.com/bitcoin/bitcoin/pull/30493)). * Signaling for replace-by-fee is no longer required as of [PR 30592](https://github.com/bitcoin/bitcoin/pull/30592). + +* The incremental relay feerate default is 0.1sat/vB ([PR #33106](https://github.com/bitcoin/bitcoin/pull/33106)).