Skip to content

Commit 0da693f

Browse files
committed
[functional test] orphan handling with multiple announcers
1 parent b6ea4a9 commit 0da693f

File tree

2 files changed

+177
-10
lines changed

2 files changed

+177
-10
lines changed

test/functional/p2p_1p1c_network.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,6 @@ def run_test(self):
143143
for (i, peer) in enumerate(self.peers):
144144
for tx in transactions_to_presend[i]:
145145
peer.send_and_ping(msg_tx(tx))
146-
# This disconnect removes any sent orphans from the orphanage (EraseForPeer) and times
147-
# out the in-flight requests. It is currently required for the test to pass right now,
148-
# because the node will not reconsider an orphan tx and will not (re)try requesting
149-
# orphan parents from multiple peers if the first one didn't respond.
150-
# TODO: remove this in the future if the node tries orphan resolution with multiple peers.
151-
peer.peer_disconnect()
152146

153147
self.log.info("Submit full packages to node0")
154148
for package_hex in packages_to_submit:

test/functional/p2p_orphan_handling.py

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def wrapper(self):
5858
self.generate(self.nodes[0], 1)
5959
self.nodes[0].disconnect_p2ps()
6060
self.nodes[0].bumpmocktime(LONG_TIME_SKIP)
61+
# Check that mempool and orphanage have been cleared
62+
assert_equal(0, len(self.nodes[0].getorphantxs()))
63+
assert_equal(0, len(self.nodes[0].getrawmempool()))
64+
self.wallet.rescan_utxos(include_mempool=True)
6165
return wrapper
6266

6367
class PeerTxRelayer(P2PTxInvStore):
@@ -533,7 +537,7 @@ def test_same_txid_orphan_of_orphan(self):
533537
assert tx_middle["txid"] in node_mempool
534538
assert tx_grandchild["txid"] in node_mempool
535539
assert_equal(node.getmempoolentry(tx_middle["txid"])["wtxid"], tx_middle["wtxid"])
536-
assert_equal(len(node.getorphantxs()), 0)
540+
self.wait_until(lambda: len(node.getorphantxs()) == 0)
537541

538542
@cleanup
539543
def test_orphan_txid_inv(self):
@@ -585,7 +589,7 @@ def test_orphan_txid_inv(self):
585589
assert tx_parent["txid"] in node_mempool
586590
assert tx_child["txid"] in node_mempool
587591
assert_equal(node.getmempoolentry(tx_child["txid"])["wtxid"], tx_child["wtxid"])
588-
assert_equal(len(node.getorphantxs()), 0)
592+
self.wait_until(lambda: len(node.getorphantxs()) == 0)
589593

590594
@cleanup
591595
def test_max_orphan_amount(self):
@@ -610,7 +614,7 @@ def test_max_orphan_amount(self):
610614

611615
peer_1.sync_with_ping()
612616
orphanage = node.getorphantxs()
613-
assert_equal(len(orphanage), DEFAULT_MAX_ORPHAN_TRANSACTIONS)
617+
self.wait_until(lambda: len(node.getorphantxs()) == DEFAULT_MAX_ORPHAN_TRANSACTIONS)
614618

