Created
October 28, 2025 22:00
-
-
Save niteshbalusu11/582f450104a0493ed78a0d0edb54a928 to your computer and use it in GitHub Desktop.
Fetch node versions from dns seeds
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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