mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-11-15 08:31:49 +01:00
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: ACKba84a25deegmaxwell: ACKba84a25deejsarenik: Tested ACKba84a25deedarosior: ACKba84a25deeajtowns: ACKba84a25deedavidgumberg: crACKba84a25deew0xlt: ACKba84a25deecaesrcd: reACKba84a25deeismaelsadeeq: re-ACKba84a25deeTree-SHA512: b4c35e8b506b1184db466551a7e2e48bb1e535972a8dbcaa145ce3a8bfdcc70a8807dc129460f129a9d31024174d34077154a387c32f1a3e6831f6fa5e9c399e
649 lines
31 KiB
Python
Executable File
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()
|