Custom Python P2P Handshake Client
Building a Custom Python P2P Handshake Client from First Principles
Writing code that communicates directly with a Bitcoin node on the socket layer provides the ultimate insight into the wire protocol. By avoiding external library dependencies, we can serialize the exact byte fields required to execute the bi-directional P2P handshake.
This guide provides a production-grade, zero-dependency Python client that establishes a TCP socket, constructs a version packet, parses the peer's response, and acknowledges the handshake using verack.
💻 1. The Raw Python Client Source Code
Below is the complete, documented Python script using only standard library modules (socket, struct, time, random, hashlib).
import socket
import struct
import time
import random
import hashlib
class BitcoinP2PClient:
def __init__(self, target_ip, target_port=8333, magic_bytes=b"\xf9\xbe\xb4\xd9"):
self.target_ip = target_ip
self.target_port = target_port
self.magic_bytes = magic_bytes
self.socket = None
self.session_nonce = random.randint(0, 2**64 - 1)
def double_sha256(self, payload: bytes) -> bytes:
"""Returns the double-SHA256 hash of a payload."""
return hashlib.sha256(hashlib.sha256(payload).digest()).digest()
def serialize_header(self, command: str, payload: bytes) -> bytes:
"""Packs a standard 24-byte Bitcoin message header."""
# Left-align command and null-pad to 12 bytes
command_padded = command.encode("ascii").ljust(12, b"\x00")
payload_size = len(payload)
checksum = self.double_sha256(payload)[:4]
# Header layout: <4s (magic) + 12s (command) + I (size, LE) + 4s (checksum)
return struct.pack("<4s12sI4s", self.magic_bytes, command_padded, payload_size, checksum)
def serialize_net_addr(self, ip: str, port: int) -> bytes:
"""Serializes standard 26-byte net_addr structure (IPv4 mapped inside IPv6)."""
services = 1 # NODE_NETWORK capability
# Parse IPv4 and map into IPv6 bytes
ip_parts = [int(x) for x in ip.split(".")]
ip_mapped = b"\x00" * 10 + b"\xff\xff" + struct.pack("BBBB", *ip_parts)
# Port must be serialized in big-endian (network byte order)
port_packed = struct.pack(">H", port)
return struct.pack("<Q", services) + ip_mapped + port_packed
def serialize_version_payload(self) -> bytes:
"""Serializes the complete version payload."""
protocol_version = 70015
services = 1 # NODE_NETWORK
timestamp = int(time.time())
# Serialize Recv & From Addresses (26 bytes each)
addr_recv = self.serialize_net_addr(self.target_ip, self.target_port)
addr_from = self.serialize_net_addr("127.0.0.1", 8333)
user_agent_str = b"/TeachMeBitcoin:1.0/"
# CompactSize prefix for user-agent length
user_agent_len = struct.pack("B", len(user_agent_str))
start_height = 840000
relay = b"\x01" # Request unconfirmed transaction relay
# Construct the payload byte array
payload = struct.pack(
"<iQq26s26sQ",
protocol_version,
services,
timestamp,
addr_recv,
addr_from,
self.session_nonce
)
payload += user_agent_len + user_agent_str + struct.pack("<i", start_height) + relay
return payload
def connect_and_handshake(self):
"""Connects to the socket, sends version, and processes the handshake state."""
print(f"[*] Connecting to Bitcoin node at {self.target_ip}:{self.target_port}...")
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(10.0) # Establish 10-second timeout limit
try:
self.socket.connect((self.target_ip, self.target_port))
print("[✔] TCP connection established.")
# --- STAGE 1: SEND VERSION ---
version_payload = self.serialize_version_payload()
version_header = self.serialize_header("version", version_payload)
print("[*] Transmitting 'version' packet...")
self.socket.sendall(version_header + version_payload)
# --- STAGE 2: PROCESS RESPONSES ---
version_received = False
verack_received = False
while not (version_received and verack_received):
# Read standard 24-byte header
header_bytes = self.socket.recv(24)
if len(header_bytes) < 24:
print("[!] Peer truncated connection stream.")
break
magic, cmd_raw, size, checksum = struct.unpack("<4s12sI4s", header_bytes)
command = cmd_raw.split(b"\x00")[0].decode("ascii", errors="replace")
print(f"[+] Received message header: '{command}' ({size} bytes payload)")
# Fetch payload data
payload = b""
while len(payload) < size:
chunk = self.socket.recv(size - len(payload))
if not chunk:
break
payload += chunk
# Validate cryptographic checksum
calculated_checksum = self.double_sha256(payload)[:4]
if calculated_checksum != checksum:
print("[!] Checksum error! Message corrupt. Disconnecting.")
break
if command == "version":
version_received = True
# Unpack protocol version from payload (first 4 bytes)
peer_version = struct.unpack("<i", payload[:4])[0]
print(f" ├─ Peer Protocol Version: {peer_version}")
# Send verack back to confirm
verack_header = self.serialize_header("verack", b"")
print(" └─ Transmitting 'verack' response...")
self.socket.sendall(verack_header)
elif command == "verack":
verack_received = True
print(" ├─ Peer acknowledged our handshake.")
if version_received and verack_received:
print("\n[✔] Handshake ESTABLISHED successfully!")
except Exception as e:
print(f"[!] Network error during handshake: {e}")
finally:
if self.socket:
self.socket.close()
print("[*] Socket closed.")
if __name__ == "__main__":
# Test connection to a popular public mainnet peer (or your local node)
client = BitcoinP2PClient(target_ip="18.197.135.213", target_port=8333)
client.connect_and_handshake()
🛠️ 2. Executing the Script
To run the client, ensure you have python3 installed on your developer machine:
python3 python_p2p_handshake.py
Expected Output Logs:
[*] Connecting to Bitcoin node at 18.197.135.213:8333...
[✔] TCP connection established.
[*] Transmitting 'version' packet...
[+] Received message header: 'version' (102 bytes payload)
├─ Peer Protocol Version: 70016
└─ Transmitting 'verack' response...
[+] Received message header: 'verack' (0 bytes payload)
├─ Peer acknowledged our handshake.
[✔] Handshake ESTABLISHED successfully!
[*] Socket closed.
TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation: