policy: Allow any transaction version with < minrelay

Prior to cluster mempool, a policy was in place that
disallowed non-TRUC transactions from being
TX_RECONSIDERABLE in a package setting if it was below
minrelay. This was meant to simplify reasoning about mempool
trimming requirements with non-trivial transaction
topologies in the mempool. This is no longer a concern
post-cluster mempool, so this is relaxed.

In effect, this makes 0-value parent transactions relayable
through the network without the TRUC restrictions and
thus the anti-pinning protections.
This commit is contained in:
Greg Sanders committed 2025-11-26 09:18:29 -05:00
1 parent 5336bcd578
commit 1488315d76
6 files changed
+26 -53

No files matched your search

-8
View File
@@ -99,14 +99,6 @@ submitted as a package.
transaction (i.e. in which a replacement transaction with a higher fee cannot be signed) being transaction (i.e. in which a replacement transaction with a higher fee cannot be signed) being
rejected from the mempool when transaction volume is high and the mempool minimum feerate rises. rejected from the mempool when transaction volume is high and the mempool minimum feerate rises.
Note: Package feerate cannot be used to meet the minimum relay feerate (`-minrelaytxfee`)
requirement. For example, if the mempool minimum feerate is 5sat/vB and the minimum relay feerate is
set to 5sat/vB, a 1sat/vB parent transaction with a high-feerate child will not be accepted, even if
submitted as a package. Note that this rule does not apply to
[TRUC transactions](https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki) as an individual
TRUC transaction is permitted to be below the mempool min relay feerate, assuming it is considered within
a package that meets the mempool's feerate requirements.
*Rationale*: Avoid situations in which the mempool contains non-bumped transactions below min relay *Rationale*: Avoid situations in which the mempool contains non-bumped transactions below min relay
feerate (which we consider to have pay 0 fees and thus receiving free relay). While package feerate (which we consider to have pay 0 fees and thus receiving free relay). While package
submission would ensure these transactions are bumped at the time of entry, it is not guaranteed submission would ensure these transactions are bumped at the time of entry, it is not guaranteed
+5 -5
View File
@@ -908,10 +908,10 @@ BOOST_AUTO_TEST_CASE(package_cpfp_tests)
} else { } else {
BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_state.GetResult(), PackageValidationResult::PCKG_TX); BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_state.GetResult(), PackageValidationResult::PCKG_TX);
BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_tx_results.find(tx_parent->GetWitnessHash())->second.m_state.GetResult(), BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_tx_results.find(tx_parent->GetWitnessHash())->second.m_state.GetResult(),
TxValidationResult::TX_MEMPOOL_POLICY); TxValidationResult::TX_RECONSIDERABLE);
BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_tx_results.find(tx_child->GetWitnessHash())->second.m_state.GetResult(), BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_tx_results.find(tx_child->GetWitnessHash())->second.m_state.GetResult(),
TxValidationResult::TX_MISSING_INPUTS); TxValidationResult::TX_RECONSIDERABLE);
BOOST_CHECK(submit_cpfp_deprio.m_tx_results.find(tx_parent->GetWitnessHash())->second.m_state.GetRejectReason() == "min relay fee not met"); BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_tx_results.find(tx_parent->GetWitnessHash())->second.m_state.GetRejectReason(), "mempool min fee not met");
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
} }
} }
@@ -1067,8 +1067,8 @@ BOOST_AUTO_TEST_CASE(package_cpfp_tests)
strprintf("rich parent: expected fee %s, got %s", high_parent_fee, it_parent->second.m_base_fees.value())); strprintf("rich parent: expected fee %s, got %s", high_parent_fee, it_parent->second.m_base_fees.value()));
BOOST_CHECK(it_parent->second.m_effective_feerate == CFeeRate(high_parent_fee, GetVirtualTransactionSize(*tx_parent_rich))); BOOST_CHECK(it_parent->second.m_effective_feerate == CFeeRate(high_parent_fee, GetVirtualTransactionSize(*tx_parent_rich)));
BOOST_CHECK_EQUAL(it_child->second.m_result_type, MempoolAcceptResult::ResultType::INVALID); BOOST_CHECK_EQUAL(it_child->second.m_result_type, MempoolAcceptResult::ResultType::INVALID);
BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(), TxValidationResult::TX_MEMPOOL_POLICY); BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(), TxValidationResult::TX_RECONSIDERABLE);
BOOST_CHECK(it_child->second.m_state.GetRejectReason() == "min relay fee not met"); BOOST_CHECK_EQUAL(it_child->second.m_state.GetRejectReason(), "mempool min fee not met");
} }
expected_pool_size += 1; expected_pool_size += 1;
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
-12
View File
@@ -942,18 +942,6 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "bad-txns-too-many-sigops", return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "bad-txns-too-many-sigops",
strprintf("%d", nSigOpsCost)); strprintf("%d", nSigOpsCost));
// No individual transactions are allowed below the min relay feerate except from disconnected blocks.
// This requirement, unlike CheckFeeRate, cannot be bypassed using m_package_feerates because,
// while a tx could be package CPFP'd when entering the mempool, we do not have a DoS-resistant
// method of ensuring the tx remains bumped. For example, the fee-bumping child could disappear
// due to a replacement.
// The only exception is TRUC transactions.
if (!bypass_limits && ws.m_ptx->version != TRUC_VERSION && ws.m_modified_fees < m_pool.m_opts.min_relay_feerate.GetFee(ws.m_vsize)) {
// Even though this is a fee-related failure, this result is TX_MEMPOOL_POLICY, not
// TX_RECONSIDERABLE, because it cannot be bypassed using package validation.
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met",
strprintf("%d < %d", ws.m_modified_fees, m_pool.m_opts.min_relay_feerate.GetFee(ws.m_vsize)));
}
// No individual transactions are allowed below the mempool min feerate except from disconnected // No individual transactions are allowed below the mempool min feerate except from disconnected
// blocks and transactions in a package. Package transactions will be checked using package // blocks and transactions in a package. Package transactions will be checked using package
// feerate later. // feerate later.
+4 -6
View File
@@ -207,18 +207,16 @@ class EphemeralDustTest(BitcoinTestFramework):
self.connect_nodes(0, 1) self.connect_nodes(0, 1)
assert_mempool_contents(self, self.nodes[0], expected=[]) assert_mempool_contents(self, self.nodes[0], expected=[])
# N.B. If individual minrelay requirement is dropped, this test can be dropped
def test_non_truc(self): def test_non_truc(self):
self.log.info("Test that v2 dust-having transaction is rejected even if spent, because of min relay requirement") self.log.info("Test that v2 dust-having transaction is also accepted if spent")
assert_equal(self.nodes[0].getrawmempool(), []) assert_equal(self.nodes[0].getrawmempool(), [])
dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=2) dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=2)
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]]) res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
assert_equal(res["package_msg"], "transaction failed") assert_equal(res["package_msg"], "success")
assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "min relay fee not met, 0 < 15") assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
self.generate(self.nodes[0], 1)
assert_equal(self.nodes[0].getrawmempool(), [])
def test_unspent_ephemeral(self): def test_unspent_ephemeral(self):
self.log.info("Test that spending from a tx with ephemeral outputs is only allowed if dust is spent as well") self.log.info("Test that spending from a tx with ephemeral outputs is only allowed if dust is spent as well")
+3 -9
View File
@@ -598,7 +598,7 @@ class MempoolTRUC(BitcoinTestFramework):
@cleanup(extra_args=None) @cleanup(extra_args=None)
def test_minrelay_in_package_combos(self): def test_minrelay_in_package_combos(self):
node = self.nodes[0] node = self.nodes[0]
self.log.info("Test that only TRUC transactions can be under minrelaytxfee for various settings...") self.log.info("Test that all transaction versions can be under minrelaytxfee for various settings...")
for minrelay_setting in (0, 5, 10, 100, 500, 1000, 5000, 333333, 2500000): for minrelay_setting in (0, 5, 10, 100, 500, 1000, 5000, 333333, 2500000):
self.log.info(f"-> Test -minrelaytxfee={minrelay_setting}sat/kvB...") self.log.info(f"-> Test -minrelaytxfee={minrelay_setting}sat/kvB...")
@@ -635,14 +635,8 @@ class MempoolTRUC(BitcoinTestFramework):
assert_equal(result_truc["package_msg"], "success") assert_equal(result_truc["package_msg"], "success")
result_non_truc = node.submitpackage([tx_v2_0fee_parent["hex"], tx_v2_child["hex"]], maxfeerate=0) 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"], "success")
assert_equal(result_non_truc["package_msg"], "transaction failed") self.check_mempool([tx_v2_0fee_parent["txid"], tx_v2_child["txid"], tx_v3_0fee_parent["txid"], tx_v3_child["txid"]])
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): def run_test(self):
+14 -13
View File
@@ -5,6 +5,7 @@
import time import time
from test_framework.blocktools import MAX_STANDARD_TX_WEIGHT
from test_framework.mempool_util import ( from test_framework.mempool_util import (
create_large_orphan, create_large_orphan,
tx_in_orphanage, tx_in_orphanage,
@@ -188,15 +189,15 @@ class OrphanHandlingTest(BitcoinTestFramework):
peer2 = node.add_p2p_connection(PeerTxRelayer()) peer2 = node.add_p2p_connection(PeerTxRelayer())
self.log.info("Test orphan handling when a nonsegwit parent is known to be invalid") self.log.info("Test orphan handling when a nonsegwit parent is known to be invalid")
parent_low_fee_nonsegwit = self.wallet_nonsegwit.create_self_transfer(fee_rate=0) parent_overly_large_nonsegwit = self.wallet_nonsegwit.create_self_transfer(target_vsize=int(MAX_STANDARD_TX_WEIGHT / 4) + 1)
assert_equal(parent_low_fee_nonsegwit["txid"], parent_low_fee_nonsegwit["tx"].wtxid_hex) assert_equal(parent_overly_large_nonsegwit["txid"], parent_overly_large_nonsegwit["tx"].wtxid_hex)
parent_other = self.wallet_nonsegwit.create_self_transfer() parent_other = self.wallet_nonsegwit.create_self_transfer()
child_nonsegwit = self.wallet_nonsegwit.create_self_transfer_multi( child_nonsegwit = self.wallet_nonsegwit.create_self_transfer_multi(
utxos_to_spend=[parent_other["new_utxo"], parent_low_fee_nonsegwit["new_utxo"]]) utxos_to_spend=[parent_other["new_utxo"], parent_overly_large_nonsegwit["new_utxo"]])
# Relay the parent. It should be rejected because it pays 0 fees. # Relay the parent. It should be rejected (and not reconsiderable) because it violated size limitations.
self.relay_transaction(peer1, parent_low_fee_nonsegwit["tx"]) self.relay_transaction(peer1, parent_overly_large_nonsegwit["tx"])
assert parent_low_fee_nonsegwit["txid"] not in node.getrawmempool() assert parent_overly_large_nonsegwit["txid"] not in node.getrawmempool()
# Relay the child. It should not be accepted because it has missing inputs. # Relay the child. It should not be accepted because it has missing inputs.
# Its parent should not be requested because its hash (txid == wtxid) has been added to the rejection filter. # Its parent should not be requested because its hash (txid == wtxid) has been added to the rejection filter.
@@ -208,7 +209,7 @@ class OrphanHandlingTest(BitcoinTestFramework):
self.nodes[0].bumpmocktime(GETDATA_TX_INTERVAL) self.nodes[0].bumpmocktime(GETDATA_TX_INTERVAL)
peer1.assert_never_requested(int(parent_other["txid"], 16)) peer1.assert_never_requested(int(parent_other["txid"], 16))
peer2.assert_never_requested(int(parent_other["txid"], 16)) peer2.assert_never_requested(int(parent_other["txid"], 16))
peer2.assert_never_requested(int(parent_low_fee_nonsegwit["txid"], 16)) peer2.assert_never_requested(int(parent_overly_large_nonsegwit["txid"], 16))
self.log.info("Test orphan handling when a segwit parent was invalid but may be retried with another witness") self.log.info("Test orphan handling when a segwit parent was invalid but may be retried with another witness")
parent_low_fee = self.wallet.create_self_transfer(fee_rate=0) parent_low_fee = self.wallet.create_self_transfer(fee_rate=0)
@@ -391,23 +392,23 @@ class OrphanHandlingTest(BitcoinTestFramework):
peer3 = node.add_p2p_connection(PeerTxRelayer(wtxidrelay=False)) peer3 = node.add_p2p_connection(PeerTxRelayer(wtxidrelay=False))
self.log.info("Test that an orphan with rejected parents, along with any descendants, cannot be retried with an alternate witness") self.log.info("Test that an orphan with rejected parents, along with any descendants, cannot be retried with an alternate witness")
parent_low_fee_nonsegwit = self.wallet_nonsegwit.create_self_transfer(fee_rate=0) parent_overly_large_nonsegwit = self.wallet_nonsegwit.create_self_transfer(target_vsize=int(MAX_STANDARD_TX_WEIGHT / 4) + 1)
assert_equal(parent_low_fee_nonsegwit["txid"], parent_low_fee_nonsegwit["tx"].wtxid_hex) assert_equal(parent_overly_large_nonsegwit["txid"], parent_overly_large_nonsegwit["tx"].wtxid_hex)
child = self.wallet.create_self_transfer(utxo_to_spend=parent_low_fee_nonsegwit["new_utxo"]) child = self.wallet.create_self_transfer(utxo_to_spend=parent_overly_large_nonsegwit["new_utxo"])
grandchild = self.wallet.create_self_transfer(utxo_to_spend=child["new_utxo"]) grandchild = self.wallet.create_self_transfer(utxo_to_spend=child["new_utxo"])
assert_not_equal(child["txid"], child["tx"].wtxid_hex) assert_not_equal(child["txid"], child["tx"].wtxid_hex)
assert_not_equal(grandchild["txid"], grandchild["tx"].wtxid_hex) assert_not_equal(grandchild["txid"], grandchild["tx"].wtxid_hex)
# Relay the parent. It should be rejected because it pays 0 fees. # Relay the parent. It should be rejected because it pays 0 fees.
self.relay_transaction(peer1, parent_low_fee_nonsegwit["tx"]) self.relay_transaction(peer1, parent_overly_large_nonsegwit["tx"])
assert parent_low_fee_nonsegwit["txid"] not in node.getrawmempool() assert parent_overly_large_nonsegwit["txid"] not in node.getrawmempool()
# Relay the child. It should be rejected for having missing parents, and this rejection is # Relay the child. It should be rejected for having missing parents, and this rejection is
# cached by txid and wtxid. # cached by txid and wtxid.
self.relay_transaction(peer1, child["tx"]) self.relay_transaction(peer1, child["tx"])
assert_equal(0, len(node.getrawmempool())) assert_equal(0, len(node.getrawmempool()))
assert not tx_in_orphanage(node, child["tx"]) assert not tx_in_orphanage(node, child["tx"])
peer1.assert_never_requested(parent_low_fee_nonsegwit["txid"]) peer1.assert_never_requested(parent_overly_large_nonsegwit["txid"])
# Grandchild should also not be kept in orphanage because its parent has been rejected. # Grandchild should also not be kept in orphanage because its parent has been rejected.
self.relay_transaction(peer2, grandchild["tx"]) self.relay_transaction(peer2, grandchild["tx"])