Skip to content

Instantly share code, notes, and snippets.

@niteshbalusu11
Created October 28, 2025 22:00
Show Gist options
  • Select an option

  • Save niteshbalusu11/582f450104a0493ed78a0d0edb54a928 to your computer and use it in GitHub Desktop.

Select an option

Save niteshbalusu11/582f450104a0493ed78a0d0edb54a928 to your computer and use it in GitHub Desktop.
Fetch node versions from dns seeds
cat dns-seed.py
#!/usr/bin/env python3
"""
Bitcoin DNS Seed Version Checker
Outputs nice tables for screenshots
"""
import socket
import struct
import time
import hashlib
from collections import Counter
import sys
import re
# Bitcoin Protocol Constants
MAGIC_MAINNET = 0xD9B4BEF9
MY_VERSION = 70015
def sha256(s):
return hashlib.sha256(s).digest()
def create_message(command, payload):
"""Create Bitcoin protocol message"""
magic = struct.pack('<I', MAGIC_MAINNET)
command_bytes = command.encode('ascii').ljust(12, b'\x00')
length = struct.pack('<I', len(payload))
checksum = sha256(sha256(payload))[:4]
return magic + command_bytes + length + checksum + payload
def create_version_message(dest_ip, dest_port):
"""Create Bitcoin version message"""
version = struct.pack('<i', MY_VERSION)
services = struct.pack('<Q', 0)
timestamp = struct.pack('<q', int(time.time()))
# addr_recv
addr_recv_services = struct.pack('<Q', 1)
addr_recv_ip = b'\x00' * 10 + b'\xff\xff' + socket.inet_aton(dest_ip)
addr_recv_port = struct.pack('>H', dest_port)
addr_recv = addr_recv_services + addr_recv_ip + addr_recv_port
# addr_from
addr_from_services = struct.pack('<Q', 0)
addr_from_ip = b'\x00' * 10 + b'\xff\xff' + b'\x00' * 4
addr_from_port = struct.pack('>H', 0)
addr_from = addr_from_services + addr_from_ip + addr_from_port
nonce = struct.pack('<Q', 0)
user_agent_bytes = b'\x00'
start_height = struct.pack('<i', 0)
relay = b'\x01'
payload = (version + services + timestamp + addr_recv + addr_from +
nonce + user_agent_bytes + start_height + relay)
return create_message('version', payload)
def read_varint(data, offset):
"""Read Bitcoin variable-length integer"""
if offset >= len(data):
return 0, offset
first = data[offset]
if first < 0xfd:
return first, offset + 1
elif first == 0xfd:
if offset + 2 >= len(data):
return 0, offset + 1
return struct.unpack('<H', data[offset+1:offset+3])[0], offset + 3
elif first == 0xfe:
if offset + 4 >= len(data):
return 0, offset + 1
return struct.unpack('<I', data[offset+1:offset+5])[0], offset + 5
else:
if offset + 8 >= len(data):
return 0, offset + 1
return struct.unpack('<Q', data[offset+1:offset+9])[0], offset + 9
def parse_version_message(payload):
"""Parse Bitcoin version message and extract user agent"""
try:
if len(payload) < 80:
return None
offset = 0
# int32: version (protocol version)
protocol_version = struct.unpack('<i', payload[offset:offset+4])[0]
offset += 4
# uint64: services
offset += 8
# int64: timestamp
offset += 8
# net_addr: addr_recv (26 bytes)
offset += 26
# net_addr: addr_from (26 bytes)
offset += 26
# uint64: nonce
offset += 8
# var_str: user_agent
user_agent_length, offset = read_varint(payload, offset)
if offset + user_agent_length > len(payload):
return None
user_agent = payload[offset:offset+user_agent_length].decode('utf-8', errors='ignore')
# Extract version from user agent
# Common formats: "/Satoshi:27.0.0/", "/Satoshi:26.1.0/", "/Bitcoin Core:25.0.0/"
version_match = re.search(r'/(?:Satoshi|Bitcoin Core):(\d+\.\d+\.\d+)/', user_agent)
if version_match:
return version_match.group(1)
# Also try without slashes
version_match = re.search(r'(\d+\.\d+\.\d+)', user_agent)
if version_match:
return version_match.group(1)
return user_agent if user_agent else None
except Exception as e:
return None
def recv_all(sock, length):
"""Receive exact amount of data"""
data = b''
while len(data) < length:
chunk = sock.recv(length - len(data))
if not chunk:
return None
data += chunk
return data
def get_node_version(ip, port=8333, timeout=4):
"""Connect to Bitcoin node and get its version from user agent"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((ip, port))
# Send version message
version_msg = create_version_message(ip, port)
sock.send(version_msg)
# Receive response header
header = recv_all(sock, 24)
if not header or len(header) < 24:
sock.close()
return None
# Parse header
magic, command, length, checksum = struct.unpack('<I12sI4s', header)
if magic != MAGIC_MAINNET:
sock.close()
return None
# Get payload
if length > 10000: # Sanity check
sock.close()
return None
payload = recv_all(sock, length)
if not payload:
sock.close()
return None
# Parse version message to get user agent
version = parse_version_message(payload)
sock.close()
return version
except Exception as e:
return None
def query_dns_seed(seed):
"""Get all IPs from DNS seed"""
try:
result = socket.getaddrinfo(seed, None, socket.AF_INET)
ips = list(set([r[4][0] for r in result]))
return ips
except Exception as e:
print(f" DNS Error: {e}", file=sys.stderr)
return []
def test_dns_seed(seed, max_nodes=100):
"""Test a DNS seed and return version distribution"""
print(f"\nTesting: {seed}", file=sys.stderr)
print("-" * 70, file=sys.stderr)
# Get IPs
ips = query_dns_seed(seed)
total_ips = len(ips)
print(f" Found {total_ips} unique IPs", file=sys.stderr)
if total_ips == 0:
return {}
# Limit sample size
ips = ips[:max_nodes]
print(f" Testing {len(ips)} nodes...", file=sys.stderr)
# Test each node
versions = []
for i, ip in enumerate(ips):
version = get_node_version(ip)
if version:
versions.append(version)
# Progress indicator
if (i + 1) % 10 == 0:
print(f" Progress: {i + 1}/{len(ips)} ({len(versions)} successful)", file=sys.stderr)
# Count versions
version_counts = Counter(versions)
print(f" Results: {len(versions)} nodes responded", file=sys.stderr)
for ver, count in version_counts.most_common(10):
print(f" {ver}: {count}", file=sys.stderr)
return dict(version_counts)
def version_key(v):
"""Sort key for version strings"""
try:
return [int(p) for p in v.split('.')]
except:
return [0, 0, 0]
def export_csv(results, filename):
"""Export results to CSV"""
all_versions = set()
for versions in results.values():
all_versions.update(versions.keys())
version_list = sorted(all_versions, key=version_key, reverse=True)
with open(filename, 'w') as f:
# Header
f.write("Seed," + ",".join(version_list) + "\n")
# Data
for seed, versions in results.items():
f.write(seed)
for ver in version_list:
count = versions.get(ver, 0)
f.write(f",{count if count > 0 else ''}")
f.write("\n")
print(f"\nCSV exported to: {filename}", file=sys.stderr)
def export_html(results, filename):
"""Export results to HTML table"""
all_versions = set()
for versions in results.values():
all_versions.update(versions.keys())
version_list = sorted(all_versions, key=version_key, reverse=True)
html = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Bitcoin DNS Seed Version Distribution</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
text-align: center;
}
.timestamp {
text-align: center;
color: #666;
margin-bottom: 20px;
}
table {
border-collapse: collapse;
margin: 0 auto;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: center;
}
th {
background-color: #f8931a;
color: white;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #f0f0f0;
}
td:first-child {
text-align: left;
font-weight: 500;
max-width: 300px;
word-wrap: break-word;
}
.total {
font-weight: bold;
background-color: #fff3e0 !important;
}
</style>
</head>
<body>
<h1>Bitcoin DNS Seed Version Distribution</h1>
<div class="timestamp">Generated: """ + time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime()) + """</div>
<table>
<thead>
<tr>
<th>DNS Seed</th>
"""
for ver in version_list:
html += f" <th>{ver}</th>\n"
html += " <th>Total</th>\n"
html += """ </tr>
</thead>
<tbody>
"""
for seed, versions in results.items():
html += " <tr>\n"
html += f" <td>{seed}</td>\n"
total = 0
for ver in version_list:
count = versions.get(ver, 0)
total += count
if count > 0:
html += f" <td>{count}</td>\n"
else:
html += " <td></td>\n"
html += f" <td class='total'>{total}</td>\n"
html += " </tr>\n"
html += """ </tbody>
</table>
</body>
</html>
"""
with open(filename, 'w') as f:
f.write(html)
print(f"HTML table exported to: {filename}", file=sys.stderr)
def print_pretty_table(results):
"""Print a nice ASCII table"""
all_versions = set()
for versions in results.values():
all_versions.update(versions.keys())
version_list = sorted(all_versions, key=version_key, reverse=True)
# Calculate column widths
max_seed_len = max(len(seed) for seed in results.keys())
col_width = 8
# Print header
print("\n" + "=" * (max_seed_len + len(version_list) * col_width + 10))
print(f"{'DNS Seed':<{max_seed_len}}", end="")
for ver in version_list:
print(f"{ver:>{col_width}}", end="")
print(f"{'Total':>{col_width}}")
print("-" * (max_seed_len + len(version_list) * col_width + 10))
# Print data
for seed, versions in results.items():
print(f"{seed:<{max_seed_len}}", end="")
total = 0
for ver in version_list:
count = versions.get(ver, 0)
total += count
if count > 0:
print(f"{count:>{col_width}}", end="")
else:
print(f"{'':>{col_width}}", end="")
print(f"{total:>{col_width}}")
print("=" * (max_seed_len + len(version_list) * col_width + 10))
def main():
seeds = [
"dnsseed.bitcoin.dashjr-list-of-p2p-nodes.us",
"seed.bitcoin.sipa.be",
"dnsseed.bluematt.me",
"seed.bitcoin.jonasschnelli.ch",
"seed.btc.petertodd.net",
"seed.bitcoin.sprovoost.nl",
"dnsseed.emzy.de",
"seed.bitcoin.wiz.biz",
"seed.mainnet.achownodes.xyz",
]
max_nodes_per_seed = 100 # Adjust this to test more/fewer nodes
print("=" * 70, file=sys.stderr)
print("Bitcoin DNS Seed Version Checker", file=sys.stderr)
print("=" * 70, file=sys.stderr)
results = {}
for seed in seeds:
try:
versions = test_dns_seed(seed, max_nodes_per_seed)
results[seed] = versions
except KeyboardInterrupt:
print("\n\nStopped by user", file=sys.stderr)
break
except Exception as e:
print(f" Error: {e}", file=sys.stderr)
results[seed] = {}
print("\n" + "=" * 70, file=sys.stderr)
print("GENERATING OUTPUT FILES", file=sys.stderr)
print("=" * 70, file=sys.stderr)
timestamp = int(time.time())
# Generate all output formats
csv_filename = f"dns-seed-versions-{timestamp}.csv"
html_filename = f"dns-seed-versions-{timestamp}.html"
export_csv(results, csv_filename)
export_html(results, html_filename)
# Print pretty table to console
print_pretty_table(results)
print(f"\n📊 Files created:", file=sys.stderr)
print(f" • {csv_filename} (CSV for Excel)", file=sys.stderr)
print(f" • {html_filename} (HTML - open in browser for screenshot)", file=sys.stderr)
print(f"\n💡 Tip: Open the HTML file in your browser for the best screenshot!", file=sys.stderr)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment