too-little-chainwork:没通过工作量安检的区块头为什么不建档 图 1
too-little-chainwork:没通过工作量安检的区块头为什么不建档 · 图 1

节点日志里出现过这样一行:调试信息写着 not adding new block header,后面跟一句 missing anti-dos proof-of-work validation,同时向上返回拒绝代号 too-little-chainwork。很多人第一反应是”这个块的证明工作算错了”,其实不是。它对应的是另一套机制:节点在把区块头写进自己的头库之前,先做一次反消耗战的工作量粗筛,没过这道筛的头干脆不建档。

参数名叫 min_pow_checked

比特币核心 v31.0 源码里,AcceptBlockHeader 的函数签名带一个布尔参数 min_pow_checked,头文件注释直说:调用方必须把 min_pow_checked 置为真,才允许把一个新头加入链结构。ProcessNewBlock 和 ProcessNewBlockHeaders 两个入口也都把这个参数透传下来。它表达的不是”这个头的哈希算对了没有”,而是”调用我的这条路径,是否已经替我做过了反消耗战检查”。

两件事要分开看。第一件是共识层面的工作量证明校验:头里的难度位(nBits)与哈希是否满足目标,这一步在任何路径上都省不掉。第二件是这里的 min_pow_checked:它防的是”用一堆算得出哈希、但几乎没花力量的头把你内存里的头索引撑爆”。第二件是策略性的粗筛,代价极低,收益是拒绝一大类廉价攻击面。

未请求的块,走的是严路径

对端推来的数据分两类,v31.0 给它们不同的默认值。

  • 对端主动塞过来、我们并没有要的块:源码先把 min_pow_checked 置为假,再算一笔账——前一个区块头累计的链上工作量,加上这个块自身宣称的工作量,是否不小于一条阈值线。达标才把 min_pow_checked 改回真,然后交给处理流程。
  • 我们自己下过订单要回来的块,以及从紧凑区块重建出来的块:直接以 force_processing=true 加 min_pow_checked=true 进入处理。理由写在注释里:既然是我们主动请求的,或者只在我们快要追平时才会做重建,那这条路径本身已经带了限制,可以放行。
  • 对端用 headers 消息推来的头、cmpctblock 消息里带的头:同样按真处理。前者是同步协议的一部分,后者由紧凑区块的整套限制兜着。

这条阈值线由 GetAntiDoSWorkThreshold() 给出,算法很直白:取”当前链顶累计工作量减去 144 个当前难度块的工作量”与”节点配置的最低链上工作量”两者中的较大值。留 144 个块的余量,是为了不打断从靠近链顶处正常分叉的合法竞争链。

拒绝之后的待遇:不罚对端

too-little-chainwork 对应的内部结果是 BLOCK_HEADER_LOW_WORK。有意思的是,节点在决定要不要处罚送块的邻居时,对这个结果什么都不做,注释解释:我们根本没试着处理这个块,因为它的头链工作量可能太少。相比之下,真正的共识层无效(BLOCK_CONSENSUS、BLOCK_MUTATED)在没有走紧凑区块通道时会触发处罚;BLOCK_MISSING_PREV、BLOCK_INVALID_HEADER 也会处罚;未来的时间戳(BLOCK_TIME_FUTURE)则只是搁置不罚。

这套分工说明 too-little-chainwork 的定位:它不指控你在作弊,只是说”你这条链现在还不够格让我花钱去存”。所以对端反复推低工作量的分支,不会因此被封,但也不会被记录。运维视角上,看到这条日志通常意味着节点正在被某条较短的分支骚扰,或者你手动 invalidateblock 之后本地链形态发生了变化,属于可观察、不需要动手的信号。

三条实操结论

一是别把它当故障。偶发的 too-little-chainwork 出现在调试日志里,说明粗筛在正常工作;如果它高频刷屏,先核对节点的最低链上工作量配置与同步状态,而不是急着重启。

二是理解”为什么不存”。头索引是常驻内存的结构,一个头几十到八十来字节,看起来便宜,但没有粗筛时攻击者可以用巨量低难度头把它推高。粗筛把”值不值得占内存”这道判断前移到一次加法比较上,成本几乎为零。

三是别把这个代号读成”这个块被判定无效”。它只表示未被纳入头库处理,任何后续的下载、验证、中继都不会围绕它展开;如果这条分支后来真的积累了足够工作量,节点会走正常的头同步路径重新认识它。

风险提示:本文描述的是比特币核心 v31.0 源码中的实现细节,参数与代号可能随版本调整,请以你实际运行版本的源码和文档为准;内容为技术说明,不构成任何投资建议。