一条addr消息进节点要过几道门:从洗牌到入地址库的顺序 图 1
一条addr消息进节点要过几道门:从洗牌到入地址库的顺序 · 图 1

一条 addr 消息到达节点时,里面可能塞着一千条”我见过的活跃节点”地址。节点对这份名单的处理不是收下入库这么简单,而是一条有多道闸门的流水线。按 v31.0 源码 net_processing.cpp 中 addr 分支的执行顺序走一遍,能解释很多”为什么我收不到某个节点地址”的日常困惑。

第一道门:整包体检

先验条数:单条消息超过一千条地址直接判违规,连接会被标记 Misbehaving。注意这条线对”超发”是零容忍,但对”少发”很宽松——同一对端每小时能发多少次 addr,由另一套令牌桶限速管(每十分之一秒补充一个处理额度,桶深一千),所以刷屏不会挤爆你的 CPU,只会自己排队。

一条addr消息进节点要过几道门:从洗牌到入地址库的顺序 图 2
一条addr消息进节点要过几道门:从洗牌到入地址库的顺序 · 图 2

第二道门:逐条过滤

通过后先把整个列表洗牌打乱——这一手很关键:限速会在随机位置截断处理,洗牌保证被丢的是随机样本而不是”列表尾部固定人群”,攻击者没法把想让你扔的地址排到后面。然后逐条过筛:

  1. 令牌检查:桶里没额度就跳过(计入 rate-limited);
  2. 服务位筛查:MayHaveUsefulAddressDB 看服务位里有没有”能提供完整或受限历史”的标志,全节点、受限全节点之外的大部分轻量客户端地址直接被忽略——节点只想存能当邻居用的地址;
  3. 时间钳制:时间戳太老、等于初始哨兵值、或者超前本地时钟十分钟以上的,一律改写成”五天前”——既收下可疑地址又不让它冒充新鲜;
  4. 封禁名单:已封禁或被策略劝退的地址只记已知,不再处理;
  5. 中继条件:时间戳在十分钟内、来自对方主动的 getaddr 响应之外、且消息总共不到十一条时,这条地址才有资格被转发给你的其他邻居——默认只中继新鲜地址,防陈旧信息扩散;
  6. 可达性过滤:与你当前网络视角不可达的地址(比如你关了 IPv6 支持)不会入库,只留在”已见过”集合里防重复。

第三道门:入库还要扣时间

闯过全部闸门的地址交给地址管理器,统一带两小时时间罚时:邻居转告的地址永远比它自报的时间戳晚两小时才算”见过”,自报地址才免罚。这是给”转发链”上的时间注水留的缓冲——每转一手扣一次,多跳传播伪造新鲜度就会露馅。

与出向的一侧对照

入向有门槛,出向同样有账:响应 getaddr 时节点最多交出地址库里约四分之一的存量(源码常量 MAX_PCT_ADDR_TO_SEND 为 23),单条消息封顶一千条,主动中继还按二十四小时周期轮换投递目标,避免任何对端反复询问就拼出你的完整邻居图谱。入向防灌、出向防泄,两半合起来才是地址层的完整防务。也正因为两侧独立限速,一个”地址传播特别慢”的节点未必有问题——它可能只是安静地按自己的节奏过闸门。

处理顺序还解释了一个反直觉现象:地址”被收到”和”被入库”是两个事件。已经见过的地址会进每个对端的已知集合(这就是为什么同一条地址第二次传来几乎零成本,节点不再处理也不会转发),但对端集合和地址库是两套存储:前者为防重复投递,后者才是选邻居用的候选池。日志级别调到 net 分类后,上面每一道闸门的去留都有计数可查,绝大多数”我发的地址别人收不到”的工单,查到最后是服务位、可达性或时钟三个环节之一。

排障视角

日志里一行 Received addr: ... (N processed, M rate-limited) 直接对应这条流水线的产出。如果节点长期收不到新邻居,按此顺序排查:确认本机时钟没有大幅漂移(时间钳制会把超前地址全打成五天前)、确认没有挂满封禁/劝退名单、确认所在网络类型(-onlynet)与可达性设置没有把目标网络排除在入库门外。机制均对照 v31.0 源码,历史版本的阈值有过调整,核对旧版本请看对应 tag 的源码。