Merge bitcoin/bitcoin#26933: mempool: disallow txns under min relay fee, even in packages

bf77fc9cb4 [test] mempool full in package accept (glozow)
b51ebccc28 [validation] set PackageValidationState when mempool full (glozow)
563a2ee4f5 [policy] disallow transactions under min relay fee, even in packages (glozow)
c4554fe894 [test] package cpfp bumps parents <mempoolminfee but >=minrelaytxfee (glozow)
ac463e87df [test util] mock mempool minimum feerate (glozow)

Pull request description:

  Part of package relay, see #27463.

  Note that this still allows packages to bump transactions that are below the dynamic mempool minimum feerate, which means this still solves the "mempool is congested and my presigned 1sat/vB tx is screwed" problem for all transactions.

  On master, the package policy (only accessible through regtest-only RPC submitpackage) allows 0-fee (or otherwise below min relay feerate) transactions if they are bumped by a child. However, with default package limits, we don't yet have a DoS-resistant way of ensuring these transactions remain bumped throughout their time in the mempool. Primarily, the fee-bumping child may later be replaced by another transaction that doesn't bump the parent(s). The parent(s) could potentially stay bumped by other transactions, but not enough to ever be selected by the `BlockAssembler` (due to `blockmintxfee`).

  For example, (tested [here](https://github.com/glozow/bitcoin/commits/26933-motivation)):
  - The mempool accepts 24 below-minrelayfeerate transactions ("0-fee parents"), all bumped by a single high-fee transaction ("the fee-bumping child"). The fee-bumping child also spends a confirmed UTXO.
  - Two additional children are added to each 0-fee parent. These children each pay a feerate slightly above the minimum relay feerate (e.g. 1.9sat/vB) such that, for each 0-fee parent, the total fees of its two children divided by the total size of the children and parent is above the minimum relay feerate.
  - If a block template is built now, all transactions would be selected.
  - A transaction replaces the the fee-bumping child, spending only the confirmed UTXO and not any of the outputs from the 0-fee parents.
   - The 0-fee parents now each have 2 children. Their descendant feerates are above minrelayfeerate, which means that they remain in the mempool, even if the mempool evicts all below-minrelayfeerate packages.
   - If a block template is built now, none of the 0-fee parents or their children would be selected.
   - Even more low-feerate descendants can be added to these below-minrelayfeerate packages and they will not be evicted until they expire or the mempool reaches capacity.

  Unless we have a DoS-resistant way of ensuring package CPFP-bumped transactions are always bumped, allowing package CPFP to bump below-minrelayfeerate transactions can result in these problematic situations. See #27018 which proposes a partial solution with some limitations, and contains discussion about potential improvements to eviction strategy. While no adequate solution exists, for now, avoid these situations by requiring all transactions to meet min relay feerate.

ACKs for top commit:
  ajtowns:
    reACK bf77fc9cb4
  instagibbs:
    re-ACK bf77fc9cb4

Tree-SHA512: 28940f41493a9e280b010284316fb8caf1ed7b2090ba9a4ef8a3b2eafc5933601074b142f4f7d4e3c6c4cce99d3146f5c8e1393d9406c6f2070dd41c817985c9
This commit is contained in:
glozow
2023-04-26 11:17:46 +01:00
7 changed files with 192 additions and 85 deletions

View File

@@ -7,15 +7,21 @@
from decimal import Decimal
from test_framework.blocktools import COINBASE_MATURITY
from test_framework.p2p import P2PTxInvStore
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_fee_amount,
assert_greater_than,
assert_raises_rpc_error,
create_lots_of_big_transactions,
gen_return_txouts,
)
from test_framework.wallet import MiniWallet
from test_framework.wallet import (
COIN,
DEFAULT_FEE,
MiniWallet,
)
class MempoolLimitTest(BitcoinTestFramework):
@@ -38,13 +44,14 @@ class MempoolLimitTest(BitcoinTestFramework):
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
tx_batch_size = 25
num_of_batches = 3
tx_batch_size = 1
num_of_batches = 75
# Generate UTXOs to flood the mempool
# 1 to create a tx initially that will be evicted from the mempool later
# 3 batches of multiple transactions with a fee rate much higher than the previous UTXO
# And 1 more to verify that this tx does not get added to the mempool with a fee rate less than the mempoolminfee
self.generate(miniwallet, 1 + (num_of_batches * tx_batch_size) + 1)
# And 2 more for the package cpfp test
self.generate(miniwallet, 1 + (num_of_batches * tx_batch_size) + 1 + 2)
# Mine 99 blocks so that the UTXOs are allowed to be spent
self.generate(node, COINBASE_MATURITY - 1)
@@ -76,6 +83,68 @@ class MempoolLimitTest(BitcoinTestFramework):
self.log.info('Create a mempool tx that will not pass mempoolminfee')
assert_raises_rpc_error(-26, "mempool min fee not met", miniwallet.send_self_transfer, from_node=node, fee_rate=relayfee)
self.log.info("Check that submitpackage allows cpfp of a parent below mempool min feerate")
node = self.nodes[0]
peer = node.add_p2p_connection(P2PTxInvStore())
# Package with 2 parents and 1 child. One parent has a high feerate due to modified fees,
# another is below the mempool minimum feerate but bumped by the child.
tx_poor = miniwallet.create_self_transfer(fee_rate=relayfee)
tx_rich = miniwallet.create_self_transfer(fee=0, fee_rate=0)
node.prioritisetransaction(tx_rich["txid"], 0, int(DEFAULT_FEE * COIN))
package_txns = [tx_rich, tx_poor]
coins = [tx["new_utxo"] for tx in package_txns]
tx_child = miniwallet.create_self_transfer_multi(utxos_to_spend=coins, fee_per_output=10000) #DEFAULT_FEE
package_txns.append(tx_child)
submitpackage_result = node.submitpackage([tx["hex"] for tx in package_txns])
rich_parent_result = submitpackage_result["tx-results"][tx_rich["wtxid"]]
poor_parent_result = submitpackage_result["tx-results"][tx_poor["wtxid"]]
child_result = submitpackage_result["tx-results"][tx_child["tx"].getwtxid()]
assert_fee_amount(poor_parent_result["fees"]["base"], tx_poor["tx"].get_vsize(), relayfee)
assert_equal(rich_parent_result["fees"]["base"], 0)
assert_equal(child_result["fees"]["base"], DEFAULT_FEE)
# The "rich" parent does not require CPFP so its effective feerate is just its individual feerate.
assert_fee_amount(DEFAULT_FEE, tx_rich["tx"].get_vsize(), rich_parent_result["fees"]["effective-feerate"])
assert_equal(rich_parent_result["fees"]["effective-includes"], [tx_rich["wtxid"]])
# The "poor" parent and child's effective feerates are the same, composed of their total
# fees divided by their combined vsize.
package_fees = poor_parent_result["fees"]["base"] + child_result["fees"]["base"]
package_vsize = tx_poor["tx"].get_vsize() + tx_child["tx"].get_vsize()
assert_fee_amount(package_fees, package_vsize, poor_parent_result["fees"]["effective-feerate"])
assert_fee_amount(package_fees, package_vsize, child_result["fees"]["effective-feerate"])
assert_equal([tx_poor["wtxid"], tx_child["tx"].getwtxid()], poor_parent_result["fees"]["effective-includes"])
assert_equal([tx_poor["wtxid"], tx_child["tx"].getwtxid()], child_result["fees"]["effective-includes"])
# The node will broadcast each transaction, still abiding by its peer's fee filter
peer.wait_for_broadcast([tx["tx"].getwtxid() for tx in package_txns])
self.log.info("Check a package that passes mempoolminfee but is evicted immediately after submission")
mempoolmin_feerate = node.getmempoolinfo()["mempoolminfee"]
current_mempool = node.getrawmempool(verbose=False)
worst_feerate_btcvb = Decimal("21000000")
for txid in current_mempool:
entry = node.getmempoolentry(txid)
worst_feerate_btcvb = min(worst_feerate_btcvb, entry["fees"]["descendant"] / entry["descendantsize"])
# Needs to be large enough to trigger eviction
target_weight_each = 200000
assert_greater_than(target_weight_each * 2, node.getmempoolinfo()["maxmempool"] - node.getmempoolinfo()["bytes"])
# Should be a true CPFP: parent's feerate is just below mempool min feerate
parent_fee = (mempoolmin_feerate / 1000) * (target_weight_each // 4) - Decimal("0.00001")
# Parent + child is above mempool minimum feerate
child_fee = (worst_feerate_btcvb) * (target_weight_each // 4) - Decimal("0.00001")
# 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()
tx_parent_just_below = miniwallet.create_self_transfer(fee=parent_fee, target_weight=target_weight_each)
tx_child_just_above = miniwallet.create_self_transfer(utxo_to_spend=tx_parent_just_below["new_utxo"], fee=child_fee, target_weight=target_weight_each)
# This package ranks below the lowest descendant package in the mempool
assert_greater_than(worst_feerate_btcvb, (parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()))
assert_greater_than(mempoolmin_feerate, (parent_fee) / (tx_parent_just_below["tx"].get_vsize()))
assert_greater_than((parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()), mempoolmin_feerate / 1000)
assert_raises_rpc_error(-26, "mempool full", node.submitpackage, [tx_parent_just_below["hex"], tx_child_just_above["hex"]])
self.log.info('Test passing a value below the minimum (5 MB) to -maxmempool throws an error')
self.stop_node(0)
self.nodes[0].assert_start_raises_init_error(["-maxmempool=4"], "Error: -maxmempool must be at least 5 MB")

View File

@@ -20,7 +20,6 @@ from test_framework.util import (
assert_raises_rpc_error,
)
from test_framework.wallet import (
COIN,
DEFAULT_FEE,
MiniWallet,
)
@@ -325,42 +324,6 @@ class RPCPackagesTest(BitcoinTestFramework):
peer.wait_for_broadcast([tx["tx"].getwtxid() for tx in package_txns])
self.generate(node, 1)
def test_submit_cpfp(self):
node = self.nodes[0]
peer = node.add_p2p_connection(P2PTxInvStore())
# Package with 2 parents and 1 child. One parent pays for itself using modified fees, and
# another has 0 fees but is bumped by child.
tx_poor = self.wallet.create_self_transfer(fee=0, fee_rate=0)
tx_rich = self.wallet.create_self_transfer(fee=0, fee_rate=0)
node.prioritisetransaction(tx_rich["txid"], 0, int(DEFAULT_FEE * COIN))
package_txns = [tx_rich, tx_poor]
coins = [tx["new_utxo"] for tx in package_txns]
tx_child = self.wallet.create_self_transfer_multi(utxos_to_spend=coins, fee_per_output=10000) #DEFAULT_FEE
package_txns.append(tx_child)
submitpackage_result = node.submitpackage([tx["hex"] for tx in package_txns])
rich_parent_result = submitpackage_result["tx-results"][tx_rich["wtxid"]]
poor_parent_result = submitpackage_result["tx-results"][tx_poor["wtxid"]]
child_result = submitpackage_result["tx-results"][tx_child["tx"].getwtxid()]
assert_equal(rich_parent_result["fees"]["base"], 0)
assert_equal(poor_parent_result["fees"]["base"], 0)
assert_equal(child_result["fees"]["base"], DEFAULT_FEE)
# The "rich" parent does not require CPFP so its effective feerate.
assert_fee_amount(DEFAULT_FEE, tx_rich["tx"].get_vsize(), rich_parent_result["fees"]["effective-feerate"])
assert_equal(rich_parent_result["fees"]["effective-includes"], [tx_rich["wtxid"]])
# The "poor" parent and child's effective feerates are the same, composed of the child's fee
# divided by their combined vsize.
assert_fee_amount(DEFAULT_FEE, tx_poor["tx"].get_vsize() + tx_child["tx"].get_vsize(), poor_parent_result["fees"]["effective-feerate"])
assert_fee_amount(DEFAULT_FEE, tx_poor["tx"].get_vsize() + tx_child["tx"].get_vsize(), child_result["fees"]["effective-feerate"])
assert_equal([tx_poor["wtxid"], tx_child["tx"].getwtxid()], poor_parent_result["fees"]["effective-includes"])
assert_equal([tx_poor["wtxid"], tx_child["tx"].getwtxid()], child_result["fees"]["effective-includes"])
# The node will broadcast each transaction, still abiding by its peer's fee filter
peer.wait_for_broadcast([tx["tx"].getwtxid() for tx in package_txns])
self.generate(node, 1)
def test_submitpackage(self):
node = self.nodes[0]
@@ -369,9 +332,6 @@ class RPCPackagesTest(BitcoinTestFramework):
self.test_submit_child_with_parents(num_parents, False)
self.test_submit_child_with_parents(num_parents, True)
self.log.info("Submitpackage valid packages with CPFP")
self.test_submit_cpfp()
self.log.info("Submitpackage only allows packages of 1 child with its parents")
# Chain of 3 transactions has too many generations
chain_hex = [t["hex"] for t in self.wallet.create_self_transfer_chain(chain_length=25)]