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) 2022 Pieter Wuille3# Distributed under the MIT software license, see the accompanying4# file LICENSE or http://www.opensource.org/licenses/mit-license.php.6import argparse7import sys8import ipaddress9import json10import math11from collections import defaultdict13import asmap15def load_file(input_file):16try:17contents = input_file.read()18except OSError as err:19sys.exit(f"Input file '{input_file.name}' cannot be read: {err.strerror}.")20try:21bin_asmap = asmap.ASMap.from_binary(contents)22except ValueError:23bin_asmap = None24txt_error = None25entries = None26try:27txt_contents = str(contents, encoding="utf-8")28except UnicodeError:29txt_error = "invalid UTF-8"30txt_contents = None31if txt_contents is not None:32entries = []33for line in txt_contents.split("\n"):34idx = line.find('#')35if idx >= 0:36line = line[:idx]37line = line.lstrip(' ').rstrip(' \t\r\n')38if len(line) == 0:39continue40fields = line.split(' ')41if len(fields) != 2:42txt_error = f"unparseable line '{line}'"43entries = None44break45prefix, asn = fields46if len(asn) <= 2 or asn[:2] != "AS" or any(c < '0' or c > '9' for c in asn[2:]):47txt_error = f"invalid ASN '{asn}'"48entries = None49break50try:51net = ipaddress.ip_network(prefix)52except ValueError:53txt_error = f"invalid network '{prefix}'"54entries = None55break56entries.append((asmap.net_to_prefix(net), int(asn[2:])))57if entries is not None and bin_asmap is not None and len(contents) > 0:58sys.exit(f"Input file '{input_file.name}' is ambiguous.")59if entries is not None:60state = asmap.ASMap()61state.update_multi(entries)62return state63if bin_asmap is not None:64return bin_asmap65sys.exit(f"Input file '{input_file.name}' is neither a valid binary asmap file nor valid text input ({txt_error}).")68def save_binary(output_file, state, fill):69contents = state.to_binary(fill=fill)70try:71output_file.write(contents)72output_file.close()73except OSError as err:74sys.exit(f"Output file '{output_file.name}' cannot be written to: {err.strerror}.")76def save_text(output_file, state, fill, overlapping):77for prefix, asn in state.to_entries(fill=fill, overlapping=overlapping):78net = asmap.prefix_to_net(prefix)79try:80print(f"{net} AS{asn}", file=output_file)81except OSError as err:82sys.exit(f"Output file '{output_file.name}' cannot be written to: {err.strerror}.")83try:84output_file.close()85except OSError as err:86sys.exit(f"Output file '{output_file.name}' cannot be written to: {err.strerror}.")88def main():89parser = argparse.ArgumentParser(description="Tool for performing various operations on textual and binary asmap files.")90subparsers = parser.add_subparsers(title="valid subcommands", dest="subcommand")92parser_encode = subparsers.add_parser("encode", help="convert asmap data to binary format")93parser_encode.add_argument('-f', '--fill', dest="fill", default=False, action="store_true",94help="permit reassigning undefined network ranges arbitrarily to reduce size")95parser_encode.add_argument('infile', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer,96help="input asmap file (text or binary); default is stdin")97parser_encode.add_argument('outfile', nargs='?', type=argparse.FileType('wb'), default=sys.stdout.buffer,98help="output binary asmap file; default is stdout")100parser_decode = subparsers.add_parser("decode", help="convert asmap data to text format")101parser_decode.add_argument('-f', '--fill', dest="fill", default=False, action="store_true",102help="permit reassigning undefined network ranges arbitrarily to reduce length")103parser_decode.add_argument('-n', '--nonoverlapping', dest="overlapping", default=True, action="store_false",104help="output strictly non-overall ping network ranges (increases output size)")105parser_decode.add_argument('infile', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer,106help="input asmap file (text or binary); default is stdin")107parser_decode.add_argument('outfile', nargs='?', type=argparse.FileType('w'), default=sys.stdout,108help="output text file; default is stdout")110parser_diff = subparsers.add_parser("diff", help="compute the difference between two asmap files")111parser_diff.add_argument('-i', '--ignore-unassigned', dest="ignore_unassigned", default=False, action="store_true",112help="ignore unassigned ranges in the first input (useful when second input is filled)")113parser_diff.add_argument('infile1', type=argparse.FileType('rb'),114help="first file to compare (text or binary)")115parser_diff.add_argument('infile2', type=argparse.FileType('rb'),116help="second file to compare (text or binary)")118parser_diff_addrs = subparsers.add_parser("diff_addrs",119help="compute difference between two asmap files for a set of addresses")120parser_diff_addrs.add_argument('-s', '--show-addresses', dest="show_addresses", default=False, action="store_true",121help="include reassigned addresses in the output")122parser_diff_addrs.add_argument("infile1", type=argparse.FileType("rb"),123help="first file to compare (text or binary)")124parser_diff_addrs.add_argument("infile2", type=argparse.FileType("rb"),125help="second file to compare (text or binary)")126parser_diff_addrs.add_argument("addrs_file", type=argparse.FileType("r"),127help="address file containing getnodeaddresses output to use in the comparison "128"(make sure to set the count parameter to zero to get all node addresses, "129"e.g. 'bitcoin-cli getnodeaddresses 0 > addrs.json')")130args = parser.parse_args()131if args.subcommand is None:132parser.print_help()133elif args.subcommand == "encode":134if args.outfile.isatty():135sys.exit("Not much use in writing binary to a TTY. Please specify an output file or pipe output to another process.")136state = load_file(args.infile)137save_binary(args.outfile, state, fill=args.fill)138elif args.subcommand == "decode":139state = load_file(args.infile)140save_text(args.outfile, state, fill=args.fill, overlapping=args.overlapping)141elif args.subcommand == "diff":142state1 = load_file(args.infile1)143state2 = load_file(args.infile2)144ipv4_changed = 0145ipv4_entries_changed = 0146ipv6_changed = 0147ipv6_entries_changed = 0148for prefix, old_asn, new_asn in state1.diff(state2):149if args.ignore_unassigned and old_asn == 0:150continue151net = asmap.prefix_to_net(prefix)152if isinstance(net, ipaddress.IPv4Network):153ipv4_changed += net.num_addresses154ipv4_entries_changed += 1155elif isinstance(net, ipaddress.IPv6Network):156ipv6_changed += net.num_addresses157ipv6_entries_changed += 1158if new_asn == 0:159print(f"# {net} was AS{old_asn}")160elif old_asn == 0:161print(f"{net} AS{new_asn} # was unassigned")162else:163print(f"{net} AS{new_asn} # was AS{old_asn}")164ipv4_change_str = "" if ipv4_changed == 0 else f" (2^{math.log2(ipv4_changed):.2f})"165ipv6_change_str = "" if ipv6_changed == 0 else f" (2^{math.log2(ipv6_changed):.2f})"167print(168f"""# Summary169IPv4: {ipv4_entries_changed} entries with {ipv4_changed}{ipv4_change_str} addresses changed170IPv6: {ipv6_entries_changed} entries with {ipv6_changed}{ipv6_change_str} addresses changed"""171)172elif args.subcommand == "diff_addrs":173state1 = load_file(args.infile1)174state2 = load_file(args.infile2)175address_info = json.load(args.addrs_file)176addrs = {a["address"] for a in address_info if a["network"] in ["ipv4", "ipv6"]}177reassignments = defaultdict(list)178for addr in addrs:179net = ipaddress.ip_network(addr)180prefix = asmap.net_to_prefix(net)181old_asn = state1.lookup(prefix)182new_asn = state2.lookup(prefix)183if new_asn != old_asn:184reassignments[(old_asn, new_asn)].append(addr)185reassignments = sorted(reassignments.items(), key=lambda item: len(item[1]), reverse=True)186num_reassignment_type = defaultdict(int)187for (old_asn, new_asn), reassigned_addrs in reassignments:188num_reassigned = len(reassigned_addrs)189num_reassignment_type[(bool(old_asn), bool(new_asn))] += num_reassigned190old_asn_str = f"AS{old_asn}" if old_asn else "unassigned"191new_asn_str = f"AS{new_asn}" if new_asn else "unassigned"192opt = ": " + ", ".join(reassigned_addrs) if args.show_addresses else ""193print(f"{num_reassigned} address(es) reassigned from {old_asn_str} to {new_asn_str}{opt}")194num_reassignments = sum(len(addrs) for _, addrs in reassignments)195share = num_reassignments / len(addrs) if len(addrs) > 0 else 0196print(f"Summary: {num_reassignments:,} ({share:.2%}) of {len(addrs):,} addresses were reassigned "197f"(migrations={num_reassignment_type[True, True]}, assignments={num_reassignment_type[False, True]}, "198f"unassignments={num_reassignment_type[True, False]})")199else:200parser.print_help()201sys.exit("No command provided.")203if __name__ == '__main__':204main()
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.