首页
/ Bitcoin Core ASMap 工具实战:asmap-tool.py 的编码、解码与对比全解析

Bitcoin Core ASMap 工具实战:asmap-tool.py 的编码、解码与对比全解析

2026-09-05 22:07:57作者:余洋婵Anita

本文围绕 contrib/asmap/README.md 讲解 Bitcoin Core 附带的 ASMap 工具(asmap-tool.py):如何用 encode/decode 在文本与二进制两种 ASMap 格式间转换、用 diff/diff_addrs 评估两份 ASMap 之间的变化对实际节点地址的影响;并结合 asmap.pysrc/util/asmap.cpp 源码,深入解释二进制 ASMap 的指令式编码格式,帮助读者真正理解 -asmap 选项背后的实现机制与使用取舍。

什么是 ASMap,以及它如何进入 Bitcoin Core

ASMap(Autonomous System Map)是一份"IP 前缀 → ASN(自治系统号)"的映射表。Bitcoin Core 默认按 IP 地址的 /16 前缀做同族(same-family)分桶,而启用 ASMap 后可以按 IP 所属的自治系统分桶,使地址簿中的对等节点在 AS 级别上更加分散,这是 -asmap 选项的核心目的。

src/init.cpp 的参数定义可见,-asmap=<file> 的完整语义是:

  • 相对路径会以"当前网络的 datadir"(net-specific datadir)为前缀拼接;
  • 若给的是布尔值(-asmap-asmap=1),则使用编译进二进制的内嵌映射数据(取决于构建时是否启用 ENABLE_EMBEDDED_ASMAP,见 src/init.cpp)。

启动时的加载与校验逻辑在 src/init.cpp:文件不存在时报 Could not find asmap file,解析失败时报 Could not parse asmap file,成功后日志输出 Using asmap version <sha256> for IP bucketing——该版本哈希由 AsmapVersion() 对整个文件计算 SHA256 得到,便于在日志与 RPC 中识别当前使用的 ASMap 版本。未配置 ASMap 时,日志则为 Using /16 prefix for IP bucketing

这些错误路径与日志断言都有对应的功能测试:test/functional/feature_asmap.py 覆盖了无参数、-noasmap、绝对/相对路径、内嵌数据、缺失文件、空文件等场景,并校验预期的 debug 日志。

工具总览:四个子命令

ASMap 文件在文本形态下相当大,供 Bitcoin Core 使用前需要编码为紧凑的二进制格式。contrib/asmap/asmap-tool.py 就是完成这些转换与比较的官方工具,核心库在 contrib/asmap/asmap.py。README 给出的基本用法:

python3 asmap-tool.py encode /path/to/input.file /path/to/output.file
python3 asmap-tool.py decode /path/to/input.file /path/to/output.file
python3 asmap-tool.py diff /path/to/first.file /path/to/second.file
python3 asmap-tool.py diff_addrs /path/to/first.file /path/to/second.file addrs.file

README 特别提示:这些命令用 python3 跑可能需要几分钟,取决于数据量与机器性能;若追求更快的运行时间,可以改用 pypy3

asmap-tool.py 的 argparse 定义 可以看到完整的参数面(比 README 更细,例如流默认值与 diff_addrs-s 选项):

子命令 选项 含义
encode -f/--fill 允许把未定义网段任意重分配给相邻 AS,以压缩输出体积
encode infile / outfile 输入为文本或二进制 asmap(默认 stdin),输出为二进制(默认 stdout)
decode -f/--fill 解码输出文本时同样允许重分配未定义网段,缩短输出
decode -n/--non-overlapping 输出严格不重叠的网段(输出更大,但语义更清晰)
diff -i/--ignore-unassigned 忽略"第二份有、第一份没有"的新增分配(当第二份是 fill 过的输出时有用)
diff_addrs -s/--show-addresses 在输出中列出具体被重新分配的地址(README 未提及,源码支持)

两个值得注意的实现细节(见 main()):

  • encode 拒绝把二进制直接写到 TTY(Not much use in writing binary to a TTY...),必须指定输出文件;
  • 输入文件不指定时默认读 stdin,输出不指定时默认写 stdout,因此工具可以自然嵌入 shell 管道。

输入格式:文本与二进制的自动识别

load_file() 对输入做双路尝试:先按二进制 ASMap 解码(ASMap.from_binary()),再按 UTF-8 文本逐行解析。文本格式为每行一个 <网段> <AS数字>,例如:

