Files
bitcoin/test/functional/p2p_opportunistic_1p1c.py
merge-script c99f5c5e1b Merge bitcoin/bitcoin#33106: policy: lower the default blockmintxfee, incrementalrelayfee, minrelaytxfee
ba84a25dee [doc] update mempool-replacements.md for incremental relay feerate change (glozow)
18720bc5d5 [doc] release note for min feerate changes (glozow)
6da5de58ca [policy] lower default minrelaytxfee and incrementalrelayfee to 100sat/kvB (glozow)
2e515d2897 [prep/test] make wallet_fundrawtransaction's minrelaytxfee assumption explicit (glozow)
457cfb61b5 [prep/util] help MockMempoolMinFee handle more precise feerates (glozow)
3eab8b7240 [prep/test] replace magic number 1000 with respective feerate vars (glozow)
5f2df0ef78 [miner] lower default -blockmintxfee to 1sat/kvB (glozow)
d6213d6aa1 [doc] assert that default min relay feerate and incremental are the same (glozow)
1fbee5d7b6 [test] explicitly check default -minrelaytxfee and -incrementalrelayfee (glozow)
72dc18467d [test] RBF rule 4 for various incrementalrelayfee settings (glozow)
85f498893f [test] check bypass of minrelay for various minrelaytxfee settings (glozow)
e5f896bb1f [test] check miner doesn't select 0fee transactions (glozow)

