Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2013-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5#6# Generate seeds.txt from Pieter's DNS seeder7#9import argparse10import collections11import ipaddress12from pathlib import Path13import random14import re15import sys16from typing import Union18asmap_dir = Path(__file__).parent.parent / "asmap"19sys.path.append(str(asmap_dir))20from asmap import ASMap, net_to_prefix # noqa: E40222NSEEDS=51224MAX_SEEDS_PER_ASN = {25'ipv4': 2,26'ipv6': 10,27}29MIN_BLOCKS = 91000031PATTERN_IPV4 = re.compile(r"^(([0-2]?\d{1,2})\.([0-2]?\d{1,2})\.([0-2]?\d{1,2})\.([0-2]?\d{1,2})):(\d{1,5})$")32PATTERN_IPV6 = re.compile(r"^\[([\da-f:]+)]:(\d{1,5})$", re.IGNORECASE)33PATTERN_ONION = re.compile(r"^([a-z2-7]{56}\.onion):(\d+)$")34PATTERN_I2P = re.compile(r"^([a-z2-7]{52}\.b32\.i2p):(\d{1,5})$")35PATTERN_AGENT = re.compile(36r"^/Satoshi:("37r"0\.14\.(0|1|2|3|99)"38r"|0\.15\.(0|1|2|99)"39r"|0\.16\.(0|1|2|3|99)"40r"|0\.17\.(0|0\.1|1|2|99)"41r"|0\.18\.(0|1|99)"42r"|0\.19\.(0|1|2|99)"43r"|0\.20\.(0|1|2|99)"44r"|0\.21\.(0|1|2|99)"45r"|22\.(0|1|99)\.0"46r"|23\.(0|1|2|99)\.0"47r"|24\.(0|1|2|99)\.(0|1)"48r"|25\.(0|1|2|99)\.0"49r"|26\.(0|1|2|99)\.0"50r"|27\.(0|1|2|99)\.0"51r"|28\.(0|1|2|3|4|99)\.0"52r"|29\.(0|1|2|3|99)\.0"53r"|30\.(0|1|2|3|99)\.0"54r"|31\.(0|1|99)\.0"55r")")57def parseline(line: str) -> Union[dict, None]:58""" Parses a line from `seeds_main.txt` into a dictionary of details for that line.59or `None`, if the line could not be parsed.60"""61if line.startswith('#'):62# Ignore line that starts with comment63return None64sline = line.split()65if len(sline) < 11:66# line too short to be valid, skip it.67return None68# Skip bad results.69if int(sline[1]) == 0:70return None71m = PATTERN_IPV4.match(sline[0])72sortkey = None73ip = None74if m is None:75m = PATTERN_IPV6.match(sline[0])76if m is None:77m = PATTERN_ONION.match(sline[0])78if m is None:79m = PATTERN_I2P.match(sline[0])80if m is None:81return None82else:83net = 'i2p'84ipstr = sortkey = m.group(1)85port = int(m.group(2))86else:87net = 'onion'88ipstr = sortkey = m.group(1)89port = int(m.group(2))90else:91net = 'ipv6'92if m.group(1) in ['::']: # Not interested in localhost93return None94ipstr = m.group(1)95if ipstr.startswith("fc"): # cjdns looks like ipv6 but always begins with fc96net = "cjdns"97sortkey = ipstr # XXX parse IPv6 into number, could use name_to_ipv6 from generate-seeds98port = int(m.group(2))99else:100# Do IPv4 sanity check101ip = 0102for i in range(0,4):103if int(m.group(i+2)) < 0 or int(m.group(i+2)) > 255:104return None105ip = ip + (int(m.group(i+2)) << (8*(3-i)))106if ip == 0:107return None108net = 'ipv4'109sortkey = ip110ipstr = m.group(1)111port = int(m.group(6))112# Extract uptime %.113uptime30 = float(sline[7][:-1])114# Extract Unix timestamp of last success.115lastsuccess = int(sline[2])116# Extract protocol version.117version = int(sline[10])118# Extract user agent.119agent = sline[11][1:-1]120# Extract service flags.121service = int(sline[9], 16)122# Extract blocks.123blocks = int(sline[8])124# Construct result.125return {126'net': net,127'ip': ipstr,128'port': port,129'ipnum': ip,130'uptime': uptime30,131'lastsuccess': lastsuccess,132'version': version,133'agent': agent,134'service': service,135'blocks': blocks,136'sortkey': sortkey,137}139def dedup(ips: list[dict]) -> list[dict]:140""" Remove duplicates from `ips` where multiple ips share address and port. """141d = {}142for ip in ips:143ip_port = (ip["ip"], ip["port"])144if ip_port not in d or ip["lastsuccess"] > d[ip_port]["lastsuccess"]:145d[ip_port] = ip146return list(d.values())148def filtermultiport(ips: list[dict]) -> list[dict]:149""" Filter out hosts with more nodes per IP"""150hist = collections.defaultdict(list)151for ip in ips:152hist[ip['sortkey']].append(ip)153return [value[0] for (key,value) in list(hist.items()) if len(value)==1]155# Based on Greg Maxwell's seed_filter.py156def filterbyasn(asmap: ASMap, ips: list[dict], max_per_asn: dict, max_per_net: int) -> list[dict]:157""" Prunes `ips` by158(a) trimming ips to have at most `max_per_net` ips from each net (e.g. ipv4, ipv6); and159(b) trimming ips to have at most `max_per_asn` ips from each asn in each net.160"""161# Sift out ips by type162ips_ipv46 = [ip for ip in ips if ip['net'] in ['ipv4', 'ipv6']]163ips_onion = [ip for ip in ips if ip['net'] == 'onion']164ips_i2p = [ip for ip in ips if ip['net'] == 'i2p']165ips_cjdns = [ip for ip in ips if ip["net"] == "cjdns"]167# Filter IPv46 by ASN, and limit to max_per_net per network168result = []169net_count: dict[str, int] = collections.defaultdict(int)170asn_count: dict[int, int] = collections.defaultdict(int)172for i, ip in enumerate(ips_ipv46):173if net_count[ip['net']] == max_per_net:174# do not add this ip as we already too many175# ips from this network176continue177asn = asmap.lookup(net_to_prefix(ipaddress.ip_network(ip['ip'])))178if not asn or asn_count[ip['net'], asn] == max_per_asn[ip['net']]:179# do not add this ip as we already have too many180# ips from this ASN on this network181continue182asn_count[ip['net'], asn] += 1183net_count[ip['net']] += 1184ip['asn'] = asn185result.append(ip)187# Add back Onions (up to max_per_net)188result.extend(ips_onion[0:max_per_net])189result.extend(ips_i2p[0:max_per_net])190result.extend(ips_cjdns[0:max_per_net])191return result193def ip_stats(ips: list[dict]) -> str:194""" Format and return pretty string from `ips`. """195hist: dict[str, int] = collections.defaultdict(int)196for ip in ips:197if ip is not None:198hist[ip['net']] += 1200return f"{hist['ipv4']:6d} {hist['ipv6']:6d} {hist['onion']:6d} {hist['i2p']:6d} {hist['cjdns']:6d}"202def parse_args():203argparser = argparse.ArgumentParser(description='Generate a list of bitcoin node seed ip addresses.')204argparser.add_argument("-a","--asmap", help='the location of the asmap asn database file (required)', required=True)205argparser.add_argument("-s","--seeds", help='the location of the DNS seeds file (required)', required=True)206argparser.add_argument("-m", "--minblocks", help="The minimum number of blocks each node must have", default=MIN_BLOCKS, type=int)207return argparser.parse_args()209def main():210args = parse_args()212print(f'Loading asmap database "{args.asmap}"…', end='', file=sys.stderr, flush=True)213with open(args.asmap, 'rb') as f:214asmap = ASMap.from_binary(f.read())215print('Done.', file=sys.stderr)217print('Loading and parsing DNS seeds…', end='', file=sys.stderr, flush=True)218with open(args.seeds, 'r') as f:219lines = f.readlines()220ips = [parseline(line) for line in lines]221random.shuffle(ips)222print('Done.', file=sys.stderr)224print('\x1b[7m IPv4 IPv6 Onion I2P CJDNS Pass \x1b[0m', file=sys.stderr)225print(f'{ip_stats(ips):s} Initial', file=sys.stderr)226# Skip entries with invalid address.227ips = [ip for ip in ips if ip is not None]228print(f'{ip_stats(ips):s} Skip entries with invalid address', file=sys.stderr)229# Skip duplicates (in case multiple seeds files were concatenated)230ips = dedup(ips)231print(f'{ip_stats(ips):s} After removing duplicates', file=sys.stderr)232# Enforce minimal number of blocks.233ips = [ip for ip in ips if ip['blocks'] >= args.minblocks]234print(f'{ip_stats(ips):s} Enforce minimal number of blocks', file=sys.stderr)235# Require service bit 1.236ips = [ip for ip in ips if (ip['service'] & 1) == 1]237print(f'{ip_stats(ips):s} Require service bit 1', file=sys.stderr)238# Require at least 50% 30-day uptime for clearnet, onion and i2p; 10% for cjdns239req_uptime = {240'ipv4': 50,241'ipv6': 50,242'onion': 50,243'i2p': 50,244'cjdns': 10,245}246ips = [ip for ip in ips if ip['uptime'] > req_uptime[ip['net']]]247print(f'{ip_stats(ips):s} Require minimum uptime', file=sys.stderr)248# Require a known and recent user agent.249ips = [ip for ip in ips if PATTERN_AGENT.match(ip['agent'])]250print(f'{ip_stats(ips):s} Require a known and recent user agent', file=sys.stderr)
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.