[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.

Github-Pull: #33106
Rebased-From: 6da5de58ca
This commit is contained in:
glozow
2025-07-29 14:37:16 -04:00
parent bbdab3ef7b
commit 9dd7efc8c3
12 changed files with 54 additions and 52 deletions

View File

@@ -589,7 +589,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())

View File

@@ -215,7 +215,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(), [])

View File

@@ -185,8 +185,8 @@ 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'], Decimal('0.00000100'))
assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00000100'))
fill_mempool(self, node)
current_info = node.getmempoolinfo()
@@ -215,7 +215,7 @@ class MempoolLimitTest(BitcoinTestFramework):
# coin is no longer available, but the cache could still contains the tx.
cpfp_parent = self.wallet.create_self_transfer(
utxo_to_spend=mempool_evicted_tx["new_utxo"],
fee_rate=mempoolmin_feerate - Decimal('0.00001'),
fee_rate=mempoolmin_feerate / 2,
confirmed_only=True)
package_hex.append(cpfp_parent["hex"])
parent_utxos.append(cpfp_parent["new_utxo"])
@@ -230,7 +230,7 @@ class MempoolLimitTest(BitcoinTestFramework):
# Need to be large enough to trigger eviction
# (note that the mempool usage of a tx is about three times its vsize)
assert_greater_than(parent_vsize * num_big_parents * 3, current_info["maxmempool"] - current_info["bytes"])
parent_feerate = 100 * mempoolmin_feerate
parent_feerate = 10 * mempoolmin_feerate
big_parent_txids = []
for i in range(num_big_parents):
@@ -249,7 +249,7 @@ class MempoolLimitTest(BitcoinTestFramework):
# Specific number of satoshis to fit within a small window. The parent_cpfp + child package needs to be
# - When there is mid-package eviction, high enough feerate to meet the new mempoolminfee
# - When there is no mid-package eviction, low enough feerate to be evicted immediately after submission.
magic_satoshis = 1200
magic_satoshis = 120
cpfp_satoshis = int(cpfp_fee * COIN) + magic_satoshis
child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=cpfp_satoshis)
@@ -302,7 +302,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()
@@ -408,9 +408,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()

View File

@@ -163,13 +163,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].rehash()}, 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].rehash()}, 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)
@@ -563,12 +563,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,
)

View File

@@ -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
@@ -51,12 +50,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"]
@@ -87,8 +86,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(),

View File

@@ -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):

View File

@@ -9,10 +9,12 @@ Test opportunistic 1p1c package submission logic.
from decimal import Decimal
import time
from test_framework.mempool_util import (
DEFAULT_MIN_RELAY_TX_FEE,
fill_mempool,
)
from test_framework.messages import (
CInv,
COIN,
CTxInWitness,
MAX_BIP125_RBF_SEQUENCE,
MSG_WTX,
@@ -65,13 +67,13 @@ class PackageRelayTest(BitcoinTestFramework):
self.supports_cli = False
def create_tx_below_mempoolminfee(self, wallet):
"""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, confirmed_only=True)
return wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN, sequence=self.sequence, confirmed_only=True)
@cleanup
def test_basic_child_then_parent(self):

View File

@@ -20,9 +20,9 @@ from .wallet import (
ORPHAN_TX_EXPIRE_TIME = 1200
# 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,

View File

@@ -534,7 +534,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)
@@ -846,7 +846,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})