Pull request description:

  ML post for discussion about the general concept, how this impacts the wider ecosystem, philosophy about minimum feerates, etc: https://delvingbitcoin.org/t/changing-the-minimum-relay-feerate/1886

  This PR is inspired by #13922 and #32959 to lower the minimum relay feerate in response to bitcoin's exchange rate changes in the last ~10 years. It lowers the default `-minrelaytxfee` and `-incrementalrelayfee`, and knocks `-blockmintxfee` down to the minimum nonzero setting. Also adds some tests for the settings and pulls in #32750.

  The minimum relay feerate is a DoS protection rule, representing a price on the network bandwidth used to relay transactions that have no PoW. While relay nodes don't all collect fees, the assumption is that if nodes on the network use their resources to relay this transaction, it will reach a miner and the attacker's money will be spent once it is mined. The incremental relay feerate is similar: it's used to price the relay of replacement transactions (the additional fees need to cover the new transactions at this feerate) and evicted transactions (following a trim, the new mempool minimum feerate is the package feerate of what was removed + incremental).

  Also note that many nodes on the network have elected to relay/mine lower feerate transactions. Miners (some say up to 85%) are choosing to mine these low feerate transactions instead of leaving block space unfilled, but these blocks have extremely poor compact block reconstruction rates with nodes that rejected or didn't hear about those transactions earlier.
  - https://github.com/bitcoin/bitcoin/pull/33106#issuecomment-3155627414
  - https://x.com/caesrcd/status/1947022514267230302
  - https://mempool.space/block/00000000000000000001305770e0aa279dcd8ba8be18c3d5cf736a26f77e06fd
  - https://mempool.space/block/00000000000000000001b491649ec030aa8e003e1f4f9d3b24bb99ba16f91e97
  - https://x.com/mononautical/status/1949452586391855121

  While it wouldn't make sense to loosen DoS restrictions recklessly in response to these events, I think the current price is higher than necessary, and this motivates us changing the default soon. Since the minimum relay feerate defines an amount as too small based on what it costs the attacker, it makes sense to consider BTC's conversion rate to what resources you can buy in the "real world."

  Going off of [this comment](https://github.com/bitcoin/bitcoin/pull/32959#issuecomment-3095260286) and [this comment](https://github.com/bitcoin/bitcoin/pull/33106#issuecomment-3142444090)
  - 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 ec2 bandwidth is 10c/GB, that's like 1-4c per kvB of transaction data
  - Then a 1000vB transaction should pay at least 4c
  - $0.04 USD is 40 satoshis at 100k USD/BTC
  - Baking in some margin for changes in USD/BTC conversion rate, number of nodes (and thus bandwidth), and commercial service costs, I think 50-100 satoshis is on the conservative end but in the right ballpark
  - At least 97% of the recent sub-1sat/vB transactions would be accepted with a new threshold of 0.1sat/vB: https://github.com/bitcoin/bitcoin/pull/33106#issuecomment-3156213089

  List of feerates that are changed and why:
  - min relay feerate: significant conversion rate changes, see above
  - incremental relay feerate: should follow min relay feerate, see above
  - block minimum feerate: shouldn’t be above min relay feerate, otherwise the node accepts transactions it will never mine. I've knocked it down to the bare minimum of 1sat/kvB. Now that we no longer have coin age priority (removed in v0.15), I think we can leave it to the `CheckFeeRate` policy rule to enforce a minimum entry price, and the block assembly code should just fill up the block with whatever it finds in mempool.

  List of feerates that are not changed and why:
  - dust feerate: this feerate cannot be changed as flexibly as the minrelay feerate. A much longer record of low feerate transactions being mined is needed to motivate a decrease there.
  - maxfeerate (RPC, wallet): I think the conversion rate is relevant as well, but out of scope for this PR
  - minimum feerate returned by fee estimator: should be done later. In the past, we've excluded new policy defaults from fee estimation until we feel confident they represent miner policy (e.g. #9519). Also, the fee estimator itself doesn't have support for sub-1sat/vB yet.
  - all wallet feerates (mintxfee, fallbackfee, discardfee, consolidatefeerate, WALLET_INCREMENTAL_RELAY_FEE, etc.): should be done later. Our standard procedure is to do wallet changes at least 1 release after policy changes.

ACKs for top commit:
  achow101:
    ACK ba84a25dee
  gmaxwell:
    ACK ba84a25dee
  jsarenik:
    Tested ACK ba84a25dee
  darosior:
    ACK ba84a25dee
  ajtowns:
    ACK ba84a25dee
  davidgumberg:
    crACK  ba84a25dee
  w0xlt:
    ACK ba84a25dee
  caesrcd:
    reACK ba84a25dee
  ismaelsadeeq:
    re-ACK ba84a25dee

Tree-SHA512: b4c35e8b506b1184db466551a7e2e48bb1e535972a8dbcaa145ce3a8bfdcc70a8807dc129460f129a9d31024174d34077154a387c32f1a3e6831f6fa5e9c399e
2025-08-15 10:39:16 +01:00

649 lines
31 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2024-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
Test opportunistic 1p1c package submission logic.
"""
from decimal import Decimal
import random
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,
CTxOut,
CTxInWitness,
MAX_BIP125_RBF_SEQUENCE,
MSG_WTX,
msg_inv,
msg_tx,
tx_from_hex,
)
from test_framework.p2p import (
NONPREF_PEER_TX_DELAY,
P2PInterface,
TXID_RELAY_DELAY,
)
from test_framework.script import (
CScript,
OP_NOP,
OP_RETURN,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_greater_than,
assert_greater_than_or_equal,
)
from test_framework.wallet import (
MiniWallet,
MiniWalletMode,
)
# 1sat/vB feerate denominated in BTC/KvB
FEERATE_1SAT_VB = Decimal("0.00001000")
# Number of seconds to wait to ensure no getdata is received
GETDATA_WAIT = 60
def cleanup(func):
def wrapper(self, *args, **kwargs):
try:
func(self, *args, **kwargs)
finally:
self.nodes[0].disconnect_p2ps()
# Do not clear the node's mempool, as each test requires mempool min feerate > min
# relay feerate. However, do check that this is the case.
assert self.nodes[0].getmempoolinfo()["mempoolminfee"] > self.nodes[0].getnetworkinfo()["relayfee"]
# Ensure we do not try to spend the same UTXOs in subsequent tests, as they will look like RBF attempts.
self.wallet.rescan_utxos(include_mempool=True)
# Resets if mocktime was used
self.nodes[0].setmocktime(0)
return wrapper
class PackageRelayTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 1
self.extra_args = [[
"-maxmempool=5",
]]
def create_tx_below_mempoolminfee(self, wallet, utxo_to_spend=None):
"""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"], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN)
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):
node = self.nodes[0]
self.log.info("Check that opportunistic 1p1c logic works when child is received before parent")
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
peer_sender = node.add_p2p_connection(P2PInterface())
# 1. Child is received first (perhaps the low feerate parent didn't meet feefilter or the requests were sent to different nodes). It is missing an input.
high_child_wtxid_int = high_fee_child["tx"].wtxid_int
peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
peer_sender.wait_for_getdata([high_child_wtxid_int])
peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
# 2. Node requests the missing parent by txid.
parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_txid_int])
# 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted.
peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
# 4. Both transactions should now be in mempool.
node_mempool = node.getrawmempool()
assert low_fee_parent["txid"] in node_mempool
assert high_fee_child["txid"] in node_mempool
node.disconnect_p2ps()
@cleanup
def test_basic_parent_then_child(self, wallet):
node = self.nodes[0]
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(wallet)
high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
peer_ignored = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, connection_type="outbound-full-relay")
# 1. Parent is relayed first. It is too low feerate.
parent_wtxid_int = low_fee_parent["tx"].wtxid_int
peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
peer_sender.wait_for_getdata([parent_wtxid_int])
peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
assert low_fee_parent["txid"] not in node.getrawmempool()
# Send again from peer_ignored, check that it is ignored
peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
assert "getdata" not in peer_ignored.last_message
# 2. Child is relayed next. It is missing an input.
high_child_wtxid_int = high_fee_child["tx"].wtxid_int
peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
peer_sender.wait_for_getdata([high_child_wtxid_int])
peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
# 3. Node requests the missing parent by txid.
# It should do so even if it has previously rejected that parent for being too low feerate.
parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_txid_int])
# 4. Sender re-relays the parent. Parent+Child are evaluated as a package and accepted.
peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
# 5. Both transactions should now be in mempool.
node_mempool = node.getrawmempool()
assert low_fee_parent["txid"] in node_mempool
assert high_fee_child["txid"] in node_mempool
@cleanup
def test_low_and_high_child(self, wallet):
node = self.nodes[0]
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(wallet)
# This feerate is above mempoolminfee, but not enough to also bump the low feerate parent.
feerate_just_above = node.getmempoolinfo()["mempoolminfee"]
med_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=feerate_just_above)
high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
peer_ignored = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, connection_type="outbound-full-relay")
self.log.info("Check that tx caches low fee parent + low fee child package rejections")
# 1. Send parent, rejected for being low feerate.
parent_wtxid_int = low_fee_parent["tx"].wtxid_int
peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
peer_sender.wait_for_getdata([parent_wtxid_int])
peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
assert low_fee_parent["txid"] not in node.getrawmempool()
# Send again from peer_ignored, check that it is ignored
peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
assert "getdata" not in peer_ignored.last_message
# 2. Send an (orphan) child that has a higher feerate, but not enough to bump the parent.
med_child_wtxid_int = med_fee_child["tx"].wtxid_int
peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=med_child_wtxid_int)]))
peer_sender.wait_for_getdata([med_child_wtxid_int])
peer_sender.send_and_ping(msg_tx(med_fee_child["tx"]))
# 3. Node requests the orphan's missing parent.
parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_txid_int])
# 4. The low parent + low child are submitted as a package. They are not accepted due to low package feerate.
peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
assert low_fee_parent["txid"] not in node.getrawmempool()
assert med_fee_child["txid"] not in node.getrawmempool()
# If peer_ignored announces the low feerate child, it should be ignored
peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=med_child_wtxid_int)]))
assert "getdata" not in peer_ignored.last_message
# If either peer sends the parent again, package evaluation should not be attempted
peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
peer_ignored.send_and_ping(msg_tx(low_fee_parent["tx"]))
assert low_fee_parent["txid"] not in node.getrawmempool()
assert med_fee_child["txid"] not in node.getrawmempool()
# 5. Send the high feerate (orphan) child
high_child_wtxid_int = high_fee_child["tx"].wtxid_int
peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
peer_sender.wait_for_getdata([high_child_wtxid_int])
peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
# 6. Node requests the orphan's parent, even though it has already been rejected, both by
# itself and with a child. This is necessary, otherwise high_fee_child can be censored.
parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_txid_int])
# 7. The low feerate parent + high feerate child are submitted as a package.
peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
# 8. Both transactions should now be in mempool
node_mempool = node.getrawmempool()
assert low_fee_parent["txid"] in node_mempool
assert high_fee_child["txid"] in node_mempool
assert med_fee_child["txid"] not in node_mempool
@cleanup
def test_orphan_consensus_failure(self):
self.log.info("Check opportunistic 1p1c logic requires parent and child to be from the same peer")
node = self.nodes[0]
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
coin = low_fee_parent["new_utxo"]
address = node.get_deterministic_priv_key().address
# Create raw transaction spending the parent, but with no signature (a consensus error).
hex_orphan_no_sig = node.createrawtransaction([{"txid": coin["txid"], "vout": coin["vout"]}], {address : coin["value"] - Decimal("0.0001")})
tx_orphan_bad_wit = tx_from_hex(hex_orphan_no_sig)
tx_orphan_bad_wit.wit.vtxinwit.append(CTxInWitness())
tx_orphan_bad_wit.wit.vtxinwit[0].scriptWitness.stack = [b'garbage']
bad_orphan_sender = node.add_p2p_connection(P2PInterface())
parent_sender = node.add_p2p_connection(P2PInterface())
# 1. Child is received first. It is missing an input.
child_wtxid_int = tx_orphan_bad_wit.wtxid_int
bad_orphan_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
bad_orphan_sender.wait_for_getdata([child_wtxid_int])
bad_orphan_sender.send_and_ping(msg_tx(tx_orphan_bad_wit))
# 2. Node requests the missing parent by txid.
parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
bad_orphan_sender.wait_for_getdata([parent_txid_int])
# 3. A different peer relays the parent. Package is not evaluated because the transactions
# were not sent from the same peer.
parent_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
# 4. Transactions should not be in mempool.
node_mempool = node.getrawmempool()
assert low_fee_parent["txid"] not in node_mempool
assert tx_orphan_bad_wit.txid_hex not in node_mempool
# 5. Have the other peer send the tx too, so that tx_orphan_bad_wit package is attempted.
bad_orphan_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
# The bad orphan sender should not be disconnected.
bad_orphan_sender.sync_with_ping()
# The peer that didn't provide the orphan should not be disconnected.
parent_sender.sync_with_ping()
@cleanup
def test_parent_consensus_failure(self):
self.log.info("Check opportunistic 1p1c logic with consensus-invalid parent causes disconnect of the correct peer")
node = self.nodes[0]
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
# Create invalid version of parent with a bad signature.
tx_parent_bad_wit = tx_from_hex(low_fee_parent["hex"])
tx_parent_bad_wit.wit.vtxinwit.append(CTxInWitness())
tx_parent_bad_wit.wit.vtxinwit[0].scriptWitness.stack = [b'garbage']
package_sender = node.add_p2p_connection(P2PInterface())
fake_parent_sender = node.add_p2p_connection(P2PInterface())
# 1. Child is received first. It is missing an input.
child_wtxid_int = high_fee_child["tx"].wtxid_int
package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
package_sender.wait_for_getdata([child_wtxid_int])
package_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
# 2. Node requests the missing parent by txid.
parent_txid_int = tx_parent_bad_wit.txid_int
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
package_sender.wait_for_getdata([parent_txid_int])
# 3. A different node relays the parent. The parent is first evaluated by itself and
# rejected for being too low feerate. It is not evaluated as a package because the child was
# sent from a different peer, so we don't find out that the child is consensus-invalid.
fake_parent_sender.send_and_ping(msg_tx(tx_parent_bad_wit))
# 4. Transactions should not be in mempool.
node_mempool = node.getrawmempool()
assert tx_parent_bad_wit.txid_hex not in node_mempool
assert high_fee_child["txid"] not in node_mempool
self.log.info("Check that fake parent does not cause orphan to be deleted and real package can still be submitted")
# 5. Child-sending should not have been punished and the orphan should remain in orphanage.
# It can send the "real" parent transaction, and the package is accepted.
parent_wtxid_int = low_fee_parent["tx"].wtxid_int
package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
package_sender.wait_for_getdata([parent_wtxid_int])
package_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
node_mempool = node.getrawmempool()
assert low_fee_parent["txid"] in node_mempool
assert high_fee_child["txid"] in node_mempool
@cleanup
def test_multiple_parents(self):
self.log.info("Check that node does not request more than 1 previously-rejected low feerate parent")
node = self.nodes[0]
node.setmocktime(int(time.time()))
# 2-parent-1-child package where both parents are below mempool min feerate
parent_low_1 = self.create_tx_below_mempoolminfee(self.wallet_nonsegwit)
parent_low_2 = self.create_tx_below_mempoolminfee(self.wallet_nonsegwit)
child_bumping = self.wallet_nonsegwit.create_self_transfer_multi(
utxos_to_spend=[parent_low_1["new_utxo"], parent_low_2["new_utxo"]],
fee_per_output=999*parent_low_1["tx"].get_vsize(),
)
peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
# 1. Send both parents. Each should be rejected for being too low feerate.
# Send unsolicited so that we can later check that no "getdata" was ever received.
peer_sender.send_and_ping(msg_tx(parent_low_1["tx"]))
peer_sender.send_and_ping(msg_tx(parent_low_2["tx"]))
# parent_low_1 and parent_low_2 are rejected for being low feerate.
assert parent_low_1["txid"] not in node.getrawmempool()
assert parent_low_2["txid"] not in node.getrawmempool()
# 2. Send child.
peer_sender.send_and_ping(msg_tx(child_bumping["tx"]))
# 3. Node should not request any parents, as it should recognize that it will not accept
# multi-parent-1-child packages.
node.bumpmocktime(GETDATA_WAIT)
peer_sender.sync_with_ping()
assert "getdata" not in peer_sender.last_message
@cleanup
def test_other_parent_in_mempool(self):
self.log.info("Check opportunistic 1p1c works when part of a 2p1c (child already has another parent in mempool)")
node = self.nodes[0]
node.setmocktime(int(time.time()))
# Grandparent will enter mempool by itself
grandparent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
# This parent needs CPFP
parent_low = self.create_tx_below_mempoolminfee(self.wallet, utxo_to_spend=grandparent_high["new_utxo"])
# This parent does not need CPFP and can be submitted alone ahead of time
parent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
child = self.wallet.create_self_transfer_multi(
utxos_to_spend=[parent_high["new_utxo"], parent_low["new_utxo"]],
fee_per_output=999*parent_low["tx"].get_vsize(),
)
peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
# 1. Send grandparent which is accepted
peer_sender.send_and_ping(msg_tx(grandparent_high["tx"]))
assert grandparent_high["txid"] in node.getrawmempool()
# 2. Send first parent which is accepted.
peer_sender.send_and_ping(msg_tx(parent_high["tx"]))
assert parent_high["txid"] in node.getrawmempool()
# 3. Send child which is handled as an orphan.
peer_sender.send_and_ping(msg_tx(child["tx"]))
# 4. Node requests parent_low.
parent_low_txid_int = int(parent_low["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_low_txid_int])
peer_sender.send_and_ping(msg_tx(parent_low["tx"]))
node_mempool = node.getrawmempool()
assert grandparent_high["txid"] in node_mempool
assert parent_high["txid"] in node_mempool
assert parent_low["txid"] in node_mempool
assert child["txid"] in node_mempool
def create_small_orphan(self):
"""Create small orphan transaction"""
tx = CTransaction()
# Nonexistent UTXO
tx.vin = [CTxIn(COutPoint(random.randrange(1 << 256), random.randrange(1, 100)))]
tx.wit.vtxinwit = [CTxInWitness()]
tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_NOP] * 5)]
tx.vout = [CTxOut(100, CScript([OP_RETURN, b'a' * 3]))]
return tx
@cleanup
def test_orphanage_dos_large(self):
self.log.info("Test that the node can still resolve orphans when peers use lots of orphanage space")
node = self.nodes[0]
node.setmocktime(int(time.time()))
peer_normal = node.add_p2p_connection(P2PInterface())
peer_doser = node.add_p2p_connection(P2PInterface())
num_individual_dosers = 10
self.log.info("Create very large orphans to be sent by DoSy peers (may take a while)")
large_orphans = [create_large_orphan() for _ in range(50)]
# Check to make sure these are orphans, within max standard size (to be accepted into the orphanage)
for large_orphan in large_orphans:
assert_greater_than_or_equal(100000, large_orphan.get_vsize())
assert_greater_than(MAX_STANDARD_TX_WEIGHT, large_orphan.get_weight())
assert_greater_than_or_equal(3 * large_orphan.get_vsize(), 2 * 100000)
testres = node.testmempoolaccept([large_orphan.serialize().hex()])
assert not testres[0]["allowed"]
assert_equal(testres[0]["reject-reason"], "missing-inputs")
self.log.info(f"Connect {num_individual_dosers} peers and send a very large orphan from each one")
# This test assumes that unrequested transactions are processed (skipping inv and
# getdata steps because they require going through request delays)
# Connect 10 peers and have each of them send a large orphan.
for large_orphan in large_orphans[:num_individual_dosers]:
peer_doser_individual = node.add_p2p_connection(P2PInterface())
peer_doser_individual.send_and_ping(msg_tx(large_orphan))
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_doser_individual.wait_for_getdata([large_orphan.vin[0].prevout.hash])
# Make sure that these transactions are going through the orphan handling codepaths.
# Subsequent rounds will not wait for getdata because the time mocking will cause the
# normal package request to time out.
self.wait_until(lambda: len(node.getorphantxs()) == num_individual_dosers)
self.log.info("Send an orphan from a non-DoSy peer. Its orphan should not be evicted.")
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
high_fee_child = self.wallet.create_self_transfer(
utxo_to_spend=low_fee_parent["new_utxo"],
fee_rate=200*FEERATE_1SAT_VB,
target_vsize=100000
)
# Announce
orphan_tx = high_fee_child["tx"]
orphan_inv = CInv(t=MSG_WTX, h=orphan_tx.wtxid_int)
# Wait for getdata
peer_normal.send_and_ping(msg_inv([orphan_inv]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
peer_normal.wait_for_getdata([orphan_tx.wtxid_int])
peer_normal.send_and_ping(msg_tx(orphan_tx))
# Wait for parent request
parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_normal.wait_for_getdata([parent_txid_int])
self.log.info("Send another round of very large orphans from a DoSy peer")
for large_orphan in large_orphans[num_individual_dosers:]:
peer_doser.send_and_ping(msg_tx(large_orphan))
# Something was evicted; the orphanage does not contain all large orphans + the 1p1c child
self.wait_until(lambda: len(node.getorphantxs()) < len(large_orphans) + 1)
self.log.info("Provide the orphan's parent. This 1p1c package should be successfully accepted.")
peer_normal.send_and_ping(msg_tx(low_fee_parent["tx"]))
assert_equal(node.getmempoolentry(orphan_tx.txid_hex)["ancestorcount"], 2)
@cleanup
def test_orphanage_dos_many(self):
self.log.info("Test that the node can still resolve orphans when peers are sending tons of orphans")
node = self.nodes[0]
node.setmocktime(int(time.time()))
peer_normal = node.add_p2p_connection(P2PInterface())
# The first set of peers all send the same batch_size orphans. Then a single peer sends
# batch_single_doser distinct orphans.
batch_size = 51
num_peers_shared = 60
batch_single_doser = 100
assert_greater_than(num_peers_shared * batch_size + batch_single_doser, 3000)
# 60 peers * 51 orphans = 3060 announcements
shared_orphans = [self.create_small_orphan() for _ in range(batch_size)]
self.log.info(f"Send the same {batch_size} orphans from {num_peers_shared} DoSy peers (may take a while)")
peer_doser_shared = [node.add_p2p_connection(P2PInterface()) for _ in range(num_peers_shared)]
for i in range(num_peers_shared):
for orphan in shared_orphans:
peer_doser_shared[i].send_without_ping(msg_tx(orphan))
# We sync peers to make sure we have processed as many orphans as possible. Ensure at least
# one of the orphans was processed.
for peer_doser in peer_doser_shared:
peer_doser.sync_with_ping()
self.wait_until(lambda: any([tx.txid_hex in node.getorphantxs() for tx in shared_orphans]))
self.log.info("Send an orphan from a non-DoSy peer. Its orphan should not be evicted.")
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
high_fee_child = self.wallet.create_self_transfer(
utxo_to_spend=low_fee_parent["new_utxo"],
fee_rate=200*FEERATE_1SAT_VB,
)
# Announce
orphan_tx = high_fee_child["tx"]
orphan_inv = CInv(t=MSG_WTX, h=orphan_tx.wtxid_int)
# Wait for getdata
peer_normal.send_and_ping(msg_inv([orphan_inv]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
peer_normal.wait_for_getdata([orphan_tx.wtxid_int])
peer_normal.send_and_ping(msg_tx(orphan_tx))
# Orphan has been entered and evicted something else
self.wait_until(lambda: high_fee_child["txid"] in node.getorphantxs())
# Wait for parent request
parent_txid_int = low_fee_parent["tx"].txid_int
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_normal.wait_for_getdata([parent_txid_int])
self.log.info(f"Send {batch_single_doser} new orphans from one DoSy peer")
peer_doser_batch = node.add_p2p_connection(P2PInterface())
this_batch_orphans = [self.create_small_orphan() for _ in range(batch_single_doser)]
for tx in this_batch_orphans:
# Don't wait for responses, because it dramatically increases the runtime of this test.
peer_doser_batch.send_without_ping(msg_tx(tx))
peer_doser_batch.sync_with_ping()
self.wait_until(lambda: any([tx.txid_hex in node.getorphantxs() for tx in this_batch_orphans]))
self.log.info("Check that orphan from normal peer still exists in orphanage")
assert high_fee_child["txid"] in node.getorphantxs()
self.log.info("Provide the orphan's parent. This 1p1c package should be successfully accepted.")
peer_normal.send_and_ping(msg_tx(low_fee_parent["tx"]))
assert orphan_tx.txid_hex in node.getrawmempool()
assert_equal(node.getmempoolentry(orphan_tx.txid_hex)["ancestorcount"], 2)
@cleanup
def test_1p1c_on_1p1c(self):
self.log.info("Test that opportunistic 1p1c works when part of a 4-generation chain (1p1c chained from a 1p1c)")
node = self.nodes[0]
# Prep 2 generations of 1p1c packages to be relayed
low_fee_great_grandparent = self.create_tx_below_mempoolminfee(self.wallet)
high_fee_grandparent = self.wallet.create_self_transfer(utxo_to_spend=low_fee_great_grandparent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet, utxo_to_spend=high_fee_grandparent["new_utxo"])
high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
peer_sender = node.add_p2p_connection(P2PInterface())
# The 1p1c that spends the confirmed utxo must be received first. Afterwards, the "younger" 1p1c can be received.
for package in [[low_fee_great_grandparent, high_fee_grandparent], [low_fee_parent, high_fee_child]]:
# Aliases
parent_relative, child_relative = package
# 1. Child is received first (perhaps the low feerate parent didn't meet feefilter or the requests were sent to different nodes). It is missing an input.
high_child_wtxid_int = child_relative["tx"].wtxid_int
peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
peer_sender.wait_for_getdata([high_child_wtxid_int])
peer_sender.send_and_ping(msg_tx(child_relative["tx"]))
# 2. Node requests the missing parent by txid.
parent_txid_int = parent_relative["tx"].txid_int
peer_sender.wait_for_getdata([parent_txid_int])
# 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted.
peer_sender.send_and_ping(msg_tx(parent_relative["tx"]))
# 4. All transactions should now be in mempool.
node_mempool = node.getrawmempool()
assert low_fee_great_grandparent["txid"] in node_mempool
assert high_fee_grandparent["txid"] in node_mempool
assert low_fee_parent["txid"] in node_mempool
assert high_fee_child["txid"] in node_mempool
assert_equal(node.getmempoolentry(low_fee_great_grandparent["txid"])["descendantcount"], 4)
def run_test(self):
node = self.nodes[0]
# To avoid creating transactions with the same txid (can happen if we set the same feerate
# and reuse the same input as a previous transaction that wasn't successfully submitted),
# we give each subtest a different nSequence for its transactions.
self.sequence = MAX_BIP125_RBF_SEQUENCE
self.wallet = MiniWallet(node)
self.wallet_nonsegwit = MiniWallet(node, mode=MiniWalletMode.RAW_P2PK)
self.generate(self.wallet_nonsegwit, 10)
self.generate(self.wallet, 20)
fill_mempool(self, node)
self.log.info("Check opportunistic 1p1c logic when parent (txid != wtxid) is received before child")
self.test_basic_parent_then_child(self.wallet)
self.log.info("Check opportunistic 1p1c logic when parent (txid == wtxid) is received before child")
self.test_basic_parent_then_child(self.wallet_nonsegwit)
self.log.info("Check opportunistic 1p1c logic when child is received before parent")
self.test_basic_child_then_parent()
self.log.info("Check opportunistic 1p1c logic when 2 candidate children exist (parent txid != wtxid)")
self.test_low_and_high_child(self.wallet)
self.log.info("Check opportunistic 1p1c logic when 2 candidate children exist (parent txid == wtxid)")
self.test_low_and_high_child(self.wallet_nonsegwit)
self.test_orphan_consensus_failure()
self.test_parent_consensus_failure()
self.test_multiple_parents()
self.test_other_parent_in_mempool()
self.test_1p1c_on_1p1c()
self.test_orphanage_dos_large()
self.test_orphanage_dos_many()
if __name__ == '__main__':
PackageRelayTest(__file__).main()