两个对端刚建立连接,互相报完自己链尖的位置,接下来的第一件事是找到彼此共享的那一段链。问题在于两条链可能只在很深的历史里才重合——各自主链上有几百个对方根本不知道的块。逐块回溯要发几千条消息,比特币的做法是用一条 getheaders 携带一份精心挑选的“定位表”,让对方几步之内猜出分叉点,再用有上限的 headers 消息批量补齐。这套机制的尺寸常量都写在 v31.0 源码里,值得逐个看清楚。
定位表(block locator)的构造思路是“从链尖向创世方向、步长不断翻倍”。节点先放入自己的链尖哈希,然后往回跳一步、再两步、四步、八步,指数式加速,中途把创世块哈希垫在最后。为什么要指数回跳:距离现在越近的位置,双方分叉的概率越高;越远的位置,用一两个采样点就足够把搜索范围切大。这份表不是无限长的——v31.0 的 net_processing.cpp 把 MAX_LOCATOR_SZ 定为 101,getblocks 与 getheaders 里都有同一道检查,超过就直接丢弃消息并记日志。表越往后跳得越远,一百零一条足以覆盖从相邻块到几十万块深度的全部距离,再长就是浪费带宽。
对方的处理是对称的:从表头开始逐个问“这个块你认识吗”,通常第一个就命中,说明两条链尖相邻,直接把后面的头列表发过来即可;若命中发生在靠后的位置,对方就从命中的块往后发 headers,一次消息最多两千条——MAX_HEADERS_RESULTS 在 net_processing.h 里等于 2000。若定位表里一个都不认识,回复是一个空的 headers,这本身也是信息:分叉在极深处,剩下的交给创世块兜底。整个过程的消息量与分叉深度呈对数关系,而不是线性关系。
对端之间真正用来搬运区块数据的 inv 消息同样有尺寸纪律。MAX_INV_SZ 在 v31.0 里是 50000:任何 inv 条目超过五万条,整条消息作废,日志里留下条目数与实际大小的对比。这个数字与区块体量是配套的——一笔交易一条条目,块与过滤器各有自己的类型位,五万条的余量足够声明一个巨块的全部交易,又不至于让单条 inv 变成内存负担。发送方向上另有节流:单次 getdata 批量按一千条控制(MAX_GETDATA_SZ,源码注释明确它只用于发送侧、接收侧出于兼容不做此限制),每条连接在途请求的区块数被压在个位数到十六这个量级,块与块之间的流水线深度因此可控。
这些常量组合起来解释了日常日志里的几种形态。刚同步完历史的新节点会连续收发大块头清单,那是 headers-first 阶段的正常心跳:先花几分钟把几十万个区块头排成骨架,再决定从哪些对端并行取正文。取正文阶段若你抓包,看到的是一条连接上十几条在途 getdata 的滑动窗口——窗口小是故意的,坏块、慢盘、拥塞都在这个尺度上被消化,不至于让单条坏连接拖死同步。反过来,如果日志反复出现 locator size 或 inv size 越限的调试信息,几乎总是对端实现有缺陷,你的节点按常量丢弃消息正是设计行为,不需要你做任何配置。
还有一个容易被忽略的性质:定位表暴露的是采样位置的哈希,等于向对端公开“我的链尖大概在哪里、我在哪些高度有块”。这属于同步的必要泄漏,与地址发现相比规模有限,但对隐私敏感的连接方值得知道。协议为此提供了区块专线式的连接类别——只谈块头与块、不参与交易中继,正是把这类探测面收窄的工程折中。
回到开头的问题,为什么比特币能在没有索引服务、没有中心目录的情况下让任意两个节点快速对齐:靠的不是谁记录得多,而是一份对数尺度的采样表加一组克制的尺寸上限。101 条定位哈希管“找到共同祖先”,2000 条头消息管“补齐骨架”,50000 条 inv 上限管“声明库存”,十几个在途窗口管“取货节奏”。每个数字都不大,但四个数字咬合在一起,让最坏情况的同步流量与历史深度只成对数关系。二十年前定下这些常量时谁也没法预知链长到今天,而这套尺度的伸缩性正是它们至今原封不动的原因。
风险提示:本文为网络协议机制说明,不构成任何投资建议;文中常量以比特币核心 v31.0 源码为准,后续版本可能调整,请以对应版本为准。

发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。