615619
for orphan in orphans:
616620
assert tx_in_orphanage(node, orphan)
@@ -626,15 +630,181 @@ def test_max_orphan_amount(self):
626630
self.log.info("Clearing the orphanage")
627631
for index, parent_orphan in enumerate(parent_orphans):
628632
peer_1.send_and_ping(msg_tx(parent_orphan))
629-
assert_equal(len(node.getorphantxs()),0)
633+
self.wait_until(lambda: len(node.getorphantxs()) == 0)
634+
635+
@cleanup
636+
def test_orphan_handling_prefer_outbound(self):
637+
self.log.info("Test that the node prefers requesting from outbound peers")
638+
node = self.nodes[0]
639+
orphan_wtxid, orphan_tx, parent_tx = self.create_parent_and_child()
640+
orphan_inv = CInv(t=MSG_WTX, h=int(orphan_wtxid, 16))
641+
642+
peer_inbound = node.add_p2p_connection(PeerTxRelayer())
643+
peer_outbound = node.add_outbound_p2p_connection(PeerTxRelayer(), p2p_idx=1)
644+
645+
# Inbound peer relays the transaction.
646+
peer_inbound.send_and_ping(msg_inv([orphan_inv]))
647+
self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP)
648+
peer_inbound.wait_for_getdata([int(orphan_wtxid, 16)])
649+
650+
# Both peers send invs for the orphan, so the node can expect both to know its ancestors.
651+
peer_outbound.send_and_ping(msg_inv([orphan_inv]))
652+
653+
peer_inbound.send_and_ping(msg_tx(orphan_tx))
654+
655+
# There should be 1 orphan with 2 announcers (we don't know what their peer IDs are)
656+
orphanage = node.getorphantxs(verbosity=2)
657+
assert_equal(orphanage[0]["wtxid"], orphan_wtxid)
658+
assert_equal(len(orphanage[0]["from"]), 2)
659+
660+
# The outbound peer should be preferred for getting orphan parents
661+
self.nodes[0].bumpmocktime(TXID_RELAY_DELAY)
662+
peer_outbound.wait_for_parent_requests([int(parent_tx.rehash(), 16)])
663+
664+
# There should be no request to the inbound peer
665+
peer_inbound.assert_never_requested(int(parent_tx.rehash(), 16))
666+
667+
self.log.info("Test that, if the preferred peer doesn't respond, the node sends another request")
668+
self.nodes[0].bumpmocktime(GETDATA_TX_INTERVAL)
669+
peer_inbound.sync_with_ping()
670+
peer_inbound.wait_for_parent_requests([int(parent_tx.rehash(), 16)])
671+
672+
@cleanup
673+
def test_announcers_before_and_after(self):
674+
self.log.info("Test that the node uses all peers who announced the tx prior to realizing it's an orphan")
675+
node = self.nodes[0]
676+
orphan_wtxid, orphan_tx, parent_tx = self.create_parent_and_child()
677+
orphan_inv = CInv(t=MSG_WTX, h=int(orphan_wtxid, 16))
678+
679+
# Announces before tx is sent, disconnects while node is requesting parents
680+
peer_early_disconnected = node.add_outbound_p2p_connection(PeerTxRelayer(), p2p_idx=3)
681+
# Announces before tx is sent, doesn't respond to parent request
682+
peer_early_unresponsive = node.add_p2p_connection(PeerTxRelayer())
683+
684+
# Announces after tx is sent
685+
peer_late_announcer = node.add_p2p_connection(PeerTxRelayer())
686+
687+
# Both peers send invs for the orphan, so the node can expect both to know its ancestors.
688+
peer_early_disconnected.send_and_ping(msg_inv([orphan_inv]))
689+
self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP)
690+
peer_early_disconnected.wait_for_getdata([int(orphan_wtxid, 16)])
691+
peer_early_unresponsive.send_and_ping(msg_inv([orphan_inv]))
692+
peer_early_disconnected.send_and_ping(msg_tx(orphan_tx))
693+
694+
# There should be 1 orphan with 2 announcers (we don't know what their peer IDs are)
695+
orphanage = node.getorphantxs(verbosity=2)
696+
assert_equal(len(orphanage), 1)
697+
assert_equal(orphanage[0]["wtxid"], orphan_wtxid)
698+
assert_equal(len(orphanage[0]["from"]), 2)
699+
700+
# Peer disconnects before responding to request
701+
self.nodes[0].bumpmocktime(TXID_RELAY_DELAY)
702+
peer_early_disconnected.wait_for_parent_requests([int(parent_tx.rehash(), 16)])
703+
peer_early_disconnected.peer_disconnect()
630704

