Blockchain technology is built around consensus algorithms which allow distributed nodes to share a common ledger. A fundamental dependency of these algorithms is a common network protocol to enable communication between participating nodes. Today, let's write a Python program from scratch to interact with a real Bitcoin node.
This post will assume you're familiar with the fundamentals of blockchain technology. If you aren't, I would recommend checking out the Bitcoin White Paper by Satoshi Nakamoto.
Bitcoin nodes communicate with each other using the TCP protocol. Nodes will typically listen on port number 8333. For a detailed description of the bitcoin network protocol check out this resource.
Today, we are going to write a Python program to connect to a Bitcoin node and fetch the details of a specific transaction. Here is a diagram of the message flow that will be developed.
Before we start coding our program, we must make one point clear. Interacting with a Bitcoin node using raw TCP sockets is reinventing the wheel. This has already been done by python packages such as python-bitcoinlib.
If you want to write sophisticated applications you should definitely use the correct tool for the job. With that said though, programming with TCP sockets is a great way to improve your low level understanding of a network protocol.
To begin, let's import the dependencies our program will require.
#!/usr/bin/env python
# Filename: bitcoin-network-tutorial.py
# Command to run the program: python bitcoin-network-tutorial.py
# Import dependencies
import socket
import time
import random
import struct
import hashlib
import binascii
Let's now define the methods required for constructing the "version" request message.
# Binary encode the sub-version
def create_sub_version():
sub_version = "/Satoshi:0.7.2/"
return b'\x0F' + sub_version.encode()
# Binary encode the network addresses
def create_network_address(ip_address, port):
network_address = struct.pack('>8s16sH', b'\x01',
bytearray.fromhex("00000000000000000000ffff") + socket.inet_aton(ip_address), port)
return(network_address)
# Create the TCP request object
def create_message(magic, command, payload):
checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[0:4]
return(struct.pack('L12sL4s', magic, command.encode(), len(payload), checksum) + payload)
# Create the "version" request payload
def create_payload_version(peer_ip_address):
version = 60002
services = 1
timestamp = int(time.time())
addr_local = create_network_address("127.0.0.1", 8333)
addr_peer = create_network_address(peer_ip_address, 8333)
nonce = random.getrandbits(64)
start_height = 0
payload = struct.pack('<LQQ26s26sQ16sL', version, services, timestamp, addr_peer,
addr_local, nonce, create_sub_version(), start_height)
return(payload)
The struct module is used for packing binary data. The hashlib module is used for generating message checksums. For a full understanding of the code, you'll need to cross reference the data encoding with the protocol documentation.
Next, let's add a method for creating the "verack" request message. The verack command name is derived from "version acknowledge".
# Create the "verack" request message
def create_message_verack():
return bytearray.fromhex("f9beb4d976657261636b000000000000000000005df6e0e2")
With the mandatory messages out of the way, we may now create our "getdata" method for retrieving the details of a specific transaction.
# Create the "getdata" request payload
def create_payload_getdata(tx_id):
count = 1
type = 1
hash = bytearray.fromhex(tx_id)
payload = struct.pack('<bb32s', count, type, hash)
return(payload)
Please note that not all nodes will be able to return arbitrary transaction data; some will prune their history to save disk space.
We'll also create a method for printing TCP data to the terminal.
# Print request/response data
def print_response(command, request_data, response_data):
print("")
print("Command: " + command)
print("Request:")
print(binascii.hexlify(request_data))
print("Response:")
print(binascii.hexlify(response_data))
We may now add our main method which will connect to a bitcoin node and execute the desired message flow.
if __name__ == '__main__':
# Set constants
magic_value = 0xd9b4bef9
tx_id = "fc57704eff327aecfadb2cf3774edc919ba69aba624b836461ce2be9c00a0c20"
peer_ip_address = '104.199.184.15'
peer_tcp_port = 8333
buffer_size = 1024
# Create Request Objects
version_payload = create_payload_version(peer_ip_address)
version_message = create_message(magic_value, 'version', version_payload)
verack_message = create_message_verack()
getdata_payload = create_payload_getdata(tx_id)
getdata_message = create_message(magic_value, 'getdata', getdata_payload)
# Establish TCP Connection
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((peer_ip_address, peer_tcp_port))
# Send message "version"
s.send(version_message)
response_data = s.recv(buffer_size)
print_response("version", version_message, response_data)
# Send message "verack"
s.send(verack_message)
response_data = s.recv(buffer_size)
print_response("verack", verack_message, response_data)
# Send message "getdata"
s.send(getdata_message)
response_data = s.recv(buffer_size)
print_response("getdata", getdata_message, response_data)
# Close the TCP connection
s.close()
We found the IP address of the node using Bitnodes. Details of the transaction we elected to query can be found on a block explorer.
Execute the program on a terminal with the command python bitcoin-network-tutorial.py
. A sample output is provided below.
Command: version
Request:
b'f9beb4d976657273696f6e000000000064000000f4de76b762ea00000100000000000000c8c6ae5d00000000010000000000000000000000000000000000ffff68c7b80f208d010000000000000000000000000000000000ffff7f000001208d0f2f736a397699b60f2f5361746f7368693a302e372e322f00000000'
Response:
b'f9beb4d976657273696f6e000000000066000000fe4aee167f1101000d04000000000000c2c6ae5d00000000010000000000000000000000000000000000ffff68c7b80f208d0d040000000000000000000000000000000000000000000000000b63185e17ebcdb3102f5361746f7368693a302e31382e302fbc29090001'
Command: verack
Request:
b'f9beb4d976657261636b000000000000000000005df6e0e2'
Response:
b'f9beb4d976657261636b000000000000000000005df6e0e2'
Command: getdata
Request:
b'f9beb4d9676574646174610000000000220000007b00a9b50101fc57704eff327aecfadb2cf3774edc919ba69aba624b836461ce2be9c00a0c20'
Response:
b'f9beb4d9616c65727400000000000000c0000000d2f50d9ef9beb4d9616c65727400000000000000a80000001bf9aaea60010000000000000000000000ffffff7f00000000ffffff7ffeffff7f01ffffff7f00000000ffffff7f00ffffff7f002f555247454e543a20416c657274206b657920636f6d70726f6d697365642c2075706772616465207265717569726564004630440220653febd6410f470f6bae11cad19c48413becb1ac2c17f908fd0fd53bdc3abd5202206d0e9c96fe88d4a0f01ed9dedae2b6f9e00da94cad0fecaae66ecf689bf71b50'
That concludes the tutorial! Stay tuned for more.
ULTRA CONFIG GENERATOR
Have you heard of Ultra Config Generator? If you haven't, I highly recommend you check it out.
We designed the product to allow network engineers to generate and automate network configuration in a highly flexible, efficient and elegant manner. Our customers love the application and I hope that you will too.
Take care until next time!
Ultra Config
Top comments (4)
i'm getting an empty response for every "s.send" . Is it because txn is outdated?
Hello, I just reran the script in its entirety and it still works for me.
Did you copy everything as it appeared in the blog?
Best Regards,
Alec
It's almost 2022 - I tried running it now, but it doesn't work. Did the protocol change?
Hi, for s.send(verack_message), I get timeout. The version message works, but every host I connect to does not respond to the verack message. Is anything changed since last updated?
Thank you