1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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()
|