Skip to content

Commit b16da7e

Browse files
committed
[functional test] attackers sending mutated orphans
1 parent 6675f64 commit b16da7e

File tree

1 file changed

+173
-1
lines changed

1 file changed

+173
-1
lines changed

test/functional/p2p_orphan_handling.py

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from test_framework.messages import (
99
CInv,
10+
CTxInWitness,
1011
MSG_TX,
1112
MSG_WITNESS_TX,
1213
MSG_WTX,
@@ -21,6 +22,7 @@
2122
NONPREF_PEER_TX_DELAY,
2223
OVERLOADED_PEER_TX_DELAY,
2324
p2p_lock,
25+
P2PInterface,
2426
P2PTxInvStore,
2527
TXID_RELAY_DELAY,
2628
)
@@ -127,6 +129,22 @@ def relay_transaction(self, peer, tx):
127129
peer.wait_for_getdata([wtxid])
128130
peer.send_and_ping(msg_tx(tx))
129131

132+
def create_malleated_version(self, tx):
133+
"""
134+
Create a malleated version of the tx where the witness is replaced with garbage data.
135+
Returns a CTransaction object.
136+
"""
137+
tx_bad_wit = tx_from_hex(tx["hex"])
138+
tx_bad_wit.wit.vtxinwit = [CTxInWitness()]
139+
# Add garbage data to witness 0. We cannot simply strip the witness, as the node would
140+
# classify it as a transaction in which the witness was missing rather than wrong.
141+
tx_bad_wit.wit.vtxinwit[0].scriptWitness.stack = [b'garbage']
142+
143+
assert_equal(tx["txid"], tx_bad_wit.rehash())
144+
assert tx["wtxid"] != tx_bad_wit.getwtxid()
145+
146+
return tx_bad_wit
147+
130148
@cleanup
131149
def test_arrival_timing_orphan(self):
132150
self.log.info("Test missing parents that arrive during delay are not requested")
@@ -284,8 +302,8 @@ def test_orphan_multiple_parents(self):
284302
# doesn't give up on the orphan. Once all of the missing parents are received, it should be
285303
# submitted to mempool.
286304
peer.send_message(msg_notfound(vec=[CInv(MSG_WITNESS_TX, int(txid_conf_old, 16))]))
305+
# Sync with ping to ensure orphans are reconsidered
287306
peer.send_and_ping(msg_tx(missing_tx["tx"]))
288-
peer.sync_with_ping()
289307
assert_equal(node.getmempoolentry(orphan["txid"])["ancestorcount"], 3)
290308

291309
@cleanup
@@ -394,10 +412,161 @@ def test_orphan_inherit_rejection(self):
394412
peer2.assert_never_requested(child["tx"].getwtxid())
395413

396414
# The child should never be requested, even if announced again with potentially different witness.
415+
# Sync with ping to ensure orphans are reconsidered
397416
peer3.send_and_ping(msg_inv([CInv(t=MSG_TX, h=int(child["txid"], 16))]))
398417
self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP)
399418
peer3.assert_never_requested(child["txid"])
400419

