TeachMeBitcoin

Custom Python P2P Handshake Client

From TeachMeBitcoin, the free encyclopedia ⏱️ 4 min read

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.
☕ Help support TeachMeBitcoin

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:

Ethereum: 0x578417C51783663D8A6A811B3544E1f779D39A85
Bitcoin: bc1q77k9e95rn669kpzyjr8ke9w95zhk7pa5s63qzz
Solana: 4ycT2ayqeMucixj3wS8Ay8Tq9NRDYRPKYbj3UGESyQ4J
Address copied to clipboard!