mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-05 14:06:27 -05:00
[functional test] opportunistic 1p1c package submission
This commit is contained in:
parent
87c5c524d6
commit
e518a8bf8a
3 changed files with 581 additions and 0 deletions
165
test/functional/p2p_1p1c_network.py
Executable file
165
test/functional/p2p_1p1c_network.py
Executable file
|
@ -0,0 +1,165 @@
|
|||
#!/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 that 1p1c package submission allows a 1p1c package to propagate in a "network" of nodes. Send
|
||||
various packages from different nodes on a network in which some nodes have already received some of
|
||||
the transactions (and submitted them to mempool, kept them as orphans or rejected them as
|
||||
too-low-feerate transactions). The packages should be received and accepted by all nodes.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from math import ceil
|
||||
|
||||
from test_framework.messages import (
|
||||
msg_tx,
|
||||
)
|
||||
from test_framework.p2p import (
|
||||
P2PInterface,
|
||||
)
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
fill_mempool,
|
||||
)
|
||||
from test_framework.wallet import (
|
||||
MiniWallet,
|
||||
MiniWalletMode,
|
||||
)
|
||||
|
||||
# 1sat/vB feerate denominated in BTC/KvB
|
||||
FEERATE_1SAT_VB = Decimal("0.00001000")
|
||||
|
||||
class PackageRelayTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.setup_clean_chain = True
|
||||
self.num_nodes = 4
|
||||
# hugely speeds up the test, as it involves multiple hops of tx relay.
|
||||
self.noban_tx_relay = True
|
||||
self.extra_args = [[
|
||||
"-datacarriersize=100000",
|
||||
"-maxmempool=5",
|
||||
]] * self.num_nodes
|
||||
self.supports_cli = False
|
||||
|
||||
def raise_network_minfee(self):
|
||||
filler_wallet = MiniWallet(self.nodes[0])
|
||||
fill_mempool(self, self.nodes[0], filler_wallet)
|
||||
|
||||
self.log.debug("Wait for the network to sync mempools")
|
||||
self.sync_mempools()
|
||||
|
||||
self.log.debug("Check that all nodes' mempool minimum feerates are above min relay feerate")
|
||||
for node in self.nodes:
|
||||
assert_equal(node.getmempoolinfo()['minrelaytxfee'], FEERATE_1SAT_VB)
|
||||
assert_greater_than(node.getmempoolinfo()['mempoolminfee'], FEERATE_1SAT_VB)
|
||||
|
||||
def create_basic_1p1c(self, wallet):
|
||||
low_fee_parent = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, confirmed_only=True)
|
||||
high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
|
||||
package_hex_basic = [low_fee_parent["hex"], high_fee_child["hex"]]
|
||||
return package_hex_basic, low_fee_parent["tx"], high_fee_child["tx"]
|
||||
|
||||
def create_package_2outs(self, wallet):
|
||||
# First create a tester tx to see the vsize, and then adjust the fees
|
||||
utxo_for_2outs = wallet.get_utxo(confirmed_only=True)
|
||||
|
||||
low_fee_parent_2outs_tester = wallet.create_self_transfer_multi(
|
||||
utxos_to_spend=[utxo_for_2outs],
|
||||
num_outputs=2,
|
||||
)
|
||||
|
||||
# Target 1sat/vB so the number of satoshis is equal to the vsize.
|
||||
# Round up. The goal is to be between min relay feerate and mempool min feerate.
|
||||
fee_2outs = ceil(low_fee_parent_2outs_tester["tx"].get_vsize() / 2)
|
||||
|
||||
low_fee_parent_2outs = wallet.create_self_transfer_multi(
|
||||
utxos_to_spend=[utxo_for_2outs],
|
||||
num_outputs=2,
|
||||
fee_per_output=fee_2outs,
|
||||
)
|
||||
|
||||
# Now create the child
|
||||
high_fee_child_2outs = wallet.create_self_transfer_multi(
|
||||
utxos_to_spend=low_fee_parent_2outs["new_utxos"][::-1],
|
||||
fee_per_output=fee_2outs*100,
|
||||
)
|
||||
return [low_fee_parent_2outs["hex"], high_fee_child_2outs["hex"]], low_fee_parent_2outs["tx"], high_fee_child_2outs["tx"]
|
||||
|
||||
def create_package_2p1c(self, wallet):
|
||||
parent1 = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
|
||||
parent2 = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*20, confirmed_only=True)
|
||||
child = wallet.create_self_transfer_multi(
|
||||
utxos_to_spend=[parent1["new_utxo"], parent2["new_utxo"]],
|
||||
fee_per_output=999*parent1["tx"].get_vsize(),
|
||||
)
|
||||
return [parent1["hex"], parent2["hex"], child["hex"]], parent1["tx"], parent2["tx"], child["tx"]
|
||||
|
||||
def create_packages(self):
|
||||
# 1: Basic 1-parent-1-child package, parent 1sat/vB, child 999sat/vB
|
||||
package_hex_1, parent_1, child_1 = self.create_basic_1p1c(self.wallet)
|
||||
|
||||
# 2: same as 1, parent's txid is the same as its wtxid.
|
||||
package_hex_2, parent_2, child_2 = self.create_basic_1p1c(self.wallet_nonsegwit)
|
||||
|
||||
# 3: 2-parent-1-child package. Both parents are above mempool min feerate. No package submission happens.
|
||||
# We require packages to be child-with-unconfirmed-parents and only allow 1-parent-1-child packages.
|
||||
package_hex_3, parent_31, parent_32, child_3 = self.create_package_2p1c(self.wallet)
|
||||
|
||||
# 4: parent + child package where the child spends 2 different outputs from the parent.
|
||||
package_hex_4, parent_4, child_4 = self.create_package_2outs(self.wallet)
|
||||
|
||||
# Assemble return results
|
||||
packages_to_submit = [package_hex_1, package_hex_2, package_hex_3, package_hex_4]
|
||||
# node0: sender
|
||||
# node1: pre-received the children (orphan)
|
||||
# node3: pre-received the parents (too low fee)
|
||||
# All nodes receive parent_31 ahead of time.
|
||||
txns_to_send = [
|
||||
[],
|
||||
[child_1, child_2, parent_31, child_3, child_4],
|
||||
[parent_31],
|
||||
[parent_1, parent_2, parent_31, parent_4]
|
||||
]
|
||||
|
||||
return packages_to_submit, txns_to_send
|
||||
|
||||
def run_test(self):
|
||||
self.wallet = MiniWallet(self.nodes[1])
|
||||
self.wallet_nonsegwit = MiniWallet(self.nodes[2], mode=MiniWalletMode.RAW_P2PK)
|
||||
self.generate(self.wallet_nonsegwit, 10)
|
||||
self.generate(self.wallet, 120)
|
||||
|
||||
self.log.info("Fill mempools with large transactions to raise mempool minimum feerates")
|
||||
self.raise_network_minfee()
|
||||
|
||||
# Create the transactions.
|
||||
self.wallet.rescan_utxos(include_mempool=True)
|
||||
packages_to_submit, transactions_to_presend = self.create_packages()
|
||||
|
||||
self.peers = [self.nodes[i].add_p2p_connection(P2PInterface()) for i in range(self.num_nodes)]
|
||||
|
||||
self.log.info("Pre-send some transactions to nodes")
|
||||
for (i, peer) in enumerate(self.peers):
|
||||
for tx in transactions_to_presend[i]:
|
||||
peer.send_and_ping(msg_tx(tx))
|
||||
# This disconnect removes any sent orphans from the orphanage (EraseForPeer) and times
|
||||
# out the in-flight requests. It is currently required for the test to pass right now,
|
||||
# because the node will not reconsider an orphan tx and will not (re)try requesting
|
||||
# orphan parents from multiple peers if the first one didn't respond.
|
||||
# TODO: remove this in the future if the node tries orphan resolution with multiple peers.
|
||||
peer.peer_disconnect()
|
||||
|
||||
self.log.info("Submit full packages to node0")
|
||||
for package_hex in packages_to_submit:
|
||||
submitpackage_result = self.nodes[0].submitpackage(package_hex)
|
||||
assert_equal(submitpackage_result["package_msg"], "success")
|
||||
|
||||
self.log.info("Wait for mempools to sync")
|
||||
self.sync_mempools(timeout=20)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
PackageRelayTest().main()
|
414
test/functional/p2p_opportunistic_1p1c.py
Executable file
414
test/functional/p2p_opportunistic_1p1c.py
Executable file
|
@ -0,0 +1,414 @@
|
|||
#!/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.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,
|
||||
fill_mempool,
|
||||
)
|
||||
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 with consensus-invalid orphan causes disconnect of the correct 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. Parent+Child are evaluated as a package and rejected.
|
||||
parent_sender.send_message(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. Peer that sent a consensus-invalid transaction should be disconnected.
|
||||
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. Then it is evaluated as a package and, after passing
|
||||
# feerate checks, rejected for having a bad signature (consensus error).
|
||||
fake_parent_sender.send_message(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
|
||||
|
||||
# 5. Peer sent a consensus-invalid transaction.
|
||||
fake_parent_sender.wait_for_disconnect()
|
||||
|
||||
self.log.info("Check that fake parent does not cause orphan to be deleted and real package can still be submitted")
|
||||
# 6. 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)
|
||||
|
||||
filler_wallet = MiniWallet(node)
|
||||
fill_mempool(self, node, filler_wallet)
|
||||
|
||||
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().main()
|
|
@ -199,6 +199,8 @@ BASE_SCRIPTS = [
|
|||
'wallet_txn_clone.py --segwit',
|
||||
'rpc_getchaintips.py',
|
||||
'rpc_misc.py',
|
||||
'p2p_1p1c_network.py',
|
||||
'p2p_opportunistic_1p1c.py',
|
||||
'interface_rest.py',
|
||||
'mempool_spend_coinbase.py',
|
||||
'wallet_avoid_mixing_output_types.py --descriptors',
|
||||
|
|
Loading…
Add table
Reference in a new issue