217.199.160.0/19 AS26496
2001:470:49::/48 AS20205

解析规则包括:# 之后视为注释、允许空行、ASN 字段必须形如 AS 加纯数字、网段需能被 ipaddress.ip_network 解析。更关键的是歧义检测:如果文件同时能被当作合法二进制和合法文本解析,工具会直接报错 Input file '...' is ambiguous.asmap-tool.py)。文本多条映射通过 ASMap.update_multi() 批量写入,其中较长前缀优先生效。

encode 命令:文本转二进制与 --fill 的取舍

encode 把 ASMap 编码为 Bitcoin Core 可直接消费的紧凑二进制。核心调用是 ASMap.to_binary(fill),它先在内部二进制前缀树(trie)上寻找体积最小的指令序列,再把比特流按位打包进字节。

--fill/-f 标志用空间换时间的方式进一步压缩:当某个网段未分配、而其相邻网段有分配时,直接借用相邻 AS 的编号。README 对此给出了明确的三条告诫,值得完整记住:

  1. 有损:丢失了"哪些范围原本未分配"的信息;
  2. 若输入 ASMap 本身不完整,fill 会把本应有分配的网段也改掉,产出可能与现实显著偏离的 ASMap;
  3. fill 过的文件再拿去做 diff 将不再有意义

因此结论是:只有在追求空间优化、输入 ASMap 相当完整、且不打算日后对其做 diff 时,才使用 --fill

从源码看,fill 的实际行为体现在 _to_binnode():trie 中值为 0(未分配)的叶节点,在 fill 模式下被视同"任意上下文"(None)参与编码,从而允许上层用 DEFAULT 指令统一兜底,压缩结果体积。单元测试 TestASMap.test_asmap_roundtrips() 验证了 fill 编码解码回原映射后满足 extends() 关系——即"原映射在所有已分配子网上仍然成立",精确印证了 fill 的有损特性。

decode 命令:二进制转文本

decode 反向操作,由 save_text() 调用 ASMap.to_entries(overlapping, fill) 生成 <网段> <AS数字> 文本行。两个可选标志:

  • --fill/-f:与 encode 同理,通过重分配未映射网段缩短输出;
  • --non-overlapping/-n:输出严格不重叠的网段集合。默认(overlap 模式)走 _to_entries_minimal(),允许网段重叠以换取更少的行数;加 -n 后走 _to_entries_flat(),按 trie 展开为互不重叠的划分,行数更多但便于人工审阅。

diff 命令:量化两份 ASMap 的差异

由于 AS 对 IP 网络的控制经常变化,README 指出用 diff 命令获取两份 ASMap 之间的变化很有价值。其底层是 ASMap.diff():对两棵 trie 做递归对比,凡是叶子 ASN 不同的最小前缀都输出为 (prefix, old_asn, new_asn) 三元组。

diff 的输出有三种状态(README 给出的示例完整保留了三种形态):

217.199.160.0/19 AS26496 # was AS20738        # 重新分配到新 AS
# 220.157.65.0/24 was AS9723                   # 第一份有、第二份没有
216.151.172.0/23 AS400080 # was unassigned     # 第二份有、第一份没有
2001:470:49::/48 AS20205 # was AS6939
# 2001:678:bd0::/48 was AS207631
2001:67c:308::/48 AS26496 # was unassigned

对应 main() 中的打印逻辑new_asn == 0 时输出 # <net> was AS<old>old_asn == 0 时输出 <net> AS<new> # was unassigned;否则输出 <net> AS<new> # was AS<old>

此外,diff 会累计变更规模并以对数形式输出总结,例如 IPv4: 12 entries with 524288 (2^19.00) addresses changed——这给了维护者一个直观的量级感(该总结块是 README 未提及、但源码实际输出的部分)。--ignore-unassigned/-i 跳过所有 old_asn == 0 的条目,README 说明其用途是:当第二份输入是 fill 过的文件时,避免把大量"新增分配"噪音计入差异。

diff_addrs 命令:评估差异对真实对等地址的影响

diff 回答"ASMap 本身变了什么",而 diff_addrs 进一步回答"这些变化对我的节点实际看到的对等地址意味着什么"。按 README 的流程:

  1. 把节点已知的地址导出到文件(count 参数设为 0 以获取全部地址):

    bitcoin-cli getnodeaddresses 0 > addrs.json
    
  2. 把地址文件作为第三个参数传入:

    python3 asmap-tool.py diff_addrs path/to/first.file path/to/second.file addrs.json
    