705+
# The orphan should have 1 announcer left after the node finishes disconnecting peer_early_disconnected.
706+
self.wait_until(lambda: len(node.getorphantxs(verbosity=2)[0]["from"]) == 1)
707+
708+
# The node should retry with the other peer that announced the orphan earlier.
709+
# This node's request was additionally delayed because it's an inbound peer.
710+
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY)
711+
peer_early_unresponsive.wait_for_parent_requests([int(parent_tx.rehash(), 16)])
712+
713+
self.log.info("Test that the node uses peers who announce the tx after realizing it's an orphan")
714+
peer_late_announcer.send_and_ping(msg_inv([orphan_inv]))
715+
716+
# The orphan should have 2 announcers now
717+
orphanage = node.getorphantxs(verbosity=2)
718+
assert_equal(orphanage[0]["wtxid"], orphan_wtxid)
719+
assert_equal(len(orphanage[0]["from"]), 2)
720+
721+
self.nodes[0].bumpmocktime(GETDATA_TX_INTERVAL)
722+
peer_late_announcer.wait_for_parent_requests([int(parent_tx.rehash(), 16)])
723+
724+
@cleanup
725+
def test_parents_change(self):
726+
self.log.info("Test that, if a parent goes missing during orphan reso, it is requested")
727+
node = self.nodes[0]
728+
# Orphan will have 2 parents, 1 missing and 1 already in mempool when received.
729+
# Create missing parent.
730+
parent_missing = self.wallet.create_self_transfer()
731+
732+
# Create parent that will already be in mempool, but become missing during orphan resolution.
733+
# Get 3 UTXOs for replacement-cycled parent, UTXOS A, B, C
734+
coin_A = self.wallet.get_utxo(confirmed_only=True)
735+
coin_B = self.wallet.get_utxo(confirmed_only=True)
736+
coin_C = self.wallet.get_utxo(confirmed_only=True)
737+
# parent_peekaboo_AB spends A and B. It is replaced by tx_replacer_BC (conflicting UTXO B),
738+
# and then replaced by tx_replacer_C (conflicting UTXO C). This replacement cycle is used to
739+
# ensure that parent_peekaboo_AB can be reintroduced without requiring package RBF.
740+
FEE_INCREMENT = 2400
741+
parent_peekaboo_AB = self.wallet.create_self_transfer_multi(
742+
utxos_to_spend=[coin_A, coin_B],
743+
num_outputs=1,
744+
fee_per_output=FEE_INCREMENT
745+
)
746+
tx_replacer_BC = self.wallet.create_self_transfer_multi(
747+
utxos_to_spend=[coin_B, coin_C],
748+
num_outputs=1,
749+
fee_per_output=2*FEE_INCREMENT
750+
)
751+
tx_replacer_C = self.wallet.create_self_transfer(
752+
utxo_to_spend=coin_C,
753+
fee_per_output=3*FEE_INCREMENT
754+
)
755+
756+
# parent_peekaboo_AB starts out in the mempool
757+
node.sendrawtransaction(parent_peekaboo_AB["hex"])
758+
759+
orphan = self.wallet.create_self_transfer_multi(utxos_to_spend=[parent_peekaboo_AB["new_utxos"][0], parent_missing["new_utxo"]])
760+
orphan_wtxid = orphan["wtxid"]
761+
orphan_inv = CInv(t=MSG_WTX, h=int(orphan_wtxid, 16))
762+
763+
# peer1 sends the orphan and gets a request for the missing parent
764+
peer1 = node.add_p2p_connection(PeerTxRelayer())
765+
peer1.send_and_ping(msg_inv([orphan_inv]))
766+
node.bumpmocktime(TXREQUEST_TIME_SKIP)
767+
peer1.wait_for_getdata([int(orphan_wtxid, 16)])
768+
peer1.send_and_ping(msg_tx(orphan["tx"]))
769+
self.wait_until(lambda: node.getorphantxs(verbosity=0) == [orphan["txid"]])
770+
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
771+
peer1.wait_for_getdata([int(parent_missing["txid"], 16)])
772+
773+
# Replace parent_peekaboo_AB so that is is a newly missing parent.
774+
# Then, replace the replacement so that it can be resubmitted.
775+
node.sendrawtransaction(tx_replacer_BC["hex"])
776+
assert tx_replacer_BC["txid"] in node.getrawmempool()
777+
node.sendrawtransaction(tx_replacer_C["hex"])
778+
assert tx_replacer_BC["txid"] not in node.getrawmempool()
779+
assert tx_replacer_C["txid"] in node.getrawmempool()
780+
781+
# Second peer is an additional announcer for this orphan
782+
peer2 = node.add_p2p_connection(PeerTxRelayer())
783+
peer2.send_and_ping(msg_inv([orphan_inv]))
784+
assert_equal(len(node.getorphantxs(verbosity=2)[0]["from"]), 2)
785+
786+
# Disconnect peer1. peer2 should become the new candidate for orphan resolution.
787+
peer1.peer_disconnect()
788+
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
789+
self.wait_until(lambda: len(node.getorphantxs(verbosity=2)[0]["from"]) == 1)
790+
# Both parents should be requested, now that they are both missing.
791+
peer2.wait_for_parent_requests([int(parent_peekaboo_AB["txid"], 16), int(parent_missing["txid"], 16)])
792+
peer2.send_and_ping(msg_tx(parent_missing["tx"]))
793+
peer2.send_and_ping(msg_tx(parent_peekaboo_AB["tx"]))
794+
795+
final_mempool = node.getrawmempool()
796+
assert parent_missing["txid"] in final_mempool
797+
assert parent_peekaboo_AB["txid"] in final_mempool
798+
assert orphan["txid"] in final_mempool
799+
assert tx_replacer_C["txid"] in final_mempool
631800

632801
def run_test(self):
633802
self.nodes[0].setmocktime(int(time.time()))
634803
self.wallet_nonsegwit = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
635804
self.generate(self.wallet_nonsegwit, 10)
636805
self.wallet = MiniWallet(self.nodes[0])
637806
self.generate(self.wallet, 160)
807+
638808
self.test_arrival_timing_orphan()
639809
self.test_orphan_rejected_parents_exceptions()
640810
self.test_orphan_multiple_parents()
@@ -645,6 +815,9 @@ def run_test(self):
645815
self.test_same_txid_orphan_of_orphan()
646816
self.test_orphan_txid_inv()
647817
self.test_max_orphan_amount()
818+
self.test_orphan_handling_prefer_outbound()
819+
self.test_announcers_before_and_after()
820+
self.test_parents_change()
648821

649822

650823
if __name__ == '__main__':

0 commit comments

Comments
 (0)