From 4082a8af68e423226dc77fbb2d74ce6728228d8b Mon Sep 17 00:00:00 2001 From: glozow Date: Fri, 26 Jul 2024 16:34:55 +0100 Subject: [PATCH] [functional test] orphan handling with multiple announcers --- test/functional/p2p_1p1c_network.py | 6 --- test/functional/p2p_orphan_handling.py | 71 ++++++++++++++++++++++++++ test/functional/test_framework/p2p.py | 2 + 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/test/functional/p2p_1p1c_network.py b/test/functional/p2p_1p1c_network.py index c3cdb3e0b319d..95514c98aca05 100755 --- a/test/functional/p2p_1p1c_network.py +++ b/test/functional/p2p_1p1c_network.py @@ -146,12 +146,6 @@ def run_test(self): 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: diff --git a/test/functional/p2p_orphan_handling.py b/test/functional/p2p_orphan_handling.py index 22600bf8a4e92..9ff47f26d6d5c 100755 --- a/test/functional/p2p_orphan_handling.py +++ b/test/functional/p2p_orphan_handling.py @@ -20,6 +20,7 @@ from test_framework.p2p import ( GETDATA_TX_INTERVAL, NONPREF_PEER_TX_DELAY, + ORPHAN_ANCESTOR_GETDATA_INTERVAL, OVERLOADED_PEER_TX_DELAY, p2p_lock, P2PInterface, @@ -566,6 +567,74 @@ def test_orphan_txid_inv(self): assert tx_child["txid"] in node_mempool assert_equal(node.getmempoolentry(tx_child["txid"])["wtxid"], tx_child["wtxid"]) + @cleanup + def test_orphan_handling_prefer_outbound(self): + self.log.info("Test that the node prefers requesting from outbound peers") + node = self.nodes[0] + orphan_wtxid, orphan_tx, parent_tx = self.create_parent_and_child() + orphan_inv = CInv(t=MSG_WTX, h=int(orphan_wtxid, 16)) + + peer_inbound = node.add_p2p_connection(PeerTxRelayer()) + peer_outbound = node.add_outbound_p2p_connection(PeerTxRelayer(), p2p_idx=1) + + # Inbound peer relays the transaction. + peer_inbound.send_and_ping(msg_inv([orphan_inv])) + self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP) + peer_inbound.wait_for_getdata([int(orphan_wtxid, 16)]) + + # Both peers send invs for the orphan, so the node can expect both to know its ancestors. + peer_outbound.send_and_ping(msg_inv([orphan_inv])) + + # The outbound peer should be preferred for getting orphan parents + peer_inbound.send_and_ping(msg_tx(orphan_tx)) + self.nodes[0].bumpmocktime(TXID_RELAY_DELAY) + peer_outbound.wait_for_parent_requests([int(parent_tx.rehash(), 16)]) + + # There should be no request to the inbound peer + peer_inbound.assert_never_requested(int(parent_tx.rehash(), 16)) + + self.log.info("Test that, if the preferred peer doesn't respond, the node sends another request") + self.nodes[0].bumpmocktime(ORPHAN_ANCESTOR_GETDATA_INTERVAL) + peer_inbound.sync_with_ping() + peer_inbound.wait_for_parent_requests([int(parent_tx.rehash(), 16)]) + + @cleanup + def test_announcers_before_and_after(self): + self.log.info("Test that the node uses all peers who announced the tx prior to realizing it's an orphan") + node = self.nodes[0] + orphan_wtxid, orphan_tx, parent_tx = self.create_parent_and_child() + orphan_inv = CInv(t=MSG_WTX, h=int(orphan_wtxid, 16)) + + # Announces before tx is sent, disconnects while node is requesting parents + peer_early_disconnected = node.add_outbound_p2p_connection(PeerTxRelayer(), p2p_idx=3) + # Announces before tx is sent, doesn't respond to parent request + peer_early_unresponsive = node.add_p2p_connection(PeerTxRelayer()) + + # Announces after tx is sent + peer_late_announcer = node.add_p2p_connection(PeerTxRelayer()) + + # Both peers send invs for the orphan, so the node an expect both to know its ancestors. + peer_early_disconnected.send_and_ping(msg_inv([orphan_inv])) + self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP) + peer_early_disconnected.wait_for_getdata([int(orphan_wtxid, 16)]) + peer_early_unresponsive.send_and_ping(msg_inv([orphan_inv])) + peer_early_disconnected.send_and_ping(msg_tx(orphan_tx)) + + # Peer disconnects before responding to request + self.nodes[0].bumpmocktime(TXID_RELAY_DELAY) + peer_early_disconnected.wait_for_parent_requests([int(parent_tx.rehash(), 16)]) + peer_early_disconnected.peer_disconnect() + + # The node should retry with the other peer that announced the orphan earlier. + # This node's request was additionally delayed because it's an inbound peer. + self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY) + peer_early_unresponsive.wait_for_parent_requests([int(parent_tx.rehash(), 16)]) + + self.log.info("Test that the node uses peers who announce the tx after realizing it's an orphan") + peer_late_announcer.send_and_ping(msg_inv([orphan_inv])) + + self.nodes[0].bumpmocktime(ORPHAN_ANCESTOR_GETDATA_INTERVAL) + peer_late_announcer.wait_for_parent_requests([int(parent_tx.rehash(), 16)]) def run_test(self): self.nodes[0].setmocktime(int(time.time())) @@ -582,6 +651,8 @@ def run_test(self): self.test_same_txid_orphan() self.test_same_txid_orphan_of_orphan() self.test_orphan_txid_inv() + self.test_orphan_handling_prefer_outbound() + self.test_announcers_before_and_after() if __name__ == '__main__': diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py index 4f1265eb54889..ffbdca73f5996 100755 --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -108,6 +108,8 @@ OVERLOADED_PEER_TX_DELAY = 2 # How long to wait before downloading a transaction from an additional peer GETDATA_TX_INTERVAL = 60 +# How long to wait before requesting orphan ancpkginfo/parents from an additional peer +ORPHAN_ANCESTOR_GETDATA_INTERVAL = 60 MESSAGEMAP = { b"addr": msg_addr,