源码实现(asmap-tool.py)中:解析 getnodeaddresses 的 JSON,仅取 networkipv4/ipv6 的地址;对每个地址分别在两份 ASMap 中做 lookup(),若 ASN 变化则按 (old_asn, new_asn) 分组并按数量降序输出,形如 123 address(es) reassigned from AS15169 to AS20115。最后打印一行汇总,把变化分为三类计数:migrations(有 AS 变到另一个 AS)、assignments(未分配变为分配)、unassignments(分配变为未分配),并给出重新分配地址的绝对数与百分比。若想看到具体是哪些地址被重分配,加上 -s/--show-addresses 即可。

源码纵深:二进制 ASMap 的指令集编码

理解 encode 产物的结构,有助于理解 Bitcoin Core 启动时的校验行为。asmap.py 的 docstring 与 C++ 端 src/util/asmap.cpp 的头注释描述的是同一格式:整个映射被编译为一段按位连续存放(little-endian bit 序)的"字节码",运行时用 IP 地址的比特逐位解释执行。指令集共四种(Python 端 _InstructionC++ 端 Instruction 一一对应):

指令 编码前缀 语义
RETURN [0] 返回一个常量 ASN(后随 ASN 变长编码)
JUMP [1,0] 检查输入下一位:0 则继续执行左子程序,1 则跳过指定比特数
MATCH [1,1,0] 将输入的 1~8 位与参数模式比较,全部匹配才继续,否则返回 default
DEFAULT [1,1,1] 设置后续 MATCH 失败时返回的默认 ASN,然后继续

参数均使用变长编码(Python 端 _VarLenCoder 与 C++ 端 DecodeBits() 互为镜像):ASN 用 10 档、每档 15~24 bit 的类(minval=1,可编码到约 1670 万),MATCH 参数 2~511,JUMP 偏移 minval=17、每档 5~30 bit。指令选择与跳转目标大小由 _to_binnode() 在各上下文中择优(比较候选编码的比特数),这也是为什么编码过程计算量大、README 才会建议 pypy3。

运行时解释器是 Interpret():以 IP 比特为输入执行上述指令,RETURN 命中即返回 ASN。启动前还有一次全路径模拟——SanityCheckAsmap() 校验跳转不越界、不交叉、无不可达代码、DEFAULT/RETURN 不冗余相邻、尾部填充合法等,任何异常都会让 DecodeAsmap() 返回空,最终触发上文提到的 Could not parse asmap file 启动错误。

验证、基准与测试资源

仓库中与该工具主题直接相关的配套资源:

  • contrib/asmap/asmap.py 自带单元测试(TestASMap),覆盖 IPv4/IPv6 前缀往返、entries 与二进制的随机往返(含 fill 与非 fill)、以及 update/lookup/extends/diff 的补丁一致性验证;
  • src/bench/asmap.cpp 提供 Interpret 的基准测试,可评估 ASMap 查询开销;
  • src/test/fuzz/asmap.cppsrc/test/fuzz/asmap_direct.cpp 对解码与解释器做模糊测试;
  • test/functional/feature_asmap.py 在真实节点上验证 -asmap 的各种传参形态与日志输出,其断言中硬编码了单元测试用 ASMap(src/test/data/asmap.raw,59 字节)对应的版本哈希 bafc9da3...f68895,可作为"版本哈希 = 文件 SHA256"这一行为的佐证。

小结与实操建议

围绕 contrib/asmap/README.md 的完整工作流可以概括为:用 decode -n 审阅现有二进制 ASMap 的文本形态;用 encode 将社区维护的文本 ASMap 压缩为节点可加载的二进制;当上游 ASMap 更新时,先用 diff 看整体变化量级,再用 getnodeaddresses 0 导出本节点地址、用 diff_addrs 精确量化影响面(哪些对等地址发生了 AS 迁移、迁移/分配/取消分配的占比)。需要记住的取舍只有一条主线:--fill 换来更小的文件,代价是丢失未分配信息、可能偏离真实分配、且产物不可再用于 diff——除非 ASMap 相当完整且不再参与比较,否则保持默认的无损编码。

登录后查看全文
热门项目推荐
相关项目推荐