p2p_private_broadcast_retry_v1.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2026-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """
   6  Ensure that when v2 private broadcast connection to IPv4 fails the v1 retry
   7  will also be made through the Tor proxy.
   8  
   9  The test does:
  10  * Add a bunch of IPv4 addresses to the node's addrman (they will be added without P2P_V2 flag).
  11  * Get them to report P2P_V2 in their service flags and connect to each one, so that the flags
  12    in addrman are updated to contain P2P_V2.
  13  * Get one successful connection to a Tor peer (.onion) so that bitcoind assumes the configured
  14    Tor proxy works and is indeed a proxy to the Tor network. This will make it open private
  15    broadcast connections also to IPv4 addresses via that proxy.
  16  * Start some private broadcast connections.
  17  * Remember the destination IPv4 address of the first connection and get it to fail the v2
  18    transport.
  19  * Wait for a subsequent connection also through the Tor proxy to the same IPv4 and expect
  20    it to be v1, i.e. the v2->v1 downgrade retry.
  21  """
  22  
  23  from test_framework.netutil import (
  24      format_addr_port
  25  )
  26  from test_framework.p2p import (
  27      P2PConnection,
  28      P2PInterface,
  29      P2P_SERVICES,
  30      start_p2p_listener,
  31  )
  32  from test_framework.messages import (
  33      CAddress,
  34      NODE_P2P_V2,
  35  )
  36  from test_framework.socks5 import (
  37      start_socks5_server,
  38  )
  39  from test_framework.test_framework import (
  40      BitcoinTestFramework,
  41  )
  42  from test_framework.v2_p2p import (
  43      EncryptedP2PState,
  44  )
  45  from test_framework.wallet import (
  46      MiniWallet,
  47  )
  48  
  49  class P2PDetermineV2or1AndClose(P2PConnection):
  50      def __init__(self, on_v2or1_determined):
  51          super().__init__()
  52          self.on_v2or1_determined = on_v2or1_determined
  53  
  54      # https://docs.python.org/3/library/asyncio-protocol.html#asyncio.Protocol.data_received
  55      def data_received(self, data):
  56          self.recvbuf += data
  57          if len(self.recvbuf) >= 4:
  58              self.on_v2or1_determined(1 if self.recvbuf[0:4] == self.magic_bytes else 2)
  59              self.peer_disconnect()
  60  
  61      def on_open(self):
  62          pass
  63  
  64      def on_close(self):
  65          pass
  66  
  67  class P2PPrivateBroadcastRetryV1(BitcoinTestFramework):
  68      def set_test_params(self):
  69          self.disable_autoconnect = False
  70          self.num_nodes = 1
  71  
  72      def ipv4_via_tor_proxy_conn_versions_append(self, v2or1):
  73          """
  74          Add to the transport versions (v2 or v1) tried towards the first IPv4 which
  75          nodes[0] tries to connect to via the Tor proxy.
  76          """
  77          self.ipv4_via_tor_proxy_conn_versions.append(v2or1)
  78  
  79      def setup_nodes(self):
  80          def destinations_factory_all_proxy(requested_to_addr, requested_to_port):
  81              """
  82              Instruct the SOCKS5 proxy to redirect all connections to newly created P2PInterface
  83              objects that claim support for P2P_V2.
  84              """
  85              listener = P2PInterface()
  86              listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)
  87              listener.peer_connect_send_version(services=P2P_SERVICES | NODE_P2P_V2)
  88  
  89              actual_to_addr, actual_to_port = start_p2p_listener(self.network_thread, listener)
  90  
  91              self.log.debug("Instructing the common proxy to redirect connection for "
  92                             f"{format_addr_port(requested_to_addr, requested_to_port)} to "
  93                             f"{format_addr_port(actual_to_addr, actual_to_port)} (Python {type(listener).__name__})")
  94  
  95              return {
  96                  "actual_to_addr": actual_to_addr,
  97                  "actual_to_port": actual_to_port,
  98              }
  99  
 100          self.all_proxy = start_socks5_server(destinations_factory_all_proxy)
 101  
 102          self.ipv4_via_tor_proxy_addr_port = None # Remember the first IPv4 address connected to via the Tor proxy.
 103          self.ipv4_via_tor_proxy_conn_versions = [] # Transport versions tried on that address.
 104  
 105          def destinations_factory_tor_proxy(requested_to_addr, requested_to_port):
 106              """
 107              Instruct the SOCKS5 proxy to redirect all connections to newly created P2PInterface,
 108              except the first connection to an IPv4 address and all subsequent connections to that
 109              address which are redirected to P2PDetermineV2or1AndClose.
 110              """
 111              requested_to = format_addr_port(requested_to_addr, requested_to_port)
 112  
 113              if not requested_to_addr.endswith(".onion") and self.ipv4_via_tor_proxy_addr_port is None: # First IPv4
 114                  self.ipv4_via_tor_proxy_addr_port = requested_to
 115  
 116              if self.ipv4_via_tor_proxy_addr_port == requested_to:
 117                  # This is either the first (v2) or the second (the expected v1 retry) connection to requested_to.
 118                  listener = P2PDetermineV2or1AndClose(self.ipv4_via_tor_proxy_conn_versions_append)
 119                  listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)
 120              else:
 121                  listener = P2PInterface()
 122                  listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)
 123                  listener.peer_connect_send_version(services=P2P_SERVICES | NODE_P2P_V2)
 124                  if not requested_to_addr.endswith(".onion"):
 125                      listener.v2_state = EncryptedP2PState(initiating=False, net=self.chain)
 126  
 127              actual_to_addr, actual_to_port = start_p2p_listener(self.network_thread, listener)
 128  
 129              self.log.debug(f"Instructing the Tor proxy to redirect connection for {requested_to} to "
 130                             f"{format_addr_port(actual_to_addr, actual_to_port)} (Python {type(listener).__name__})")
 131  
 132              return {
 133                  "actual_to_addr": actual_to_addr,
 134                  "actual_to_port": actual_to_port,
 135              }
 136  
 137          self.tor_proxy = start_socks5_server(destinations_factory_tor_proxy)
 138  
 139          self.extra_args = [
 140              [
 141                  "-privatebroadcast=1",
 142                  f"-proxy={self.all_proxy.conf.addr[0]}:{self.all_proxy.conf.addr[1]}",
 143                  f"-onion={self.tor_proxy.conf.addr[0]}:{self.tor_proxy.conf.addr[1]}",
 144                  "-test=addrman",
 145                  "-v2transport=0",
 146              ],
 147          ]
 148  
 149          super().setup_nodes()
 150  
 151      def setup_network(self):
 152          self.setup_nodes()
 153  
 154      def run_test(self):
 155          node0 = self.nodes[0]
 156  
 157          self.log.info("Filling node0's addrman with addresses")
 158          self.fill_node_addrman(node_index=0, address_types_to_add=[CAddress.NET_IPV4])
 159  
 160          self.log.info("Opening manual connections to all IPv4 addresses to add P2P_V2 flag to addrman entries")
 161          for a in node0.getnodeaddresses(count=0, network="ipv4"):
 162              node0.addnode(node=format_addr_port(a["address"], a["port"]), command="onetry", v2transport=False)
 163  
 164          self.log.info("Waiting for all IPv4 addresses to get P2P_V2 as a result of peers advertising support")
 165          self.wait_until(lambda: all(a["services"] & NODE_P2P_V2 != 0 for a in node0.getnodeaddresses(count=0, network="ipv4")))
 166  
 167          # The destinations behind the -proxy= don't actually support v2. When bitcoind runs with -v2transport=1
 168          # and tries v2 on them they would print benign "magic byte mismatch" warnings.
 169          # Disable those since none of them are needed anymore.
 170          self.all_proxy.conf.destinations_factory = None
 171  
 172          self.restart_node(0, extra_args=self.extra_args[0] + ["-v2transport=1"])
 173  
 174          self.log.info("Opening a connection to a Tor addresses, so bitcoind considers -onion= is a real Tor proxy")
 175          node0.addnode(node="testonlyad777777777777777777777777777777777777777775b6qd.onion:1234", command="onetry", v2transport=False)
 176  
 177          self.log.info("Waiting for at least one Tor connection")
 178          self.wait_until(lambda: any(p["network"] == "onion" for p in node0.getpeerinfo()))
 179  
 180          self.log.info("Starting private broadcast connections")
 181          wallet = MiniWallet(node0)
 182          tx = wallet.create_self_transfer()
 183          node0.sendrawtransaction(hexstring=tx["hex"])
 184  
 185          self.log.info("Tor proxy: waiting for connection to an IPv4 address")
 186          self.wait_until(lambda: self.ipv4_via_tor_proxy_addr_port is not None)
 187          self.log.info(f"Tor proxy: got {self.ipv4_via_tor_proxy_addr_port}, waiting for v2")
 188          self.wait_until(lambda: 2 in self.ipv4_via_tor_proxy_conn_versions)
 189          self.log.info(f"Tor proxy: got {self.ipv4_via_tor_proxy_addr_port} v2, waiting for v1")
 190          self.wait_until(lambda: 1 in self.ipv4_via_tor_proxy_conn_versions)
 191          self.log.info(f"Tor proxy: got {self.ipv4_via_tor_proxy_addr_port} v2, v1")
 192  
 193          self.stop_node(0)
 194          self.all_proxy.stop()
 195          self.tor_proxy.stop()
 196  
 197  
 198  if __name__ == "__main__":
 199      P2PPrivateBroadcastRetryV1(__file__).main()
 200