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