Bitcoin Core ASMap 工具实战:asmap-tool.py 的编码、解码与对比全解析
本文围绕 contrib/asmap/README.md 讲解 Bitcoin Core 附带的 ASMap 工具(asmap-tool.py):如何用 encode/decode 在文本与二进制两种 ASMap 格式间转换、用 diff/diff_addrs 评估两份 ASMap 之间的变化对实际节点地址的影响;并结合 asmap.py 与 src/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 对此给出了明确的三条告诫,值得完整记住:
- 有损:丢失了"哪些范围原本未分配"的信息;
- 若输入 ASMap 本身不完整,fill 会把本应有分配的网段也改掉,产出可能与现实显著偏离的 ASMap;
- 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 的流程:
-
把节点已知的地址导出到文件(
count参数设为 0 以获取全部地址):bitcoin-cli getnodeaddresses 0 > addrs.json -
把地址文件作为第三个参数传入:
python3 asmap-tool.py diff_addrs path/to/first.file path/to/second.file addrs.json
源码实现(asmap-tool.py)中:解析 getnodeaddresses 的 JSON,仅取 network 为 ipv4/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 端 _Instruction 与 C++ 端 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.cpp 与 src/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 相当完整且不再参与比较,否则保持默认的无损编码。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00