Skip to content

Commit

Permalink
Merge bitcoin/bitcoin#31437: func test: Expand tx download preference…
Browse files Browse the repository at this point in the history
… tests

846a138 func test: Expand tx download preference tests (Greg Sanders)

Pull request description:

  1. Check that outbound nodes are treated the same as whitelisted connections for
  the purposes of `getdata` delays

  2. Add test case that demonstrates download retries are preferentially
  given to outbound (preferred) connections
  even when multiple announcements are
  considered ready.

  `NUM_INBOUND` is a magic number large enough that it should fail over 90% of the time
  if the underlying outbound->preferred->PriorityComputer logic was broken. Bumping this
  to 100 peers cost another 14 seconds locally for the sub-test, so I made it pretty small.

ACKs for top commit:
  i-am-yuvi:
    tACK 846a138 good catch
  maflcko:
    ACK 846a138 🍕
  marcofleon:
    lgtm ACK 846a138

Tree-SHA512: 337aa4dc33b5c0abeb4fe7e4cd5e389f7f53ae25dd991ba26615c16999872542391993020122fd255af4c7163f76c1d1feb2f2d6114f12a364c0360d4d52b8c3
  • Loading branch information
fanquake committed Feb 5, 2025
2 parents 33932d3 + 846a138 commit 1334ca6
Showing 1 changed file with 87 additions and 11 deletions.
98 changes: 87 additions & 11 deletions test/functional/p2p_tx_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Test transaction download behavior
"""
from decimal import Decimal
from enum import Enum
import time

from test_framework.mempool_util import (
Expand Down Expand Up @@ -44,17 +45,26 @@ def on_getdata(self, message):

# Constants from net_processing
GETDATA_TX_INTERVAL = 60 # seconds
INBOUND_PEER_TX_DELAY = 2 # seconds
NONPREF_PEER_TX_DELAY = 2 # seconds
INBOUND_PEER_TX_DELAY = NONPREF_PEER_TX_DELAY # inbound is non-preferred
TXID_RELAY_DELAY = 2 # seconds
OVERLOADED_PEER_DELAY = 2 # seconds
MAX_GETDATA_IN_FLIGHT = 100
MAX_PEER_TX_ANNOUNCEMENTS = 5000
NONPREF_PEER_TX_DELAY = 2

# Python test constants
NUM_INBOUND = 10
MAX_GETDATA_INBOUND_WAIT = GETDATA_TX_INTERVAL + INBOUND_PEER_TX_DELAY + TXID_RELAY_DELAY

class ConnectionType(Enum):
""" Different connection types
1. INBOUND: Incoming connection, not whitelisted
2. OUTBOUND: Outgoing connection
3. WHITELIST: Incoming connection, but whitelisted
"""
INBOUND = 0
OUTBOUND = 1
WHITELIST = 2

class TxDownloadTest(BitcoinTestFramework):
def set_test_params(self):
Expand Down Expand Up @@ -193,25 +203,90 @@ def test_notfound_fallback(self):
peer_notfound.send_and_ping(msg_notfound(vec=[CInv(MSG_WTX, WTXID)])) # Send notfound, so that fallback peer is selected
peer_fallback.wait_until(lambda: peer_fallback.tx_getdata_count >= 1, timeout=1)

def test_preferred_inv(self, preferred=False):
if preferred:
self.log.info('Check invs from preferred peers are downloaded immediately')
def test_preferred_inv(self, connection_type: ConnectionType):
if connection_type == ConnectionType.WHITELIST:
self.log.info('Check invs from preferred (whitelisted) peers are downloaded immediately')
self.restart_node(0, extra_args=['[email protected]'])
else:
elif connection_type == ConnectionType.OUTBOUND:
self.log.info('Check invs from preferred (outbound) peers are downloaded immediately')
self.restart_node(0)
elif connection_type == ConnectionType.INBOUND:
self.log.info('Check invs from non-preferred peers are downloaded after {} s'.format(NONPREF_PEER_TX_DELAY))
self.restart_node(0)
else:
raise Exception("invalid connection_type")

mock_time = int(time.time() + 1)
self.nodes[0].setmocktime(mock_time)
peer = self.nodes[0].add_p2p_connection(TestP2PConn())

if connection_type == ConnectionType.OUTBOUND:
peer = self.nodes[0].add_outbound_p2p_connection(
TestP2PConn(), wait_for_verack=True, p2p_idx=1, connection_type="outbound-full-relay")
else:
peer = self.nodes[0].add_p2p_connection(TestP2PConn())

peer.send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)]))
peer.sync_with_ping()
if preferred:
if connection_type != ConnectionType.INBOUND:
peer.wait_until(lambda: peer.tx_getdata_count >= 1, timeout=1)
else:
with p2p_lock:
assert_equal(peer.tx_getdata_count, 0)
self.nodes[0].setmocktime(mock_time + NONPREF_PEER_TX_DELAY)
peer.wait_until(lambda: peer.tx_getdata_count >= 1, timeout=1)

def test_preferred_tiebreaker_inv(self):
self.log.info("Test that preferred peers are always selected over non-preferred when ready")

self.restart_node(0)
self.nodes[0].setmocktime(int(time.time()))

# Peer that is immediately asked, but never responds.
# This will set us up to have two ready requests, one
# of which is preferred and one which is not
unresponsive_peer = self.nodes[0].add_outbound_p2p_connection(
TestP2PConn(), wait_for_verack=True, p2p_idx=0, connection_type="outbound-full-relay")
unresponsive_peer.send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)]))
unresponsive_peer.sync_with_ping()
unresponsive_peer.wait_until(lambda: unresponsive_peer.tx_getdata_count >= 1, timeout=1)

# A bunch of incoming (non-preferred) connections that advertise the same tx
non_pref_peers = []
NUM_INBOUND = 10
for _ in range(NUM_INBOUND):
non_pref_peers.append(self.nodes[0].add_p2p_connection(TestP2PConn()))
non_pref_peers[-1].send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)]))
non_pref_peers[-1].sync_with_ping()

# Check that no request made due to in-flight
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY)
with p2p_lock:
for peer in non_pref_peers:
assert_equal(peer.tx_getdata_count, 0)

# Now add another outbound (preferred) which is immediately ready for consideration
# upon advertisement
pref_peer = self.nodes[0].add_outbound_p2p_connection(
TestP2PConn(), wait_for_verack=True, p2p_idx=1, connection_type="outbound-full-relay")
pref_peer.send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)]))

assert_equal(len(self.nodes[0].getpeerinfo()), NUM_INBOUND + 2)

# Still have to wait for in-flight to timeout
with p2p_lock:
assert_equal(pref_peer.tx_getdata_count, 0)

# Timeout in-flight
self.nodes[0].bumpmocktime(GETDATA_TX_INTERVAL - NONPREF_PEER_TX_DELAY)

# Preferred peers are *always* selected next if ready
pref_peer.wait_until(lambda: pref_peer.tx_getdata_count >= 1, timeout=10)

# And none for non-preferred
for non_pref_peer in non_pref_peers:
with p2p_lock:
assert_equal(non_pref_peer.tx_getdata_count, 0)

def test_txid_inv_delay(self, glob_wtxid=False):
self.log.info('Check that inv from a txid-relay peers are delayed by {} s, with a wtxid peer {}'.format(TXID_RELAY_DELAY, glob_wtxid))
self.restart_node(0, extra_args=['[email protected]'])
Expand Down Expand Up @@ -307,8 +382,10 @@ def run_test(self):
self.test_expiry_fallback()
self.test_disconnect_fallback()
self.test_notfound_fallback()
self.test_preferred_inv()
self.test_preferred_inv(True)
self.test_preferred_tiebreaker_inv()
self.test_preferred_inv(ConnectionType.INBOUND)
self.test_preferred_inv(ConnectionType.OUTBOUND)
self.test_preferred_inv(ConnectionType.WHITELIST)
self.test_txid_inv_delay()
self.test_txid_inv_delay(True)
self.test_large_inv_batch()
Expand All @@ -335,6 +412,5 @@ def run_test(self):
self.log.info("Nodes are setup with {} incoming connections each".format(NUM_INBOUND))
test()


if __name__ == '__main__':
TxDownloadTest(__file__).main()

0 comments on commit 1334ca6

Please sign in to comment.