test: add functional test for outbound connection management #33954

pull mzumsande wants to merge 3 commits into bitcoin:master from mzumsande:202511_test_outbound_mgmt changing 4 files +355 −87
  1. mzumsande commented at 7:01 pm on November 26, 2025: contributor

    For #29415, vasild added the possibility to have outbound connections made “naturally” by the node using addrman entries, by redirecting them via a SOCKS5 proxy to python p2ps or other full nodes, so that the node under test behaves as if it is connected to normal non-local peers from arbitrary networks, while we have full control over the peers.

    I think this feature is really cool and want to suggest to integrate it more prominently into the test framework ("auto_outbound_mode") in the first commit because it allows to test the code in which the node decides to make connections - in contrast to the existing add_outbound_p2p_connection where the functional test intiates the connections.

    The second commit uses it to add functional test coverage for the following features of outbound connection management:

    • extra full outbound connections due to a stale tip (#11560)
    • periodic block-relay-only connections (#19858)
    • full outbound connection management with respect to networks (#27213)

    This should work well with the test from #29415 (which would keep its own destinations_factory but can call _create_auto_outbound_listener to avoid much of code duplication).

    Other possible uses for auto_outbound_mode I could think of are more detailed testing of anchors (feature_anchors.py), feeler connections, or any other p2p logic that uses the network of peers.

  2. DrahtBot added the label Tests on Nov 26, 2025
  3. DrahtBot commented at 7:01 pm on November 26, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33954.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK vasild

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34457 (wallet: add private broadcast support for wallet transactions by w0xlt)
    • #34410 (test: let connections happen in any order in p2p_private_broadcast.py by vasild)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  4. mzumsande force-pushed on Nov 26, 2025
  5. DrahtBot added the label CI failed on Nov 26, 2025
  6. DrahtBot removed the label CI failed on Nov 26, 2025
  7. mzumsande marked this as ready for review on Nov 26, 2025
  8. vasild commented at 9:22 am on November 27, 2025: contributor
    Concept ACK, thanks!
  9. mzumsande marked this as a draft on Nov 28, 2025
  10. mzumsande commented at 10:08 pm on November 28, 2025: contributor
    there are still some intermittent timeouts, will put in draft until I’ve fixed them.
  11. in test/functional/test_framework/test_framework.py:410 in 1158564303
    403@@ -395,6 +404,72 @@ def setup_nodes(self):
    404                 assert_equal(chain_info["blocks"], 200)
    405                 assert_equal(chain_info["initialblockdownload"], False)
    406 
    407+    def _create_auto_outbound_listener(self, listener, connection_index):
    408+        """Helper to create and setup a P2P listener for auto outbound mode"""
    409+        # Tor ports are the highest among p2p/rpc/tor, so this should be the first available port.
    410+        ports_base = tor_port(MAX_NODES) + 1
    


    vasild commented at 7:45 am on December 8, 2025:
    Would it make sense to introduce auto_listener_port(n)?

    maflcko commented at 8:03 pm on December 11, 2025:
    Haven’t looked here in detail, but my preference would be to not call + or += on an integral port returned by a helper function. This will lead to issues down the line, and it would be better to always call the helper that internally asserts and checks the port range. See https://github.com/bitcoin/bitcoin/pull/33260/files

    mzumsande commented at 8:10 pm on December 11, 2025:

    In trying to apply this I think I found a more general problem with port collisions: As soon as more than one test uses this feature, there is the possibility of port collisions between two tests that run in parallel. So we would need to assign separate ranges. We know that a node will only make up to 11 outbound connections at a given moment, but currently disconnections/new connections will just be added to self.auto_outbound_destinations, so something like

    0def auto_listener_port(n):
    1    return p2p_port(n) + PORT_RANGE * 3
    

    wouldn’t work either. Maybe we could use the fact that the node will never make more than 11 outbound connection at the same time and re-use ports of disconnected nodes for later nodes. I’ll have to think more about this.


    vasild commented at 6:40 am on December 12, 2025:
    There are about 60k ports to choose from. There must be a way to pick in a collision free manner even without reuse…

    mzumsande commented at 0:05 am on January 2, 2026:
    I cherry-picked the commit from #34186, making it possible to use port=0 and then let the OS deal with it - much better than assigning the port by the test itself.
  12. in test/functional/test_framework/test_framework.py:438 in 1158564303 outdated
    433+        return (actual_to_addr, actual_to_port)
    434+
    435+    def _setup_auto_outbound_mode(self):
    436+        """Setup SOCKS5 proxy with destinations factory for auto outbound mode."""
    437+        socks5_server_config = Socks5Configuration()
    438+        socks5_server_config.addr = ("127.0.0.1", p2p_port(self.num_nodes))
    


    vasild commented at 7:48 am on December 8, 2025:
    This is somewhat of a misuse of p2p_port(). It works, but looking at it I wonder if it can be done better. Maybe introduce proxy_port(n)? At some point if we implement also SAM proxy (for hijacking I2P connections), then we might need e.g. proxy_port(0) for the SOCKS5 proxy and proxy_port(1) for the SAM proxy.

    vasild commented at 9:02 am on January 9, 2026:

    With the changes from #34186 / socks5.py (now also included in this PR), this can be changed to:

    0socks5_server_config.addr = ("127.0.0.1", 0)
    

    mzumsande commented at 3:41 pm on February 24, 2026:
    done
  13. in test/functional/test_framework/test_framework.py:457 in 1158564303
    452+                # Create listener with default P2PInterface
    453+                listener = P2PInterface()
    454+                actual_to_addr, actual_to_port = self._create_auto_outbound_listener(listener, i)
    455+
    456+                self.log.debug(f"Instructing the SOCKS5 proxy to redirect connection i={i} to "
    457+                               f"{format_addr_port(actual_to_addr, actual_to_port)} (a Python node)")
    


    vasild commented at 12:21 pm on December 8, 2025:

    If this is changed to:

     0-                # Create listener with default P2PInterface
     1-                listener = P2PInterface()
     2-                actual_to_addr, actual_to_port = self._create_auto_outbound_listener(listener, i)
     3+                if self.decide_auto_outbound_where_to:
     4+                    actual_to_addr, actual_to_port, listener = self.decide_auto_outbound_where_to(i)
     5+                else:
     6+                    # Create listener with default P2PInterface
     7+                    listener = P2PInterface()
     8+                    actual_to_addr, actual_to_port = self._create_auto_outbound_listener(listener, i)
     9 
    10                 self.log.debug(f"Instructing the SOCKS5 proxy to redirect connection i={i} to "
    11-                               f"{format_addr_port(actual_to_addr, actual_to_port)} (a Python node)")
    12+                               f"{format_addr_port(actual_to_addr, actual_to_port)}")
    

    then it is possible to override from the test:

    • the decision where to redirect and
    • the listener creation (in case something other than P2PInterface is needed, e.g. P2PDataStore)

    With that change, the private broadcast test can take the following simplification (28 insertions(+), 78 deletions(-)):

      0diff --git i/test/functional/p2p_private_broadcast.py w/test/functional/p2p_private_broadcast.py
      1index 4e97da666a..697d44c299 100755
      2--- i/test/functional/p2p_private_broadcast.py
      3+++ w/test/functional/p2p_private_broadcast.py
      4@@ -162,95 +162,45 @@ ADDRMAN_ADDRESSES = [
      5     "[fc00::20]",
      6 ]
      7 
      8 
      9 class P2PPrivateBroadcast(BitcoinTestFramework):
     10     def set_test_params(self):
     11-        self.disable_autoconnect = False
     12+        self.auto_outbound_mode = True
     13         self.num_nodes = 2
     14 
     15     def setup_nodes(self):
     16-        # Start a SOCKS5 proxy server.
     17-        socks5_server_config = Socks5Configuration()
     18-        # self.nodes[0] listens on p2p_port(0),
     19-        # self.nodes[1] listens on p2p_port(1),
     20-        # thus we tell the SOCKS5 server to listen on p2p_port(self.num_nodes) (self.num_nodes is 2)
     21-        socks5_server_config.addr = ("127.0.0.1", p2p_port(self.num_nodes))
     22-        socks5_server_config.unauth = True
     23-        socks5_server_config.auth = True
     24-
     25-        self.socks5_server = Socks5Server(socks5_server_config)
     26-        self.socks5_server.start()
     27-
     28-        # Tor ports are the highest among p2p/rpc/tor, so this should be the first available port.
     29-        ports_base = tor_port(MAX_NODES) + 1
     30-
     31-        self.destinations = []
     32-
     33-        self.destinations_lock = threading.Lock()
     34-
     35-        def destinations_factory(requested_to_addr, requested_to_port):
     36-            with self.destinations_lock:
     37-                i = len(self.destinations)
     38-                actual_to_addr = ""
     39-                actual_to_port = 0
     40-                listener = None
     41-                if i == NUM_INITIAL_CONNECTIONS:
     42-                    # Instruct the SOCKS5 server to redirect the first private
     43-                    # broadcast connection from nodes[0] to nodes[1]
     44-                    actual_to_addr = "127.0.0.1" # nodes[1] listen address
     45-                    actual_to_port = tor_port(1) # nodes[1] listen port for Tor
     46-                    self.log.debug(f"Instructing the SOCKS5 proxy to redirect connection i={i} to "
     47-                                   f"{format_addr_port(actual_to_addr, actual_to_port)} (nodes[1])")
     48-                else:
     49-                    # Create a Python P2P listening node and instruct the SOCKS5 proxy to
     50-                    # redirect the connection to it. The first outbound connection is used
     51-                    # later to serve GETDATA, thus make it P2PDataStore().
     52-                    listener = P2PDataStore() if i == 0 else P2PInterface()
     53-                    listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)
     54-                    listener.peer_connect_send_version(services=P2P_SERVICES)
     55-
     56-                    def on_listen_done(addr, port):
     57-                        nonlocal actual_to_addr
     58-                        nonlocal actual_to_port
     59-                        actual_to_addr = addr
     60-                        actual_to_port = port
     61-
     62-                    self.network_thread.listen(
     63-                        addr="127.0.0.1",
     64-                        port=ports_base + i,
     65-                        p2p=listener,
     66-                        callback=on_listen_done)
     67-                    # Wait until the callback has been called.
     68-                    self.wait_until(lambda: actual_to_port != 0)
     69-                    self.log.debug(f"Instructing the SOCKS5 proxy to redirect connection i={i} to "
     70-                                   f"{format_addr_port(actual_to_addr, actual_to_port)} (a Python node)")
     71-
     72-                self.destinations.append({
     73-                    "requested_to": format_addr_port(requested_to_addr, requested_to_port),
     74-                    "node": listener,
     75-                })
     76-                assert_equal(len(self.destinations), i + 1)
     77-
     78-                return {
     79-                    "actual_to_addr": actual_to_addr,
     80-                    "actual_to_port": actual_to_port,
     81-                }
     82-
     83-        self.socks5_server.conf.destinations_factory = destinations_factory
     84+
     85+        def decide_auto_outbound_where_to(connection_index):
     86+            actual_to_addr = ""
     87+            actual_to_port = 0
     88+            listener = None
     89+            if connection_index == NUM_INITIAL_CONNECTIONS:
     90+                # Instruct the SOCKS5 server to redirect the first private
     91+                # broadcast connection from nodes[0] to nodes[1]
     92+                actual_to_addr = "127.0.0.1" # nodes[1] listen address
     93+                actual_to_port = tor_port(1) # nodes[1] listen port for Tor
     94+            else:
     95+                # Create a Python P2P listening node and instruct the SOCKS5 proxy to
     96+                # redirect the connection to it. The first outbound connection is used
     97+                # later to serve GETDATA, thus make it P2PDataStore().
     98+                listener = P2PDataStore() if connection_index == 0 else P2PInterface()
     99+                actual_to_addr, actual_to_port = self._create_auto_outbound_listener(listener, connection_index)
    100+            return actual_to_addr, actual_to_port, listener
    101+
    102+        self.decide_auto_outbound_where_to = decide_auto_outbound_where_to
    103 
    104         self.extra_args = [
    105             [
    106                 # Needed to be able to add CJDNS addresses to addrman (otherwise they are unroutable).
    107                 "-cjdnsreachable",
    108                 # Connecting, sending garbage, being disconnected messes up with this test's
    109                 # check_broadcasts() which waits for a particular Python node to receive a connection.
    110                 "-v2transport=0",
    111                 "-test=addrman",
    112                 "-privatebroadcast",
    113-                f"-proxy={socks5_server_config.addr[0]}:{socks5_server_config.addr[1]}",
    114                 # To increase coverage, make it think that the I2P network is reachable so that it
    115                 # selects such addresses as well. Pick a proxy address where nobody is listening
    116                 # and connection attempts fail quickly.
    117                 "-i2psam=127.0.0.1:1",
    118             ],
    119             [
    120@@ -268,14 +218,14 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
    121         i = skip_destinations - 1
    122         while broadcasts_done < broadcasts_to_expect:
    123             i += 1
    124             self.log.debug(f"{label}: waiting for outbound connection i={i}")
    125             # At this point the connection may not yet have been established (A),
    126             # may be active (B), or may have already been closed (C).
    127-            self.wait_until(lambda: len(self.destinations) > i)
    128-            dest = self.destinations[i]
    129+            self.wait_until(lambda: len(self.auto_outbound_destinations) > i)
    130+            dest = self.auto_outbound_destinations[i]
    131             peer = dest["node"]
    132             peer.wait_until(lambda: peer.message_count["version"] == 1, check_connected=False)
    133             # Now it is either (B) or (C).
    134             if peer.last_message["version"].nServices != 0:
    135                 self.log.debug(f"{label}: outbound connection i={i} to {dest['requested_to']} not a private broadcast, ignoring it (maybe feeler or extra block only)")
    136                 continue
    137@@ -313,13 +263,13 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
    138         # Fill tx_originator's addrman.
    139         for addr in ADDRMAN_ADDRESSES:
    140             res = tx_originator.addpeeraddress(address=addr, port=8333, tried=False)
    141             if not res["success"]:
    142                 self.log.debug(f"Could not add {addr} to tx_originator's addrman (collision?)")
    143 
    144-        self.wait_until(lambda: len(self.destinations) == NUM_INITIAL_CONNECTIONS)
    145+        self.wait_until(lambda: len(self.auto_outbound_destinations) == NUM_INITIAL_CONNECTIONS)
    146 
    147         # The next opened connection by tx_originator should be "private broadcast"
    148         # for sending the transaction. The SOCKS5 proxy should redirect it to tx_receiver.
    149 
    150         txs = wallet.create_self_transfer_chain(chain_length=3)
    151         self.log.info(f"Created txid={txs[0]['txid']}: for basic test")
    152@@ -353,31 +303,31 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
    153         assert_equal(len(tx_originator.getrawmempool()), 0)
    154 
    155         wtxid_int = int(txs[0]["wtxid"], 16)
    156         inv = CInv(MSG_WTX, wtxid_int)
    157 
    158         self.log.info("Sending INV and waiting for GETDATA from node")
    159-        tx_returner = self.destinations[0]["node"] # Will return the transaction back to the originator.
    160+        tx_returner = self.auto_outbound_destinations[0]["node"] # Will return the transaction back to the originator.
    161         tx_returner.tx_store[wtxid_int] = txs[0]["tx"]
    162         assert "getdata" not in tx_returner.last_message
    163         received_back_msg = f"Received our privately broadcast transaction (txid={txs[0]['txid']}) from the network"
    164         with tx_originator.assert_debug_log(expected_msgs=[received_back_msg]):
    165             tx_returner.send_without_ping(msg_inv([inv]))
    166             tx_returner.wait_until(lambda: "getdata" in tx_returner.last_message)
    167             self.wait_until(lambda: len(tx_originator.getrawmempool()) > 0)
    168 
    169         self.log.info("Waiting for normal broadcast to another peer")
    170-        self.destinations[1]["node"].wait_for_inv([inv])
    171+        self.auto_outbound_destinations[1]["node"].wait_for_inv([inv])
    172 
    173         self.log.info("Sending a transaction that is already in the mempool")
    174-        skip_destinations = len(self.destinations)
    175+        skip_destinations = len(self.auto_outbound_destinations)
    176         tx_originator.sendrawtransaction(hexstring=txs[0]["hex"], maxfeerate=0)
    177         self.check_broadcasts("Broadcast of mempool transaction", txs[0], NUM_PRIVATE_BROADCAST_PER_TX, skip_destinations)
    178 
    179         self.log.info("Sending a transaction with a dependency in the mempool")
    180-        skip_destinations = len(self.destinations)
    181+        skip_destinations = len(self.auto_outbound_destinations)
    182         tx_originator.sendrawtransaction(hexstring=txs[1]["hex"], maxfeerate=0.1)
    183         self.check_broadcasts("Dependency in mempool", txs[1], NUM_PRIVATE_BROADCAST_PER_TX, skip_destinations)
    184 
    185         self.log.info("Sending a transaction with a dependency not in the mempool (should be rejected)")
    186         assert_equal(len(tx_originator.getrawmempool()), 1)
    187         assert_raises_rpc_error(-25, "bad-txns-inputs-missingorspent",
    188@@ -388,13 +338,13 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
    189         # Since txs[1] has not been received back by tx_originator,
    190         # it should be re-broadcast after a while. Advance tx_originator's clock
    191         # to trigger a re-broadcast. Should be more than the maximum returned by
    192         # NextTxBroadcast() in net_processing.cpp.
    193         self.log.info("Checking that rebroadcast works")
    194         delta = 20 * 60 # 20min
    195-        skip_destinations = len(self.destinations)
    196+        skip_destinations = len(self.auto_outbound_destinations)
    197         rebroadcast_msg = f"Reattempting broadcast of stale txid={txs[1]['txid']}"
    198         with tx_originator.busy_wait_for_debug_log(expected_msgs=[rebroadcast_msg.encode()]):
    199             tx_originator.setmocktime(int(time.time()) + delta)
    200             tx_originator.mockscheduler(delta)
    201         self.check_broadcasts("Rebroadcast", txs[1], 1, skip_destinations)
    202         tx_originator.setmocktime(0) # Let the clock tick again (it will go backwards due to this).
    

    mzumsande commented at 8:10 pm on December 11, 2025:
    done, used the name auto_outbound_factory
  14. mzumsande force-pushed on Dec 11, 2025
  15. mzumsande force-pushed on Jan 2, 2026
  16. mzumsande force-pushed on Jan 2, 2026
  17. mzumsande force-pushed on Jan 2, 2026
  18. in test/functional/test_framework/test_framework.py:456 in 0cb08573f3 outdated
    451+                if self.auto_outbound_factory:
    452+                    actual_to_addr, actual_to_port, listener = self.auto_outbound_factory(i)
    453+                else:
    454+                    # Create listener with default P2PInterface
    455+                    listener = P2PInterface()
    456+                    actual_to_addr, actual_to_port = self._create_auto_outbound_listener(listener)
    


    vasild commented at 9:16 am on January 9, 2026:

    Maybe get auto_outbound_factory() and _create_auto_outbound_listener() to have the same interface? That is, change _create_auto_outbound_listener() to also return the listener? Then this will be more consistent:

    0                if self.auto_outbound_factory:
    1                    actual_to_addr, actual_to_port, listener = self.auto_outbound_factory(i)
    2                else:
    3                    actual_to_addr, actual_to_port, listener = self._create_auto_outbound_listener(listener)
    

    and then we can have one function (pointer) that either points to the default one or to the user-provided one and here just:

    0actual_to_addr, actual_to_port, listener = self.auto_outbound_factory(i)
    

    without an if condition. That would mean to also rename _create_auto_outbound_listener() to auto_outbound_factory() and remove the line self.auto_outbound_factory = None from BitcoinTestFramework::__init__().

    Feel free to ignore if you do not see this as an improvement.


    mzumsande commented at 4:16 pm on February 24, 2026:
    I like that suggestion - took it!
  19. in test/functional/test_framework/test_framework.py:531 in 0cb08573f3 outdated
    522@@ -447,6 +523,13 @@ def bin_dir_from_version(version):
    523             extra_confs = [[]] * num_nodes
    524         if extra_args is None:
    525             extra_args = [[]] * num_nodes
    526+        # Add auto outbound mode proxy configuration
    527+        if self.auto_outbound_mode:
    528+            socks5_config = self._setup_auto_outbound_mode()
    529+            for i in range(num_nodes):
    530+                extra_args[i] = extra_args[i] + [
    531+                    f"-proxy={socks5_config.addr[0]}:{socks5_config.addr[1]}"
    


    vasild commented at 9:21 am on January 9, 2026:
    Maybe this needs format_addr_port() as well, in case the address happens to be IPv6 and needs to be enclosed in []?

    mzumsande commented at 4:15 pm on February 24, 2026:
    The config address is hardcoded in _setup_auto_outbound_mode to 127.0.0.1, so IPv6 shouldn’t be a concern I think?

    vasild commented at 8:30 am on February 25, 2026:
    Right, only if changed in the future. Or if somebody is playing with this locally and changes it to fir their environment. An address like ::1 would also upset this because it would result in -proxy=::1:12345 which I guess will result in “syntax” error.
  20. in test/functional/p2p_outbound_management.py:75 in 0cb08573f3 outdated
    70+
    71+    def disconnect_last_block_relay_peer(self):
    72+        """Find and disconnect the most recently connected block-relay-only peer.
    73+           Done manually instead of waiting for the node to disconnect automatically to
    74+           avoid rare interactions with the scheduling of the next extra block-relay-only peer"""
    75+        node = self.nodes[0]
    


    vasild commented at 9:49 am on January 9, 2026:

    The other helper functions has_regular_outbounds(), has_extra_block_relay_peer() and has_extra_full_outbound_peer() take a node argument. Maybe add such argument also to disconnect_last_block_relay_peer() for consistency?

    PS1 same for wait_for_network_specific_peer() PS2 all of those end up using self.nodes[0], so another option is to drop the node argument from all.


    mzumsande commented at 5:22 pm on February 24, 2026:
    I decided to drop the node arg everywhere.
  21. in test/functional/p2p_outbound_management.py:95 in 0cb08573f3
    90+        node = self.nodes[0]
    91+
    92+        self.log.info("Advance time")
    93+        self.cur_time += self.MOCKTIME_BUMP
    94+        node.setmocktime(self.cur_time)
    95+        self.generate(node, 1)  # Disable stale-tip extra-outbound connections that could interfer
    


    vasild commented at 9:53 am on January 9, 2026:
    0        self.generate(node, 1)  # Disable stale-tip extra-outbound connections that could interfere
    

    vasild commented at 9:57 am on January 9, 2026:
    Is there an unlikely scenario that a stale-tip extra-outbound connection is initiated after advancing the mocktime and before self.generate(node, 1)? And eventually causing the interference that this is trying to disable?

    mzumsande commented at 4:55 pm on February 24, 2026:
    I think probably not in practice - CheckForStaleTipAndEvictPeers fires every 45 seconds (on a steady clock, not influenced by the mocktime bump). So I think this means it would only fire if 45 real seconds passed since the last mockscheduler call, which seems very unlikely.
  22. in test/functional/p2p_outbound_management.py:89 in 0cb08573f3 outdated
    84+        """Advance time, wait for a network-specific full outbound peer, and trigger eviction.
    85+
    86+        Extra block-relay-only connections are prioritized over network-specific ones, but defensively:
    87+        if an already connected tor or cjdns address is picked, there is no retry. This can cause either
    88+        type of peer to arrive first.
    89+        """
    


    vasild commented at 10:30 am on January 9, 2026:
    Is it possible that the node already has at least one connection to all reachable networks when this function starts? Would that result in timeouts due to has_extra_full_outbound_peer(node) never returning True?

    mzumsande commented at 5:12 pm on February 24, 2026:
    To avoid this, the test first adds 100 IPv4 addresses to addrman, then waits for the regular otubound connections (setup_peers), and only then the onion and cjdns addresses are added to addrman. So I think it shouldn’t be possible.

    vasild commented at 8:55 am on February 25, 2026:
    What about the tor and cjdns connections being established after adding to addrman and before wait_for_network_specific_peer() is called?
  23. vasild approved
  24. vasild commented at 10:44 am on January 9, 2026: contributor

    ACK 0cb08573f38b7aa8a18147eef1a775b6c24bcb7c

    I ran test_runner.py and p2p_outbound_management.py a bunch of times locally and did not observe timeouts.

  25. DrahtBot added the label Needs rebase on Jan 14, 2026
  26. mzumsande force-pushed on Feb 24, 2026
  27. test: add auto_outbound_mode to test framework
    This new option allows tests to make use of automatic outbound connections
    (made from addrman).
    A SOCKS5 proxy is used to redirect the connection, by default to a P2PInterface
    
    For advanced customization, tests can provide auto_outbound_factory()
    to overwrite where each outbound connection made by the node should be
    redirected to (for example to another node)
    
    Co-authored-by: Vasil Dimov <vd@FreeBSD.org>
    992f3396b4
  28. mzumsande force-pushed on Feb 24, 2026
  29. DrahtBot added the label CI failed on Feb 24, 2026
  30. test: use auto_outbound_mode for p2p_private_broadcast.py
    Using the test framework avoids some duplicated code.
    a05272246c
  31. test: add functional test for outbound connection management
    The test covers
    - Extra outbound connections when the tip is stale
    - Regularly scheduled extra block-relay-only connections
    - Management of full outbound connection slots with respect to network-specific peers
    c733d11230
  32. DrahtBot removed the label Needs rebase on Feb 24, 2026
  33. mzumsande force-pushed on Feb 24, 2026
  34. mzumsande commented at 5:28 pm on February 24, 2026: contributor

    Thanks for the review @vasild! Rebased, addressed feedback, and also added a commit that uses the test-framework auto-outbound mode for p2p_private_broadcast.py, removing some duplicate code.

    I just had a rare (?) local intermittent failure of p2p_private_broadcast.py that I need to look into, so I will keep it in draft a little bit longer.

  35. DrahtBot removed the label CI failed on Feb 24, 2026
  36. in test/functional/test_framework/test_framework.py:457 in c733d11230
    452+        def destinations_factory(requested_to_addr, requested_to_port):
    453+            with self.auto_outbound_destinations_lock:
    454+                i = len(self.auto_outbound_destinations)
    455+
    456+                actual_to_addr, actual_to_port, listener = self.auto_outbound_factory(i)
    457+
    


    vasild commented at 8:16 am on February 25, 2026:

    The order in which bitcoind makes connections is a bit undeterministic and also which connection is it going to make is not 100% deterministic either (e.g. is the next connection going to be full-outbound or feeler or net-specific?). #34410 addresses that by not relying only on the connection index but looking up the requested-to address and port in the debug log in order to figure out what the connection is.

    That said, auto_outbound_factory() might benefit from getting more information about the connection, in addition to its index, like the requested address and port:

    0- actual_to_addr, actual_to_port, listener = self.auto_outbound_factory(i)
    1+ actual_to_addr, actual_to_port, listener = self.auto_outbound_factory(i, requested_to_addr, requested_to_port)
    
  37. vasild approved
  38. vasild commented at 8:27 am on February 25, 2026: contributor

    ACK c733d112309321e3671d31cdca92e7aaa9274975

    I just had a rare (?) local intermittent failure of p2p_private_broadcast.py

    Might be related to #34410 or https://github.com/bitcoin/bitcoin/pull/34646

  39. DrahtBot added the label Needs rebase on Mar 2, 2026
  40. DrahtBot commented at 4:52 pm on March 2, 2026: contributor
    🐙 This pull request conflicts with the target branch and needs rebase.

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-03-10 00:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me