420+
@cleanup
421+
def test_same_txid_orphan(self):
422+
self.log.info("Check what happens when orphan with same txid is already in orphanage")
423+
node = self.nodes[0]
424+
425+
tx_parent = self.wallet.create_self_transfer()
426+
427+
# Create the real child
428+
tx_child = self.wallet.create_self_transfer(utxo_to_spend=tx_parent["new_utxo"])
429+
430+
# Create a fake version of the child
431+
tx_orphan_bad_wit = self.create_malleated_version(tx_child)
432+
433+
bad_peer = node.add_p2p_connection(P2PInterface())
434+
honest_peer = node.add_p2p_connection(P2PInterface())
435+
436+
# 1. Fake orphan is received first. It is missing an input.
437+
bad_peer.send_and_ping(msg_tx(tx_orphan_bad_wit))
438+
439+
# 2. Node requests the missing parent by txid.
440+
parent_txid_int = int(tx_parent["txid"], 16)
441+
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
442+
bad_peer.wait_for_getdata([parent_txid_int])
443+
444+
# 3. Honest peer relays the real child, which is also missing parents and should be placed
445+
# in the orphanage.
446+
with node.assert_debug_log(["missingorspent", "stored orphan tx"]):
447+
honest_peer.send_and_ping(msg_tx(tx_child["tx"]))
448+
449+
# Time out the previous request for the parent (node will not request the same transaction
450+
# from multiple nodes at the same time)
451+
node.bumpmocktime(GETDATA_TX_INTERVAL)
452+
453+
# 4. The parent is requested. Honest peer sends it.
454+
honest_peer.wait_for_getdata([parent_txid_int])
455+
# Sync with ping to ensure orphans are reconsidered
456+
honest_peer.send_and_ping(msg_tx(tx_parent["tx"]))
457+
458+
# 5. After parent is accepted, orphans should be reconsidered.
459+
# The real child should be accepted and the fake one rejected.
460+
node_mempool = node.getrawmempool()
461+
assert tx_parent["txid"] in node_mempool
462+
assert tx_child["txid"] in node_mempool
463+
assert_equal(node.getmempoolentry(tx_child["txid"])["wtxid"], tx_child["wtxid"])
464+
465+
@cleanup
466+
def test_same_txid_orphan_of_orphan(self):
467+
self.log.info("Check what happens when orphan's parent with same txid is already in orphanage")
468+
node = self.nodes[0]
469+
470+
tx_grandparent = self.wallet.create_self_transfer()
471+
472+
# Create middle tx (both parent and child) which will be in orphanage.
473+
tx_middle = self.wallet.create_self_transfer(utxo_to_spend=tx_grandparent["new_utxo"])
474+
475+
# Create a fake version of the middle tx
476+
tx_orphan_bad_wit = self.create_malleated_version(tx_middle)
477+
478+
# Create grandchild spending from tx_middle (and spending from tx_orphan_bad_wit since they
479+
# have the same txid).
480+
tx_grandchild = self.wallet.create_self_transfer(utxo_to_spend=tx_middle["new_utxo"])
481+
482+
bad_peer = node.add_p2p_connection(P2PInterface())
483+
honest_peer = node.add_p2p_connection(P2PInterface())
484+
485+
# 1. Fake orphan is received first. It is missing an input.
486+
bad_peer.send_and_ping(msg_tx(tx_orphan_bad_wit))
487+
488+
# 2. Node requests missing tx_grandparent by txid.
489+
grandparent_txid_int = int(tx_grandparent["txid"], 16)
490+
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
491+
bad_peer.wait_for_getdata([grandparent_txid_int])
492+
493+
# 3. Honest peer relays the grandchild, which is missing a parent. The parent by txid already
494+
# exists in orphanage, but should be re-requested because the node shouldn't assume that the
495+
# witness data is the same. In this case, a same-txid-different-witness transaction exists!
496+
with node.assert_debug_log(["stored orphan tx"]):
497+
honest_peer.send_and_ping(msg_tx(tx_grandchild["tx"]))
498+
middle_txid_int = int(tx_middle["txid"], 16)
499+
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
500+
honest_peer.wait_for_getdata([middle_txid_int])
501+
502+
# 4. Honest peer relays the real child, which is also missing parents and should be placed
503+
# in the orphanage.
504+
with node.assert_debug_log(["stored orphan tx"]):
505+
honest_peer.send_and_ping(msg_tx(tx_middle["tx"]))
506+
assert_equal(len(node.getrawmempool()), 0)
507+
508+
# 5. Honest peer sends tx_grandparent
509+
honest_peer.send_and_ping(msg_tx(tx_grandparent["tx"]))
510+
511+
# 6. After parent is accepted, orphans should be reconsidered.
512+
# The real child should be accepted and the fake one rejected.
513+
node_mempool = node.getrawmempool()
514+
assert tx_grandparent["txid"] in node_mempool
515+
assert tx_middle["txid"] in node_mempool
516+
assert tx_grandchild["txid"] in node_mempool
517+
assert_equal(node.getmempoolentry(tx_middle["txid"])["wtxid"], tx_middle["wtxid"])
518+
519+
@cleanup
520+
def test_orphan_txid_inv(self):
521+
self.log.info("Check node does not ignore announcement with same txid as tx in orphanage")
522+
node = self.nodes[0]
523+
524+
tx_parent = self.wallet.create_self_transfer()
525+
526+
# Create the real child and fake version
527+
tx_child = self.wallet.create_self_transfer(utxo_to_spend=tx_parent["new_utxo"])
528+
tx_orphan_bad_wit = self.create_malleated_version(tx_child)
529+
530+
bad_peer = node.add_p2p_connection(PeerTxRelayer())
531+
# Must not send wtxidrelay because otherwise the inv(TX) will be ignored later
532+
honest_peer = node.add_p2p_connection(P2PInterface(wtxidrelay=False))
533+
534+
# 1. Fake orphan is received first. It is missing an input.
535+
bad_peer.send_and_ping(msg_tx(tx_orphan_bad_wit))
536+
537+
# 2. Node requests the missing parent by txid.
538+
parent_txid_int = int(tx_parent["txid"], 16)
539+
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
540+
bad_peer.wait_for_getdata([parent_txid_int])
541+
542+
# 3. Honest peer announces the real child, by txid (this isn't common but the node should
543+
# still keep track of it).
544+
child_txid_int = int(tx_child["txid"], 16)
545+
honest_peer.send_and_ping(msg_inv([CInv(t=MSG_TX, h=child_txid_int)]))
546+
547+
# 4. The child is requested. Honest peer sends it.
548+
node.bumpmocktime(TXREQUEST_TIME_SKIP)
549+
honest_peer.wait_for_getdata([child_txid_int])
550+
with node.assert_debug_log(["stored orphan tx"]):
551+
honest_peer.send_and_ping(msg_tx(tx_child["tx"]))
552+
553+
# 5. After first parent request times out, the node sends another one for the missing parent
554+
# of the real orphan child.
555+
node.bumpmocktime(GETDATA_TX_INTERVAL)
556+
honest_peer.wait_for_getdata([parent_txid_int])
557+
honest_peer.send_and_ping(msg_tx(tx_parent["tx"]))
558+
559+
# 6. After parent is accepted, orphans should be reconsidered.
560+
# The real child should be accepted and the fake one rejected. This may happen in either
561+
# order since the message-processing is randomized. If tx_orphan_bad_wit is validated first,
562+
# its consensus error leads to disconnection of bad_peer. If tx_child is validated first,
563+
# tx_orphan_bad_wit is rejected for txn-same-nonwitness-data-in-mempool (no punishment).
564+
node_mempool = node.getrawmempool()
565+
assert tx_parent["txid"] in node_mempool
566+
assert tx_child["txid"] in node_mempool
567+
assert_equal(node.getmempoolentry(tx_child["txid"])["wtxid"], tx_child["wtxid"])
568+
569+
401570
def run_test(self):
402571
self.nodes[0].setmocktime(int(time.time()))
403572
self.wallet_nonsegwit = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
@@ -410,6 +579,9 @@ def run_test(self):
410579
self.test_orphans_overlapping_parents()
411580
self.test_orphan_of_orphan()
412581
self.test_orphan_inherit_rejection()
582+
self.test_same_txid_orphan()
583+
self.test_same_txid_orphan_of_orphan()
584+
self.test_orphan_txid_inv()
413585

414586

415587
if __name__ == '__main__':

0 commit comments

Comments
 (0)