0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-15 11:36:00 -05:00
bitcoin-bitcoin-core/test/functional/p2p_opportunistic_1p1c.py
glozow 86d7135e36 [p2p] only attempt 1p1c when both txns provided by the same peer
Now that we track all announcers of an orphan, it's not helpful to
consider an orphan provided by a peer that didn't send us this parent.
It can only hurt our chances of finding the right orphan when there are
multiple candidates.

Adapt the 2 tests in p2p_opportunistic_1p1c.py that looked at 1p1c
packages from different peers. Instead of checking that the right peer
is punished, we now check that the package is not submitted. We can't
use the functional test to see that the package was not considered
because the behavior is indistinguishable (except for the logs).
2025-01-06 09:02:05 -05:00

414 lines
20 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 time
from test_framework.mempool_util import (
fill_mempool,
)
from test_framework.messages import (
CInv,
CTxInWitness,
MAX_BIP125_RBF_SEQUENCE,
MSG_WTX,
msg_inv,
msg_tx,
tx_from_hex,
)
from test_framework.p2p import (
P2PInterface,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_greater_than,
)
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 = [[
"-datacarriersize=100000",
"-maxmempool=5",
]]
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
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)
return wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, sequence=self.sequence, 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")
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 = int(high_fee_child["tx"].getwtxid(), 16)
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"]))
# 2. Node requests the missing parent by txid.
parent_txid_int = int(low_fee_parent["txid"], 16)
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]
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 = int(low_fee_parent["tx"].getwtxid(), 16)
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 = int(high_fee_child["tx"].getwtxid(), 16)
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)
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]
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 = int(low_fee_parent["tx"].getwtxid(), 16)
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 = int(med_fee_child["tx"].getwtxid(), 16)
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)
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 = int(high_fee_child["tx"].getwtxid(), 16)
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)
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]
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 = int(tx_orphan_bad_wit.getwtxid(), 16)
bad_orphan_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
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)
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.rehash() 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_message(msg_tx(low_fee_parent["tx"]))
bad_orphan_sender.wait_for_disconnect()
# 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]
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 = int(high_fee_child["tx"].getwtxid(), 16)
package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
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 = int(tx_parent_bad_wit.rehash(), 16)
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.rehash() 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 = int(low_fee_parent["tx"].getwtxid(), 16)
package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
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 fails if child already has another parent in mempool")
node = self.nodes[0]
# This parent needs CPFP
parent_low = self.create_tx_below_mempoolminfee(self.wallet)
# 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 first parent which will be accepted.
peer_sender.send_and_ping(msg_tx(parent_high["tx"]))
assert parent_high["txid"] in node.getrawmempool()
# 2. Send child.
peer_sender.send_and_ping(msg_tx(child["tx"]))
# 3. Node requests parent_low. However, 1p1c fails because package-not-child-with-unconfirmed-parents
parent_low_txid_int = int(parent_low["txid"], 16)
peer_sender.wait_for_getdata([parent_low_txid_int])
peer_sender.send_and_ping(msg_tx(parent_low["tx"]))
node_mempool = node.getrawmempool()
assert parent_high["txid"] in node_mempool
assert parent_low["txid"] not in node_mempool
assert child["txid"] not in node_mempool
# Same error if submitted through submitpackage without parent_high
package_hex_missing_parent = [parent_low["hex"], child["hex"]]
result_missing_parent = node.submitpackage(package_hex_missing_parent)
assert_equal(result_missing_parent["package_msg"], "package-not-child-with-unconfirmed-parents")
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()
if __name__ == '__main__':
PackageRelayTest(__file__).main()