轻钱包同步过滤器的三对P2P消息:getcfilters、getcfheaders与cfcheckpt各管一段 图 1
轻钱包同步过滤器的三对P2P消息:getcfilters、getcfheaders与cfcheckpt各管一段 · 图 1

比特币的轻钱包要向全节点索要紧凑区块过滤器时,双方之间并不只有一问一答。除了大多数教程都会提到的 getblockfilter 这条本机查询线,节点与节点之间还有一整族专门服务过滤器同步的 P2P 消息:getcfilters 与 cfilter、getcfheaders 与 cfheaders、getcfcheckpt 与 cfcheckpt。这三对消息分工明确,理解了它们,才能看懂一个轻客户端是怎样在几分钟内把两万多个区块的过滤器账本核对完的。本文全部机制以比特币核心 v31.0 源码为准。

三对消息各管什么

第一对是最直观的:客户端发 getcfilters,带上过滤器类型、起始高度和终止区块哈希,对方对区间内每个区块回一条 cfilter,里面就是那个区块的原始过滤器。客户端发 getcfheaders,对方回一条 cfheaders,里面装的是这个区间内每个区块过滤器的哈希列表,外加区间前一个区块的过滤器头。第三对 getcfcheckpt 与 cfcheckpt 则是抽查兜底:节点每间隔 1000 个区块把过滤器头存一次检查点,客户端可以按这个节奏拿快照,快速跳到链的深处再往回对。

判断一个节点是否愿意提供这套服务,看的是服务位里的 NODE_COMPACT_FILTERS 这一位。对方没亮这一位,发这些消息就是白费流量。还要注意,服务端要能回答这些问题,前提是它自己开了过滤器索引:过滤器索引默认不建,没建的节点收到请求时只会记一条日志然后什么都不回。

轻钱包同步过滤器的三对P2P消息:getcfilters、getcfheaders与cfcheckpt各管一段 图 2
轻钱包同步过滤器的三对P2P消息:getcfilters、getcfheaders与cfcheckpt各管一段 · 图 2

两条请求各有一条尺寸红线

服务端对请求区间有硬性上限。getcfilters 一次最多要点 1000 个区块的过滤器,getcfheaders 一次最多点 2000 个。这些数字在 v31.0 源码的 net_processing.cpp 里是写死的常量。更值得留意的是超限的后果:如果起始高度大于终止高度,或者区间跨度超过上限,节点不是礼貌拒绝,而是直接断开这条连接,并在网络日志里留下对端发来的区间非法或索要过多的记录。协议把这种行为当成对异常对端的处置手段,因为漫无边际的过滤器请求本身就是一种资源消耗攻击面。

对运维来说,这解释了一个常见现象:自建节点日志里偶尔冒出与过滤器请求相关的断连记录,往往不是本机故障,而是某个实现对区间切分算错了,被这一刀切掉。

过滤器头是一条哈希链

cfheaders 里最容易被误读的是过滤器头这个字段。它不是某种元数据标签,而是一个把整条链焊起来的哈希:本区块过滤器数据的哈希,与上一个区块的过滤器头拼在一起再做一次双重 SHA-256。v31.0 源码 blockfilter.cpp 里的 ComputeHeader 就是这一行逻辑。这样每个过滤器头都隐式承诺了它之前所有过滤器没有被篡改过,轻客户端拿到一个可信的近期过滤器头,就能向前逐块验证整段过滤器历史的完整性,而不需要把每个过滤器都重算一遍再逐个比对。

这套设计与区块头靠默克尔根承诺交易是同一个思路的两次应用:一个承诺交易集合,一个承诺过滤器集合。有了这条哈希链,cfcheckpt 检查点才站得住:每隔 1000 块公布一个过滤器头,等于把长链切成一页页可以独立抽查的目录。

把三对消息串起来的一次同步

典型的过滤器同步是这样排布的:先问 cfheaders 拿一段区间的过滤器哈希与上一个头,自己逐块重算过滤器哈希接龙验证;区间太长或对不上时,用 getcfcheckpt 拿检查点快照校准坐标;确认过滤器可信后,再对感兴趣的区间发 getcfilters 取过滤器本体,筛出可能包含自己交易的区块去要完整区块。整个过程没有哪个环节要求全节点信任轻客户端,也没有哪个环节要求轻客户端无条件信任全节点,验证压力全部落在哈希链和重算上。

把这条链路走通,你就能回答那个经典问题:轻钱包凭什么不下载全链也敢信过滤器。答案不在某条消息里,而在三对消息共同支撑的哈希链结构里。

风险提示:本文只解释协议与节点行为,不构成任何投资建议;过滤器同步类操作的带宽与存储成本因节点配置而异,请以你所用软件版本的实际行为为准。