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) 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"""Test torcontrol functionality with a mock Tor control server."""6from contextlib import contextmanager7import socket8import threading9from test_framework.test_framework import BitcoinTestFramework10from test_framework.util import (11assert_equal,12ensure_for,13p2p_port,14)17class MockTorControlServer:18def __init__(self, port, manual_mode=False):19self.port = port20self.sock = None21self.conn = None22self.running = False23self.thread = None24self.received_commands = []25self.manual_mode = manual_mode26self.conn_ready = threading.Event()28def start(self):29self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)30self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)31self.sock.settimeout(1.0)32self.sock.bind(('127.0.0.1', self.port))33self.sock.listen(1)34self.running = True35self.thread = threading.Thread(target=self._serve)36self.thread.daemon = True37self.thread.start()39def stop(self):40self.running = False41if self.conn:42self.conn.close()43if self.sock:44self.sock.close()45if self.thread:46self.thread.join(timeout=5)48def _serve(self):49while self.running:50try:51self.conn, _ = self.sock.accept()52self.conn.settimeout(1.0)53self.conn_ready.set()54self._handle_connection(self.conn)55except socket.timeout:56continue57except OSError:58break60def _handle_connection(self, conn):61try:62buf = b""63while self.running:64try:65data = conn.recv(1024)66if not data:67break68buf += data69while b"\r\n" in buf:70line, buf = buf.split(b"\r\n", 1)71command = line.decode('utf-8').strip()72if command:73self.received_commands.append(command)74if not self.manual_mode:75response = self._get_response(command)76conn.sendall(response.encode('utf-8'))77except socket.timeout:78continue79finally:80conn.close()82def send_raw(self, data):83if self.conn:84self.conn.sendall(data.encode('utf-8'))86def _get_response(self, command):87if command == "PROTOCOLINFO 1":88return (89"250-PROTOCOLINFO 1\r\n"90"250-AUTH METHODS=NULL\r\n"91"250-VERSION Tor=\"0.1.2.3\"\r\n"92"250 OK\r\n"93)94elif command == "AUTHENTICATE":95return "250 OK\r\n"96elif command.startswith("ADD_ONION"):97return (98"250-ServiceID=testserviceid1234567890123456789012345678901234567890123456\r\n"99"250 OK\r\n"100)101elif command.startswith("GETINFO"):102return "250-net/listeners/socks=\"127.0.0.1:9050\"\r\n250 OK\r\n"103else:104return "510 Unrecognized command\r\n"107class TorControlTest(BitcoinTestFramework):108def set_test_params(self):109self.num_nodes = 1111def next_port(self):112self._port_counter = getattr(self, '_port_counter', 0) + 1113return p2p_port(self.num_nodes + self._port_counter)115def restart_with_mock(self, mock_tor):116mock_tor.start()117self.restart_node(0, extra_args=[118f"-torcontrol=127.0.0.1:{mock_tor.port}",119"-listenonion=1",120"-debug=tor",121])123# Wait for connection and PROTOCOLINFO command124mock_tor.conn_ready.wait(timeout=10)125self.wait_until(lambda: len(mock_tor.received_commands) >= 1, timeout=10)126assert_equal(mock_tor.received_commands[0], "PROTOCOLINFO 1")128@contextmanager129def expect_disconnect(self, expect, mock_tor):130initial_len = len(mock_tor.received_commands)131yield133if expect:134# No reconnect before the initial reconnect timeout of 1s has passed135ensure_for(duration=0.5, f=lambda: len(mock_tor.received_commands) == initial_len)136# Expect to receive a PROTOCOLINFO 1 on reconnect, bumping the received137# commands length.138self.wait_until(lambda: len(mock_tor.received_commands) == initial_len + 1)139assert_equal(mock_tor.received_commands[initial_len], "PROTOCOLINFO 1")140else:141# No disconnect, so no reconnect message142ensure_for(duration=2, f=lambda: len(mock_tor.received_commands) == initial_len)144def test_basic(self):145self.log.info("Test Tor control basic functionality")147mock_tor = MockTorControlServer(self.next_port())148self.restart_with_mock(mock_tor)150# Waiting for Tor control commands151self.wait_until(lambda: len(mock_tor.received_commands) >= 4, timeout=10)153# Verify expected protocol sequence154assert_equal(mock_tor.received_commands[0], "PROTOCOLINFO 1")155assert_equal(mock_tor.received_commands[1], "AUTHENTICATE")156assert_equal(mock_tor.received_commands[2], "GETINFO net/listeners/socks")157assert mock_tor.received_commands[3].startswith("ADD_ONION ")158assert "PoWDefensesEnabled=1" in mock_tor.received_commands[3]160# Clean up161mock_tor.stop()163def test_partial_data(self):164self.log.info("Test that partial Tor control responses are buffered until complete")166mock_tor = MockTorControlServer(self.next_port(), manual_mode=True)167self.restart_with_mock(mock_tor)169# Send partial response (no \r\n on last line)170mock_tor.send_raw(171"250-PROTOCOLINFO 1\r\n"172"250-AUTH METHODS=NULL\r\n"173"250 OK"174)176# Verify AUTHENTICATE is not sent177ensure_for(duration=2, f=lambda: len(mock_tor.received_commands) == 1)179# Complete the response180mock_tor.send_raw("\r\n")182# Should now process the complete response and send AUTHENTICATE183self.wait_until(lambda: len(mock_tor.received_commands) >= 2, timeout=5)184assert_equal(mock_tor.received_commands[1], "AUTHENTICATE")186# Clean up187mock_tor.stop()189def test_pow_fallback(self):190self.log.info("Test that ADD_ONION retries without PoW on 512 error")192class NoPowServer(MockTorControlServer):193def _get_response(self, command):194if command.startswith("ADD_ONION"):195if "PoWDefensesEnabled=1" in command:196return "512 Unrecognized option\r\n"197else:198return (199"250-ServiceID=testserviceid1234567890123456789012345678901234567890123456\r\n"200"250 OK\r\n"201)202return super()._get_response(command)204mock_tor = NoPowServer(self.next_port())205self.restart_with_mock(mock_tor)207# Expect: PROTOCOLINFO, AUTHENTICATE, GETINFO, ADD_ONION (with PoW), ADD_ONION (without PoW)208self.wait_until(lambda: len(mock_tor.received_commands) >= 5, timeout=10)210# First ADD_ONION should have PoW enabled211assert mock_tor.received_commands[3].startswith("ADD_ONION ")212assert "PoWDefensesEnabled=1" in mock_tor.received_commands[3]214# Retry should be ADD_ONION without PoW215assert mock_tor.received_commands[4].startswith("ADD_ONION ")216assert "PoWDefensesEnabled=1" not in mock_tor.received_commands[4]218# Clean up219mock_tor.stop()221def test_oversized_line(self):222mock_tor = MockTorControlServer(self.next_port(), manual_mode=True)223self.restart_with_mock(mock_tor)225MAX_LINE_LENGTH = 100000227self.log.info("Test that Tor control does not disconnect with a MAX_LINE_LENGTH line.")228with self.expect_disconnect(False, mock_tor):229msg = "250-" + ("A" * (MAX_LINE_LENGTH - 5)) + "\r"230assert_equal(len(msg), MAX_LINE_LENGTH)231# The \n is not counted in line length.232mock_tor.send_raw(msg + "\n")234self.log.info("Test that Tor control disconnects with a MAX_LINE_LENGTH + 1 line")235with self.expect_disconnect(True, mock_tor):236msg = "250-" + ("A" * (MAX_LINE_LENGTH - 4)) + "\r"237assert_equal(len(msg), MAX_LINE_LENGTH + 1)238mock_tor.send_raw(msg + "\n")240mock_tor.stop()242def test_overmany_lines(self):243mock_tor = MockTorControlServer(self.next_port(), manual_mode=True)244self.restart_with_mock(mock_tor)246MAX_LINE_COUNT = 1000248self.log.info("Test that Tor control does not disconnect on receiving MAX_LINE_COUNT lines.")249with self.expect_disconnect(False, mock_tor):250for _ in range(MAX_LINE_COUNT - 1):
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.