轻客户端用布隆过滤器在区块里找自己的收款时,节点回给它的不是整个区块,而是一种叫 merkleblock 的特殊消息:区块里只挑出命中交易的成员证明,其余交易一笔不带。它的构造与解读代码就在 Bitcoin Core v31.0 源码树 src/merkleblock.h 与 src/merkleblock.cpp,注释写得比多数论文清楚。
消息在协议里的位置
merkleblock 是对 getdata 请求的响应——当请求方在清单条目里使用 MSG_MERKLEBLOCK 类型时,节点把区块里匹配过滤器的那部分打包成消息返回,配套的还有含匹配交易的 block 消息。doc/bips.md 的账簿记着这套机制的来路:BIP 37 的布隆过滤与部分默克尔树自 v0.8.0 实现(协议版本升到 70001),并特别标注自 v0.19.0 起默认关闭、需 -peerbloomfilters 显式开启。今天它仍是完整参考实现的一部分,但默认配置下不会有节点再走这条路。

部分默克尔树的编码
头文件注释从头描述了编码协议:对默克尔树做深度优先遍历,每个节点写一个比特,表示它是否是某个命中叶子交易的祖先(或自身命中);处在叶子层、或该比特为 0 时,写入这个节点的哈希并停止下钻;比特为 1 则不写哈希,递归进入两个(或仅剩的一个)子分支。解码方执行完全相同的遍历,按序消耗比特和哈希就能重建树。
这套编码带一个硬尺寸保证,注释里的公式是:SIZE 不超过 10 加上 32.25 乘以 N 的上取整,N 是部分树的叶子数。N 本身又被两条界夹住:不超过区块交易总数,也不超过 1 加命中数乘树高。第二条界是防攻击的关键——如果有人构造一个”命中一笔却要求遍历全树”的病态过滤器,尺寸界会立刻爆炸,节点据此可以判定数据非法(结构里那个 fBad 标志位就是为这种场合准备的)。序列化字段依次是:4 字节的交易总数、变长的比特向量、变长的哈希向量,解码器对长度做严格校验。
一个容易忽略的细节:coinbase 永远在场
merkleblock.cpp 里 CalcHash 的注释有一行断言值得逐字看:默克尔块里永远不可能有零笔交易,coinbase 交易永远需要。若断言不成立,对交易数组的索引会踩到非法内存。为什么 coinbase 必然命中?因为布隆过滤请求关联的是”区块里有没有我要的东西”,而验证部分默克尔树根哈希时必须能重建从命中叶子到根的全部中间哈希,coinbase 在树根路径的重建里几乎总是被牵连;实现干脆用断言把”至少含 coinbase”当成结构不变量。对读代码的人来说,这种”协议逻辑推出来、用 assert 钉死”的位置是理解设计意图的富矿。
重建与验证的分工
提取流程在头文件的 TraverseAndExtract 声明里:递归消耗比特与哈希,返回各节点哈希与索引,最终吐出命中的 txid 列表。轻客户端拿到消息后的动作分两步:先用消息里的哈希重算默克尔根,和本地层已验证的区块头比对——这一步锚定的是”这些证明属于那条链”;再检查命中的交易确实出现在随消息附带的 block 交易里——这一步锚定”证明与实体一致”。两把锁都锁上,才能把命中的输出记进钱包。它与紧凑区块过滤器(BIP 157)解决同一个问题但立场相反:merkleblock 把过滤器交给全节点,轻客户端省下载但暴露地址;紧凑过滤器把区块头压缩过滤器拉到本地试,隐私反过来用带宽换。今天主流轻钱包走的是后者,merkleblock 更多出现在老协议文档与兼容代码里。
风险提示:本文为协议机制说明,钱包选型请以当前主流实现的实际配置为准;本文不构成投资建议。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。