Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2017-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""6Test how locally submitted transactions are sent to the network when private broadcast is used.7"""9import time10import threading12from test_framework.p2p import (13P2PDataStore,14P2PInterface,15P2P_SERVICES,16start_p2p_listener,17)18from test_framework.messages import (19CAddress,20CInv,21MSG_WTX,22malleate_tx_to_invalid_witness,23msg_inv,24msg_tx,25)26from test_framework.netutil import (27format_addr_port28)29from test_framework.script_util import build_malleated_tx_package30from test_framework.socks5 import (31start_socks5_server,32)33from test_framework.test_framework import (34BitcoinTestFramework,35)36from test_framework.util import (37assert_equal,38assert_greater_than_or_equal,39assert_not_equal,40assert_raises_rpc_error,41tor_port,42)43from test_framework.wallet import (44MiniWallet,45)47P2P_PRIVATE_VERSION = 7001648NUM_PRIVATE_BROADCAST_PER_TX = 349MAX_PRIVATE_BROADCAST_ATTEMPTS = 100052class NoRelayP2PInterface(P2PInterface):53def peer_connect_send_version(self, services):54super().peer_connect_send_version(services)55self.on_connection_send_msg.relay = 058class P2PPrivateBroadcast(BitcoinTestFramework):59def set_test_params(self):60self.disable_autoconnect = False61self.num_nodes = 263def setup_nodes(self):64self.destinations = []66self.destinations_lock = threading.Lock()68self.trigger_no_relay_peer = False69self.no_relay_peer = None71def destinations_factory(requested_to_addr, requested_to_port, proxy_client):72"""73Instruct the SOCKS5 proxy to redirect connections:74* The first automatic outbound connection -> P2PDataStore75* The first private broadcast connection -> nodes[1]76* Anything else -> P2PInterface78proxy_client is the client's socket address as seen by the proxy (host:port),79equal to the node's addrbind for this connection.80"""81conn_type = None82# SOCKS handlers run in separate threads, so each needs its own RPC connection.83rpc = self.nodes[0].create_new_rpc_connection()85def connection_type_found():86nonlocal conn_type87# The proxy has already replied SUCCESS to the SOCKS5 request, so the node88# has finished ConnectNode and registered the peer (or is about to).89# The proxy client address equals the node's addrbind for this connection.90for peer in rpc.getpeerinfo():91if peer.get("addrbind") == proxy_client:92conn_type = peer["connection_type"]93return True94return False96self.wait_until(connection_type_found)98with self.destinations_lock:99i = len(self.destinations)100actual_to_addr = ""101actual_to_port = 0102listener = None103target_name = ""104if conn_type == "private-broadcast" and not any(dest["conn_type"] == "private-broadcast" for dest in self.destinations):105# Instruct the SOCKS5 server to redirect the first private106# broadcast connection from nodes[0] to nodes[1]107actual_to_addr = "127.0.0.1" # nodes[1] listen address108actual_to_port = tor_port(1) # nodes[1] listen port for Tor109target_name = "nodes[1]"110else:111# Create a Python P2P listening node and instruct the SOCKS5 proxy to112# redirect the connection to it. The first outbound connection is used113# later to serve GETDATA, thus make it P2PDataStore().114if conn_type == "outbound-full-relay" and not any(dest["conn_type"] == "outbound-full-relay" for dest in self.destinations):115listener = P2PDataStore()116target_name = "Python P2PDataStore"117elif conn_type == "private-broadcast" and self.trigger_no_relay_peer:118listener = NoRelayP2PInterface()119target_name = "Python NoRelayP2PInterface"120self.trigger_no_relay_peer = False121self.no_relay_peer = listener122else:123listener = P2PInterface()124target_name = "Python P2PInterface"125listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)126listener.peer_connect_send_version(services=P2P_SERVICES)128actual_to_addr, actual_to_port = start_p2p_listener(self.network_thread, listener)130self.log.debug(f"Instructing the SOCKS5 proxy to redirect connection i={i} ({conn_type}) for "131f"{format_addr_port(requested_to_addr, requested_to_port)} to "132f"{format_addr_port(actual_to_addr, actual_to_port)} ({target_name})")134self.destinations.append({135"requested_to": format_addr_port(requested_to_addr, requested_to_port),136"conn_type": conn_type,137"node": listener,138})139assert_equal(len(self.destinations), i + 1)141return {142"actual_to_addr": actual_to_addr,143"actual_to_port": actual_to_port,144}146self.socks5_server = start_socks5_server(destinations_factory)148self.extra_args = [149[150# Needed to be able to add CJDNS addresses to addrman (otherwise they are unroutable).151"-cjdnsreachable",152# Connecting, sending garbage, being disconnected messes up with this test's153# check_broadcasts() which waits for a particular Python node to receive a connection.154"-v2transport=0",155"-test=addrman",156"-privatebroadcast",157f"-proxy={self.socks5_server.conf.addr[0]}:{self.socks5_server.conf.addr[1]}",158# To increase coverage, make it think that the I2P network is reachable so that it159# selects such addresses as well. Pick a proxy address where nobody is listening160# and connection attempts fail quickly.161"-i2psam=127.0.0.1:1",162],163[164"-connect=0",165f"-bind=127.0.0.1:{tor_port(1)}=onion",166],167]168super().setup_nodes()170def setup_network(self):171self.setup_nodes()173def check_broadcasts(self, label, tx, broadcasts_to_expect, skip_destinations):174def wait_and_get_destination(n):175"""Wait for self.destinations[] to have at least n elements and return the 'n'th."""176def get_destinations_len():177with self.destinations_lock:178return len(self.destinations)179self.wait_until(lambda: get_destinations_len() > n)180with self.destinations_lock:181return self.destinations[n]183broadcasts_done = 0184i = skip_destinations - 1185while broadcasts_done < broadcasts_to_expect:186i += 1187self.log.debug(f"{label}: waiting for outbound connection i={i}")188# At this point the connection may not yet have been established (A),189# may be active (B), or may have already been closed (C).190dest = wait_and_get_destination(i)191peer = dest["node"]192if peer is None:193continue # That is the first private broadcast connection, redirected to nodes[1]194peer.wait_until(lambda: peer.message_count["version"] == 1, check_connected=False)195# Now it is either (B) or (C).196if peer.last_message["version"].nServices != 0:197self.log.debug(f"{label}: outbound connection i={i} to {dest['requested_to']} not a private broadcast, ignoring it (maybe feeler or extra block only)")198continue199self.log.debug(f"{label}: outbound connection i={i} to {dest['requested_to']} must be a private broadcast, checking it")200peer.wait_for_disconnect()201# Now it is (C).202assert_equal(peer.message_count, {203"version": 1,204"verack": 1,205"inv": 1,206"tx": 1,207"ping": 1208})209dummy_address = CAddress()210dummy_address.nServices = 0211assert_equal(peer.last_message["version"].nVersion, P2P_PRIVATE_VERSION)212assert_equal(peer.last_message["version"].nServices, 0)213assert_equal(peer.last_message["version"].nTime, 0)214assert_equal(peer.last_message["version"].addrTo, dummy_address)215assert_equal(peer.last_message["version"].addrFrom, dummy_address)216assert_equal(peer.last_message["version"].strSubVer, "/pynode:0.0.1/")217assert_equal(peer.last_message["version"].nStartingHeight, 0)218assert_equal(peer.last_message["version"].relay, 0)219assert_equal(peer.last_message["tx"].tx.txid_hex, tx["txid"])220self.log.info(f"{label}: ok: outbound connection i={i} is private broadcast of txid={tx['txid']}")221broadcasts_done += 1223# Verify the tx we just observed is tracked in getprivatebroadcastinfo.224pbinfo = self.nodes[0].getprivatebroadcastinfo()225pending = [t for t in pbinfo["transactions"] if t["txid"] == tx["txid"] and t["wtxid"] == tx["wtxid"]]226assert_equal(len(pending), 1)227assert_equal(pending[0]["hex"].lower(), tx["hex"].lower())228peers = pending[0]["peers"]229assert_greater_than_or_equal(len(peers), NUM_PRIVATE_BROADCAST_PER_TX)230assert_equal(pending[0]["attempts_remaining"], MAX_PRIVATE_BROADCAST_ATTEMPTS - len(peers))231assert all("address" in p and "sent" in p for p in peers)232assert_greater_than_or_equal(sum(1 for p in peers if "received" in p), broadcasts_to_expect)234def run_test(self):235tx_originator = self.nodes[0]236tx_receiver = self.nodes[1]237far_observer = tx_receiver.add_p2p_connection(P2PInterface())239self.log.info("Test getprivatebroadcastinfo and abortprivatebroadcast fails if the node is running without -privatebroadcast set")240assert_raises_rpc_error(-32601, "Private broadcast is not enabled. Ensure you're running Bitcoin Core with -privatebroadcast=1.",241tx_receiver.getprivatebroadcastinfo)242assert_raises_rpc_error(-32601, "Private broadcast is not enabled. Ensure you're running Bitcoin Core with -privatebroadcast=1.",243tx_receiver.abortprivatebroadcast, "00" * 32)245self.fill_node_addrman(node_index=0, address_types_to_add=[CAddress.NET_IPV4, CAddress.NET_IPV6, CAddress.NET_TORV3, CAddress.NET_I2P, CAddress.NET_CJDNS])247wallet = MiniWallet(tx_originator)249txs = wallet.create_self_transfer_chain(chain_length=3)250self.log.info(f"Created txid={txs[0]['txid']}: for basic test")
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.