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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
|
#!/usr/bin/env python3
# Copyright (c) 2023 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
import time
from test_framework.messages import (
CInv,
CTxInWitness,
MSG_TX,
MSG_WITNESS_TX,
MSG_WTX,
msg_getdata,
msg_inv,
msg_notfound,
msg_tx,
tx_from_hex,
)
from test_framework.p2p import (
GETDATA_TX_INTERVAL,
NONPREF_PEER_TX_DELAY,
OVERLOADED_PEER_TX_DELAY,
p2p_lock,
P2PInterface,
P2PTxInvStore,
TXID_RELAY_DELAY,
)
from test_framework.util import (
assert_equal,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.wallet import (
MiniWallet,
MiniWalletMode,
)
# Time to bump forward (using setmocktime) before waiting for the node to send getdata(tx) in response
# to an inv(tx), in seconds. This delay includes all possible delays + 1, so it should only be used
# when the value of the delay is not interesting. If we want to test that the node waits x seconds
# for one peer and y seconds for another, use specific values instead.
TXREQUEST_TIME_SKIP = NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY + OVERLOADED_PEER_TX_DELAY + 1
def cleanup(func):
# Time to fastfoward (using setmocktime) in between subtests to ensure they do not interfere with
# one another, in seconds. Equal to 12 hours, which is enough to expire anything that may exist
# (though nothing should since state should be cleared) in p2p data structures.
LONG_TIME_SKIP = 12 * 60 * 60
def wrapper(self):
try:
func(self)
finally:
# Clear mempool
self.generate(self.nodes[0], 1)
self.nodes[0].disconnect_p2ps()
self.nodes[0].bumpmocktime(LONG_TIME_SKIP)
return wrapper
class PeerTxRelayer(P2PTxInvStore):
"""A P2PTxInvStore that also remembers all of the getdata and tx messages it receives."""
def __init__(self):
super().__init__()
self._tx_received = []
self._getdata_received = []
@property
def tx_received(self):
with p2p_lock:
return self._tx_received
@property
def getdata_received(self):
with p2p_lock:
return self._getdata_received
def on_tx(self, message):
self._tx_received.append(message)
def on_getdata(self, message):
self._getdata_received.append(message)
def wait_for_parent_requests(self, txids):
"""Wait for requests for missing parents by txid with witness data (MSG_WITNESS_TX or
WitnessTx). Requires that the getdata message match these txids exactly; all txids must be
requested and no additional requests are allowed."""
def test_function():
last_getdata = self.last_message.get('getdata')
if not last_getdata:
return False
return len(last_getdata.inv) == len(txids) and all([item.type == MSG_WITNESS_TX and item.hash in txids for item in last_getdata.inv])
self.wait_until(test_function, timeout=10)
def assert_no_immediate_response(self, message):
"""Check that the node does not immediately respond to this message with any of getdata,
inv, tx. The node may respond later.
"""
prev_lastmessage = self.last_message
self.send_and_ping(message)
after_lastmessage = self.last_message
for msgtype in ["getdata", "inv", "tx"]:
if msgtype not in prev_lastmessage:
assert msgtype not in after_lastmessage
else:
assert_equal(prev_lastmessage[msgtype], after_lastmessage[msgtype])
def assert_never_requested(self, txhash):
"""Check that the node has never sent us a getdata for this hash (int type)"""
for getdata in self.getdata_received:
for request in getdata.inv:
assert request.hash != txhash
class OrphanHandlingTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.extra_args = [[]]
def create_parent_and_child(self):
"""Create package with 1 parent and 1 child, normal fees (no cpfp)."""
parent = self.wallet.create_self_transfer()
child = self.wallet.create_self_transfer(utxo_to_spend=parent['new_utxo'])
return child["tx"].getwtxid(), child["tx"], parent["tx"]
def relay_transaction(self, peer, tx):
"""Relay transaction using MSG_WTX"""
wtxid = int(tx.getwtxid(), 16)
peer.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=wtxid)]))
self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP)
peer.wait_for_getdata([wtxid])
peer.send_and_ping(msg_tx(tx))
def create_malleated_version(self, tx):
"""
Create a malleated version of the tx where the witness is replaced with garbage data.
Returns a CTransaction object.
"""
tx_bad_wit = tx_from_hex(tx["hex"])
tx_bad_wit.wit.vtxinwit = [CTxInWitness()]
# Add garbage data to witness 0. We cannot simply strip the witness, as the node would
# classify it as a transaction in which the witness was missing rather than wrong.
tx_bad_wit.wit.vtxinwit[0].scriptWitness.stack = [b'garbage']
assert_equal(tx["txid"], tx_bad_wit.rehash())
assert tx["wtxid"] != tx_bad_wit.getwtxid()
return tx_bad_wit
@cleanup
def test_arrival_timing_orphan(self):
self.log.info("Test missing parents that arrive during delay are not requested")
node = self.nodes[0]
tx_parent_arrives = self.wallet.create_self_transfer()
tx_parent_doesnt_arrive = self.wallet.create_self_transfer()
# Fake orphan spends nonexistent outputs of the two parents
tx_fake_orphan = self.wallet.create_self_transfer_multi(utxos_to_spend=[
{"txid": tx_parent_doesnt_arrive["txid"], "vout": 10, "value": tx_parent_doesnt_arrive["new_utxo"]["value"]},
{"txid": tx_parent_arrives["txid"], "vout": 10, "value": tx_parent_arrives["new_utxo"]["value"]}
])
peer_spy = node.add_p2p_connection(PeerTxRelayer())
peer_normal = node.add_p2p_connection(PeerTxRelayer())
# This transaction is an orphan because it is missing inputs. It is a "fake" orphan that the
# spy peer has crafted to learn information about tx_parent_arrives even though it isn't
# able to spend a real output of it, but it could also just be a normal, real child tx.
# The node should not immediately respond with a request for orphan parents.
# Also, no request should be sent later because it will be resolved by
# the time the request is scheduled to be sent.
peer_spy.assert_no_immediate_response(msg_tx(tx_fake_orphan["tx"]))
# Node receives transaction. It attempts to obfuscate the exact timing at which this
# transaction entered its mempool. Send unsolicited because otherwise we need to wait for
# request delays.
peer_normal.send_and_ping(msg_tx(tx_parent_arrives["tx"]))
assert tx_parent_arrives["txid"] in node.getrawmempool()
# Spy peer should not be able to query the node for the parent yet, since it hasn't been
# announced / insufficient time has elapsed.
parent_inv = CInv(t=MSG_WTX, h=int(tx_parent_arrives["tx"].getwtxid(), 16))
assert_equal(len(peer_spy.get_invs()), 0)
peer_spy.assert_no_immediate_response(msg_getdata([parent_inv]))
# Request would be scheduled with this delay because it is not a preferred relay peer.
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY)
peer_spy.assert_never_requested(int(tx_parent_arrives["txid"], 16))
peer_spy.assert_never_requested(int(tx_parent_doesnt_arrive["txid"], 16))
# Request would be scheduled with this delay because it is by txid.
self.nodes[0].bumpmocktime(TXID_RELAY_DELAY)
peer_spy.wait_for_parent_requests([int(tx_parent_doesnt_arrive["txid"], 16)])
peer_spy.assert_never_requested(int(tx_parent_arrives["txid"], 16))
@cleanup
def test_orphan_rejected_parents_exceptions(self):
node = self.nodes[0]
peer1 = node.add_p2p_connection(PeerTxRelayer())
peer2 = node.add_p2p_connection(PeerTxRelayer())
self.log.info("Test orphan handling when a nonsegwit parent is known to be invalid")
parent_low_fee_nonsegwit = self.wallet_nonsegwit.create_self_transfer(fee_rate=0)
assert_equal(parent_low_fee_nonsegwit["txid"], parent_low_fee_nonsegwit["tx"].getwtxid())
parent_other = self.wallet_nonsegwit.create_self_transfer()
child_nonsegwit = self.wallet_nonsegwit.create_self_transfer_multi(
utxos_to_spend=[parent_other["new_utxo"], parent_low_fee_nonsegwit["new_utxo"]])
# Relay the parent. It should be rejected because it pays 0 fees.
self.relay_transaction(peer1, parent_low_fee_nonsegwit["tx"])
assert parent_low_fee_nonsegwit["txid"] not in node.getrawmempool()
# Relay the child. It should not be accepted because it has missing inputs.
# Its parent should not be requested because its hash (txid == wtxid) has been added to the rejection filter.
with node.assert_debug_log(['not keeping orphan with rejected parents {}'.format(child_nonsegwit["txid"])]):
self.relay_transaction(peer2, child_nonsegwit["tx"])
assert child_nonsegwit["txid"] not in node.getrawmempool()
# No parents are requested.
self.nodes[0].bumpmocktime(GETDATA_TX_INTERVAL)
peer1.assert_never_requested(int(parent_other["txid"], 16))
peer2.assert_never_requested(int(parent_other["txid"], 16))
peer2.assert_never_requested(int(parent_low_fee_nonsegwit["txid"], 16))
self.log.info("Test orphan handling when a segwit parent was invalid but may be retried with another witness")
parent_low_fee = self.wallet.create_self_transfer(fee_rate=0)
child_low_fee = self.wallet.create_self_transfer(utxo_to_spend=parent_low_fee["new_utxo"])
# Relay the low fee parent. It should not be accepted.
self.relay_transaction(peer1, parent_low_fee["tx"])
assert parent_low_fee["txid"] not in node.getrawmempool()
# Relay the child. It should not be accepted because it has missing inputs.
self.relay_transaction(peer2, child_low_fee["tx"])
assert child_low_fee["txid"] not in node.getrawmempool()
# The parent should be requested because even though the txid commits to the fee, it doesn't
# commit to the feerate. Delayed because it's by txid and this is not a preferred relay peer.
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer2.wait_for_getdata([int(parent_low_fee["tx"].rehash(), 16)])
self.log.info("Test orphan handling when a parent was previously downloaded with witness stripped")
parent_normal = self.wallet.create_self_transfer()
parent1_witness_stripped = tx_from_hex(parent_normal["tx"].serialize_without_witness().hex())
child_invalid_witness = self.wallet.create_self_transfer(utxo_to_spend=parent_normal["new_utxo"])
# Relay the parent with witness stripped. It should not be accepted.
self.relay_transaction(peer1, parent1_witness_stripped)
assert_equal(parent_normal["txid"], parent1_witness_stripped.rehash())
assert parent1_witness_stripped.rehash() not in node.getrawmempool()
# Relay the child. It should not be accepted because it has missing inputs.
self.relay_transaction(peer2, child_invalid_witness["tx"])
assert child_invalid_witness["txid"] not in node.getrawmempool()
# The parent should be requested since the unstripped wtxid would differ. Delayed because
# it's by txid and this is not a preferred relay peer.
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer2.wait_for_getdata([int(parent_normal["tx"].rehash(), 16)])
# parent_normal can be relayed again even though parent1_witness_stripped was rejected
self.relay_transaction(peer1, parent_normal["tx"])
assert_equal(set(node.getrawmempool()), set([parent_normal["txid"], child_invalid_witness["txid"]]))
@cleanup
def test_orphan_multiple_parents(self):
node = self.nodes[0]
peer = node.add_p2p_connection(PeerTxRelayer())
self.log.info("Test orphan parent requests with a mixture of confirmed, in-mempool and missing parents")
# This UTXO confirmed a long time ago.
utxo_conf_old = self.wallet.send_self_transfer(from_node=node)["new_utxo"]
txid_conf_old = utxo_conf_old["txid"]
self.generate(self.wallet, 10)
# Create a fake reorg to trigger BlockDisconnected, which resets the rolling bloom filter.
# The alternative is to mine thousands of transactions to push it out of the filter.
last_block = node.getbestblockhash()
node.invalidateblock(last_block)
node.preciousblock(last_block)
node.syncwithvalidationinterfacequeue()
# This UTXO confirmed recently.
utxo_conf_recent = self.wallet.send_self_transfer(from_node=node)["new_utxo"]
self.generate(node, 1)
# This UTXO is unconfirmed and in the mempool.
assert_equal(len(node.getrawmempool()), 0)
mempool_tx = self.wallet.send_self_transfer(from_node=node)
utxo_unconf_mempool = mempool_tx["new_utxo"]
# This UTXO is unconfirmed and missing.
missing_tx = self.wallet.create_self_transfer()
utxo_unconf_missing = missing_tx["new_utxo"]
assert missing_tx["txid"] not in node.getrawmempool()
orphan = self.wallet.create_self_transfer_multi(utxos_to_spend=[utxo_conf_old,
utxo_conf_recent, utxo_unconf_mempool, utxo_unconf_missing])
self.relay_transaction(peer, orphan["tx"])
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer.sync_with_ping()
assert_equal(len(peer.last_message["getdata"].inv), 2)
peer.wait_for_parent_requests([int(txid_conf_old, 16), int(missing_tx["txid"], 16)])
# Even though the peer would send a notfound for the "old" confirmed transaction, the node
# doesn't give up on the orphan. Once all of the missing parents are received, it should be
# submitted to mempool.
peer.send_message(msg_notfound(vec=[CInv(MSG_WITNESS_TX, int(txid_conf_old, 16))]))
# Sync with ping to ensure orphans are reconsidered
peer.send_and_ping(msg_tx(missing_tx["tx"]))
assert_equal(node.getmempoolentry(orphan["txid"])["ancestorcount"], 3)
@cleanup
def test_orphans_overlapping_parents(self):
node = self.nodes[0]
# In the process of relaying inflight_parent_AB
peer_txrequest = node.add_p2p_connection(PeerTxRelayer())
# Sends the orphans
peer_orphans = node.add_p2p_connection(PeerTxRelayer())
confirmed_utxos = [self.wallet_nonsegwit.get_utxo() for _ in range(4)]
assert all([utxo["confirmations"] > 0 for utxo in confirmed_utxos])
self.log.info("Test handling of multiple orphans with missing parents that are already being requested")
# Parent of child_A only
missing_parent_A = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=confirmed_utxos[0])
# Parents of child_A and child_B
missing_parent_AB = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=confirmed_utxos[1])
inflight_parent_AB = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=confirmed_utxos[2])
# Parent of child_B only
missing_parent_B = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=confirmed_utxos[3])
child_A = self.wallet_nonsegwit.create_self_transfer_multi(
utxos_to_spend=[missing_parent_A["new_utxo"], missing_parent_AB["new_utxo"], inflight_parent_AB["new_utxo"]]
)
child_B = self.wallet_nonsegwit.create_self_transfer_multi(
utxos_to_spend=[missing_parent_B["new_utxo"], missing_parent_AB["new_utxo"], inflight_parent_AB["new_utxo"]]
)
# The wtxid and txid need to be the same for the node to recognize that the missing input
# and in-flight request for inflight_parent_AB are the same transaction.
assert_equal(inflight_parent_AB["txid"], inflight_parent_AB["tx"].getwtxid())
# Announce inflight_parent_AB and wait for getdata
peer_txrequest.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=int(inflight_parent_AB["tx"].getwtxid(), 16))]))
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY)
peer_txrequest.wait_for_getdata([int(inflight_parent_AB["tx"].getwtxid(), 16)])
self.log.info("Test that the node does not request a parent if it has an in-flight txrequest")
# Relay orphan child_A
self.relay_transaction(peer_orphans, child_A["tx"])
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
# There are 3 missing parents. missing_parent_A and missing_parent_AB should be requested.
# But inflight_parent_AB should not, because there is already an in-flight request for it.
peer_orphans.wait_for_parent_requests([int(missing_parent_A["txid"], 16), int(missing_parent_AB["txid"], 16)])
self.log.info("Test that the node does not request a parent if it has an in-flight orphan parent request")
# Relay orphan child_B
self.relay_transaction(peer_orphans, child_B["tx"])
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
# Only missing_parent_B should be requested. Not inflight_parent_AB or missing_parent_AB
# because they are already being requested from peer_txrequest and peer_orphans respectively.
peer_orphans.wait_for_parent_requests([int(missing_parent_B["txid"], 16)])
peer_orphans.assert_never_requested(int(inflight_parent_AB["txid"], 16))
@cleanup
def test_orphan_of_orphan(self):
node = self.nodes[0]
peer = node.add_p2p_connection(PeerTxRelayer())
self.log.info("Test handling of an orphan with a parent who is another orphan")
missing_grandparent = self.wallet_nonsegwit.create_self_transfer()
missing_parent_orphan = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=missing_grandparent["new_utxo"])
missing_parent = self.wallet_nonsegwit.create_self_transfer()
orphan = self.wallet_nonsegwit.create_self_transfer_multi(utxos_to_spend=[missing_parent["new_utxo"], missing_parent_orphan["new_utxo"]])
# The node should put missing_parent_orphan into the orphanage and request missing_grandparent
self.relay_transaction(peer, missing_parent_orphan["tx"])
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer.wait_for_parent_requests([int(missing_grandparent["txid"], 16)])
# The node should put the orphan into the orphanage and request missing_parent, skipping
# missing_parent_orphan because it already has it in the orphanage.
self.relay_transaction(peer, orphan["tx"])
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer.wait_for_parent_requests([int(missing_parent["txid"], 16)])
@cleanup
def test_orphan_inherit_rejection(self):
node = self.nodes[0]
peer1 = node.add_p2p_connection(PeerTxRelayer())
peer2 = node.add_p2p_connection(PeerTxRelayer())
peer3 = node.add_p2p_connection(PeerTxRelayer())
self.log.info("Test that an orphan with rejected parents, along with any descendants, cannot be retried with an alternate witness")
parent_low_fee_nonsegwit = self.wallet_nonsegwit.create_self_transfer(fee_rate=0)
assert_equal(parent_low_fee_nonsegwit["txid"], parent_low_fee_nonsegwit["tx"].getwtxid())
child = self.wallet.create_self_transfer(utxo_to_spend=parent_low_fee_nonsegwit["new_utxo"])
grandchild = self.wallet.create_self_transfer(utxo_to_spend=child["new_utxo"])
assert child["txid"] != child["tx"].getwtxid()
assert grandchild["txid"] != grandchild["tx"].getwtxid()
# Relay the parent. It should be rejected because it pays 0 fees.
self.relay_transaction(peer1, parent_low_fee_nonsegwit["tx"])
# Relay the child. It should be rejected for having missing parents, and this rejection is
# cached by txid and wtxid.
with node.assert_debug_log(['not keeping orphan with rejected parents {}'.format(child["txid"])]):
self.relay_transaction(peer1, child["tx"])
assert_equal(0, len(node.getrawmempool()))
peer1.assert_never_requested(parent_low_fee_nonsegwit["txid"])
# Grandchild should also not be kept in orphanage because its parent has been rejected.
with node.assert_debug_log(['not keeping orphan with rejected parents {}'.format(grandchild["txid"])]):
self.relay_transaction(peer2, grandchild["tx"])
assert_equal(0, len(node.getrawmempool()))
peer2.assert_never_requested(child["txid"])
peer2.assert_never_requested(child["tx"].getwtxid())
# The child should never be requested, even if announced again with potentially different witness.
# Sync with ping to ensure orphans are reconsidered
peer3.send_and_ping(msg_inv([CInv(t=MSG_TX, h=int(child["txid"], 16))]))
self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP)
peer3.assert_never_requested(child["txid"])
@cleanup
def test_same_txid_orphan(self):
self.log.info("Check what happens when orphan with same txid is already in orphanage")
node = self.nodes[0]
tx_parent = self.wallet.create_self_transfer()
# Create the real child
tx_child = self.wallet.create_self_transfer(utxo_to_spend=tx_parent["new_utxo"])
# Create a fake version of the child
tx_orphan_bad_wit = self.create_malleated_version(tx_child)
bad_peer = node.add_p2p_connection(P2PInterface())
honest_peer = node.add_p2p_connection(P2PInterface())
# 1. Fake orphan is received first. It is missing an input.
bad_peer.send_and_ping(msg_tx(tx_orphan_bad_wit))
# 2. Node requests the missing parent by txid.
parent_txid_int = int(tx_parent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
bad_peer.wait_for_getdata([parent_txid_int])
# 3. Honest peer relays the real child, which is also missing parents and should be placed
# in the orphanage.
with node.assert_debug_log(["missingorspent", "stored orphan tx"]):
honest_peer.send_and_ping(msg_tx(tx_child["tx"]))
# Time out the previous request for the parent (node will not request the same transaction
# from multiple nodes at the same time)
node.bumpmocktime(GETDATA_TX_INTERVAL)
# 4. The parent is requested. Honest peer sends it.
honest_peer.wait_for_getdata([parent_txid_int])
# Sync with ping to ensure orphans are reconsidered
honest_peer.send_and_ping(msg_tx(tx_parent["tx"]))
# 5. After parent is accepted, orphans should be reconsidered.
# The real child should be accepted and the fake one rejected.
node_mempool = node.getrawmempool()
assert tx_parent["txid"] in node_mempool
assert tx_child["txid"] in node_mempool
assert_equal(node.getmempoolentry(tx_child["txid"])["wtxid"], tx_child["wtxid"])
@cleanup
def test_same_txid_orphan_of_orphan(self):
self.log.info("Check what happens when orphan's parent with same txid is already in orphanage")
node = self.nodes[0]
tx_grandparent = self.wallet.create_self_transfer()
# Create middle tx (both parent and child) which will be in orphanage.
tx_middle = self.wallet.create_self_transfer(utxo_to_spend=tx_grandparent["new_utxo"])
# Create a fake version of the middle tx
tx_orphan_bad_wit = self.create_malleated_version(tx_middle)
# Create grandchild spending from tx_middle (and spending from tx_orphan_bad_wit since they
# have the same txid).
tx_grandchild = self.wallet.create_self_transfer(utxo_to_spend=tx_middle["new_utxo"])
bad_peer = node.add_p2p_connection(P2PInterface())
honest_peer = node.add_p2p_connection(P2PInterface())
# 1. Fake orphan is received first. It is missing an input.
bad_peer.send_and_ping(msg_tx(tx_orphan_bad_wit))
# 2. Node requests missing tx_grandparent by txid.
grandparent_txid_int = int(tx_grandparent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
bad_peer.wait_for_getdata([grandparent_txid_int])
# 3. Honest peer relays the grandchild, which is missing a parent. The parent by txid already
# exists in orphanage, but should be re-requested because the node shouldn't assume that the
# witness data is the same. In this case, a same-txid-different-witness transaction exists!
with node.assert_debug_log(["stored orphan tx"]):
honest_peer.send_and_ping(msg_tx(tx_grandchild["tx"]))
middle_txid_int = int(tx_middle["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
honest_peer.wait_for_getdata([middle_txid_int])
# 4. Honest peer relays the real child, which is also missing parents and should be placed
# in the orphanage.
with node.assert_debug_log(["stored orphan tx"]):
honest_peer.send_and_ping(msg_tx(tx_middle["tx"]))
assert_equal(len(node.getrawmempool()), 0)
# 5. Honest peer sends tx_grandparent
honest_peer.send_and_ping(msg_tx(tx_grandparent["tx"]))
# 6. After parent is accepted, orphans should be reconsidered.
# The real child should be accepted and the fake one rejected.
node_mempool = node.getrawmempool()
assert tx_grandparent["txid"] in node_mempool
assert tx_middle["txid"] in node_mempool
assert tx_grandchild["txid"] in node_mempool
assert_equal(node.getmempoolentry(tx_middle["txid"])["wtxid"], tx_middle["wtxid"])
@cleanup
def test_orphan_txid_inv(self):
self.log.info("Check node does not ignore announcement with same txid as tx in orphanage")
node = self.nodes[0]
tx_parent = self.wallet.create_self_transfer()
# Create the real child and fake version
tx_child = self.wallet.create_self_transfer(utxo_to_spend=tx_parent["new_utxo"])
tx_orphan_bad_wit = self.create_malleated_version(tx_child)
bad_peer = node.add_p2p_connection(PeerTxRelayer())
# Must not send wtxidrelay because otherwise the inv(TX) will be ignored later
honest_peer = node.add_p2p_connection(P2PInterface(wtxidrelay=False))
# 1. Fake orphan is received first. It is missing an input.
bad_peer.send_and_ping(msg_tx(tx_orphan_bad_wit))
# 2. Node requests the missing parent by txid.
parent_txid_int = int(tx_parent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
bad_peer.wait_for_getdata([parent_txid_int])
# 3. Honest peer announces the real child, by txid (this isn't common but the node should
# still keep track of it).
child_txid_int = int(tx_child["txid"], 16)
honest_peer.send_and_ping(msg_inv([CInv(t=MSG_TX, h=child_txid_int)]))
# 4. The child is requested. Honest peer sends it.
node.bumpmocktime(TXREQUEST_TIME_SKIP)
honest_peer.wait_for_getdata([child_txid_int])
with node.assert_debug_log(["stored orphan tx"]):
honest_peer.send_and_ping(msg_tx(tx_child["tx"]))
# 5. After first parent request times out, the node sends another one for the missing parent
# of the real orphan child.
node.bumpmocktime(GETDATA_TX_INTERVAL)
honest_peer.wait_for_getdata([parent_txid_int])
honest_peer.send_and_ping(msg_tx(tx_parent["tx"]))
# 6. After parent is accepted, orphans should be reconsidered.
# The real child should be accepted and the fake one rejected. This may happen in either
# order since the message-processing is randomized. If tx_orphan_bad_wit is validated first,
# its consensus error leads to disconnection of bad_peer. If tx_child is validated first,
# tx_orphan_bad_wit is rejected for txn-same-nonwitness-data-in-mempool (no punishment).
node_mempool = node.getrawmempool()
assert tx_parent["txid"] in node_mempool
assert tx_child["txid"] in node_mempool
assert_equal(node.getmempoolentry(tx_child["txid"])["wtxid"], tx_child["wtxid"])
def run_test(self):
self.nodes[0].setmocktime(int(time.time()))
self.wallet_nonsegwit = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
self.generate(self.wallet_nonsegwit, 10)
self.wallet = MiniWallet(self.nodes[0])
self.generate(self.wallet, 160)
self.test_arrival_timing_orphan()
self.test_orphan_rejected_parents_exceptions()
self.test_orphan_multiple_parents()
self.test_orphans_overlapping_parents()
self.test_orphan_of_orphan()
self.test_orphan_inherit_rejection()
self.test_same_txid_orphan()
self.test_same_txid_orphan_of_orphan()
self.test_orphan_txid_inv()
if __name__ == '__main__':
OrphanHandlingTest(__file__).main()
|