Skip to content

Commit e518a8b

Browse files
committed
[functional test] opportunistic 1p1c package submission
1 parent 87c5c52 commit e518a8b

File tree

3 files changed

+581
-0
lines changed

3 files changed

+581
-0
lines changed

test/functional/p2p_1p1c_network.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2024-present The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""
6+
Test that 1p1c package submission allows a 1p1c package to propagate in a "network" of nodes. Send
7+
various packages from different nodes on a network in which some nodes have already received some of
8+
the transactions (and submitted them to mempool, kept them as orphans or rejected them as
9+
too-low-feerate transactions). The packages should be received and accepted by all nodes.
10+
"""
11+
12+
from decimal import Decimal
13+
from math import ceil
14+
15+
from test_framework.messages import (
16+
msg_tx,
17+
)
18+
from test_framework.p2p import (
19+
P2PInterface,
20+
)
21+
from test_framework.test_framework import BitcoinTestFramework
22+
from test_framework.util import (
23+
assert_equal,
24+
assert_greater_than,
25+
fill_mempool,
26+
)
27+
from test_framework.wallet import (
28+
MiniWallet,
29+
MiniWalletMode,
30+
)
31+
32+
# 1sat/vB feerate denominated in BTC/KvB
33+
FEERATE_1SAT_VB = Decimal("0.00001000")
34+
35+
class PackageRelayTest(BitcoinTestFramework):
36+
def set_test_params(self):
37+
self.setup_clean_chain = True
38+
self.num_nodes = 4
39+
# hugely speeds up the test, as it involves multiple hops of tx relay.
40+
self.noban_tx_relay = True
41+
self.extra_args = [[
42+
"-datacarriersize=100000",
43+
"-maxmempool=5",
44+
]] * self.num_nodes
45+
self.supports_cli = False
46+
47+
def raise_network_minfee(self):
48+
filler_wallet = MiniWallet(self.nodes[0])
49+
fill_mempool(self, self.nodes[0], filler_wallet)
50+
51+
self.log.debug("Wait for the network to sync mempools")
52+
self.sync_mempools()
53+
54+
self.log.debug("Check that all nodes' mempool minimum feerates are above min relay feerate")
55+
for node in self.nodes:
56+
assert_equal(node.getmempoolinfo()['minrelaytxfee'], FEERATE_1SAT_VB)
57+
assert_greater_than(node.getmempoolinfo()['mempoolminfee'], FEERATE_1SAT_VB)
58+
59+
def create_basic_1p1c(self, wallet):
60+
low_fee_parent = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, confirmed_only=True)
61+
high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
62+
package_hex_basic = [low_fee_parent["hex"], high_fee_child["hex"]]
63+
return package_hex_basic, low_fee_parent["tx"], high_fee_child["tx"]
64+
65+
def create_package_2outs(self, wallet):
66+
# First create a tester tx to see the vsize, and then adjust the fees
67+
utxo_for_2outs = wallet.get_utxo(confirmed_only=True)
68+
69+
low_fee_parent_2outs_tester = wallet.create_self_transfer_multi(
70+
utxos_to_spend=[utxo_for_2outs],
71+
num_outputs=2,
72+
)
73+
74+
# Target 1sat/vB so the number of satoshis is equal to the vsize.
75+
# Round up. The goal is to be between min relay feerate and mempool min feerate.
76+
fee_2outs = ceil(low_fee_parent_2outs_tester["tx"].get_vsize() / 2)
77+
78+
low_fee_parent_2outs = wallet.create_self_transfer_multi(
79+
utxos_to_spend=[utxo_for_2outs],
80+
num_outputs=2,
81+
fee_per_output=fee_2outs,
82+
)
83+
84+
# Now create the child
85+
high_fee_child_2outs = wallet.create_self_transfer_multi(
86+
utxos_to_spend=low_fee_parent_2outs["new_utxos"][::-1],
87+
fee_per_output=fee_2outs*100,
88+
)
89+
return [low_fee_parent_2outs["hex"], high_fee_child_2outs["hex"]], low_fee_parent_2outs["tx"], high_fee_child_2outs["tx"]
90+
91+
def create_package_2p1c(self, wallet):
92+
parent1 = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
93+
parent2 = wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*20, confirmed_only=True)
94+
child = wallet.create_self_transfer_multi(
95+
utxos_to_spend=[parent1["new_utxo"], parent2["new_utxo"]],
96+
fee_per_output=999*parent1["tx"].get_vsize(),
97+
)
98+
return [parent1["hex"], parent2["hex"], child["hex"]], parent1["tx"], parent2["tx"], child["tx"]
99+
100+
def create_packages(self):
101+
# 1: Basic 1-parent-1-child package, parent 1sat/vB, child 999sat/vB
102+
package_hex_1, parent_1, child_1 = self.create_basic_1p1c(self.wallet)
103+
104+
# 2: same as 1, parent's txid is the same as its wtxid.
105+
package_hex_2, parent_2, child_2 = self.create_basic_1p1c(self.wallet_nonsegwit)
106+
107+
# 3: 2-parent-1-child package. Both parents are above mempool min feerate. No package submission happens.
108+
# We require packages to be child-with-unconfirmed-parents and only allow 1-parent-1-child packages.
109+
package_hex_3, parent_31, parent_32, child_3 = self.create_package_2p1c(self.wallet)
110+
111+
# 4: parent + child package where the child spends 2 different outputs from the parent.
112+
package_hex_4, parent_4, child_4 = self.create_package_2outs(self.wallet)
113+
114+
# Assemble return results
115+
packages_to_submit = [package_hex_1, package_hex_2, package_hex_3, package_hex_4]
116+
# node0: sender
117+
# node1: pre-received the children (orphan)
118+
# node3: pre-received the parents (too low fee)
119+
# All nodes receive parent_31 ahead of time.
120+
txns_to_send = [
121+
[],
122+
[child_1, child_2, parent_31, child_3, child_4],
123+
[parent_31],
124+
[parent_1, parent_2, parent_31, parent_4]
125+
]
126+
127+
return packages_to_submit, txns_to_send
128+
129+
def run_test(self):
130+
self.wallet = MiniWallet(self.nodes[1])
131+
self.wallet_nonsegwit = MiniWallet(self.nodes[2], mode=MiniWalletMode.RAW_P2PK)
132+
self.generate(self.wallet_nonsegwit, 10)
133+
self.generate(self.wallet, 120)
134+
135+
self.log.info("Fill mempools with large transactions to raise mempool minimum feerates")
136+
self.raise_network_minfee()
137+
138+
# Create the transactions.
139+
self.wallet.rescan_utxos(include_mempool=True)
140+
packages_to_submit, transactions_to_presend = self.create_packages()
141+
142+
self.peers = [self.nodes[i].add_p2p_connection(P2PInterface()) for i in range(self.num_nodes)]
143+
144+
self.log.info("Pre-send some transactions to nodes")
145+
for (i, peer) in enumerate(self.peers):
146+
for tx in transactions_to_presend[i]:
147+
peer.send_and_ping(msg_tx(tx))
148+
# This disconnect removes any sent orphans from the orphanage (EraseForPeer) and times
149+
# out the in-flight requests. It is currently required for the test to pass right now,
150+
# because the node will not reconsider an orphan tx and will not (re)try requesting
151+
# orphan parents from multiple peers if the first one didn't respond.
152+
# TODO: remove this in the future if the node tries orphan resolution with multiple peers.
153+
peer.peer_disconnect()
154+
155+
self.log.info("Submit full packages to node0")
156+
for package_hex in packages_to_submit:
157+
submitpackage_result = self.nodes[0].submitpackage(package_hex)
158+
assert_equal(submitpackage_result["package_msg"], "success")
159+
160+
self.log.info("Wait for mempools to sync")
161+
self.sync_mempools(timeout=20)
162+
163+
164+
if __name__ == '__main__':
165+
PackageRelayTest().main()

0 commit comments

Comments
 (0)