协议演进 / BIP/比特币改进提案 / BIP-37
BIP-37
让 SPV 客户端给每条连接设一个布隆过滤器,全节点只回命中的交易与 merkleblock,把轻客户端的带宽和内存负担降下来。本 BIP 目前处于已部署状态。
一、档案卡
| 编号 | BIP-37 |
|---|---|
| 标题 | Connection Bloom filtering |
| 作者 | Mike Hearn <[email protected]>, Matt Corallo <[email protected]> |
| 指定日期 | 2012-10-24 |
| 层级 | Peer Services(对等服务) |
| 类型 | Specification(规范类) |
| 状态 | Deployed(已部署) |
| 阅读版 | https://bips.dev/37 |
| 官方原文 | https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki |
二、放在哪一族
BIP-37 是 SPV(简化支付验证)轻客户端这一族的关键基础设施。在它之前,比特币白皮书设想的 SPV 模式缺了临门一脚:客户端只验证区块头链,却没有办法只拿与自己有关的交易。
它的服务宣告后来由 BIP-111(NODE_BLOOM 服务位)补齐。同族的后续路线还有 BIP-157 与 BIP-158:改由全节点为每个区块预先算好紧凑过滤器、客户端整块下载比对,隐私与信任假设与 BIP-37 不同。更早的 BIP-33(分层节点)想用专用服务节点解决同一问题,最终关闭。
读这一族要抓住对照:同样是“轻客户端怎样只拿自己的交易”,BIP-33 选外包查询,BIP-37 选把过滤器交给普通全节点,BIP-157 / BIP-158 又反过来让客户端本地过滤。
三、它要解决什么
随着比特币使用量增长,下载区块与交易广播所需的带宽不断上升。SPV 客户端不完整验证区块链,只检查区块头是否正确相连,并信任高难度链中的交易是有效的。
但当时的 SPV 客户端必须下载区块的全部内容和所有广播交易,再把绝大多数与自己钱包无关的交易扔掉。这拖慢同步、浪费用户带宽(手机流量往往按量计费)、增加内存占用——三样都在招致真实用户投诉,原文点名提到实现 SPV 模式的 Android “Bitcoin Wallet” 应用。
目标因此很具体:让远程节点在发送之前就扔掉无关交易,使链同步在老手机上也能快、省、跑得动。
为什么不用最直白的办法——把密钥清单直接上传给节点?原文给了两点理由:一是隐私,布隆过滤器是概率结构,误报率由客户端自己选,可以在精度与带宽之间权衡,让对方无法确切知道哪些交易属于你;二是性能,过滤器体积紧凑、成员测试快,满足需求的同时不易打开拒绝服务攻击的口子。
四、规则本身
4.1 三个新消息:filterload、filteradd、filterclear
握手完成后,客户端可以在每条连接上设置过滤器:
filterload:设置当前连接的布隆过滤器。字段包括过滤器本体(位域,最大 36000 字节)、哈希函数个数nHashFuncs(最大 50)、随机调节值nTweak,以及控制匹配后更新行为的标志nFlags;filteradd:把一个数据元素(不超过 520 字节)加入现有过滤器,免得钱包新增密钥或脚本时必须重发整个过滤器;filterclear:删除当前过滤器,回到 BIP-37 之前的常规行为。它没有参数。
没有 filterremove:布隆过滤器天生只能追加,元素一旦加入就无法单独移除,只能整体重建。
收到 filterload 后,远程节点立即把自己通告(inv)的广播交易限制为匹配过滤器的那些。
4.2 过滤区块:merkleblock
设过滤器后,节点还可以提供“过滤后的区块”,由新消息 merkleblock 承载:它由区块头、区块内交易总数、按深度优先序排列的部分默克尔树哈希与标志位组成。
客户端凭这棵部分默克尔树,可以提取命中交易的标识,并证明这些交易确实出现在已解出的区块里,从而确信对方没有喂给自己从未上链的假交易——原文同时提醒,对方仍可能靠“不告诉你”(lying by omission)隐瞒交易。
为此,协议还扩展了既有消息:version 增加 fRelay 字段,为 false 时节点在收到过滤器相关命令前不通告广播交易,免得客户端在握手完成到设好过滤器的空窗里被流量淹没;getdata 增加新类型 MSG_FILTERED_BLOCK(值 3),请求它且连接上已设过滤器时,对方回 merkleblock,并紧接着用独立的 tx 消息把命中的交易一并发出,省去一次往返。
4.3 匹配算法:什么算“命中”
判断一笔交易是否匹配过滤器,依次测试:一、交易自身的哈希;二、每个输出脚本中的每个数据元素(地址、公钥、P2SH 的脚本哈希等都由此覆盖);三、每个输入的输出点(outpoint)序列化;四、每个输入脚本中的每个数据元素。任一步命中即算匹配并中止。
测试输出点这一条是为了让客户端能发现“花掉自己某个输出”的交易,哪怕事先完全不知道那笔交易长什么样。由此还引出一个竞态:若不动态更新过滤器,客户端可能收到一笔进账、却漏掉紧接着花掉它的下一笔。因此过滤器在连接存续期间不是静态的。
4.4 过滤器更新标志
nFlags 控制命中输出时的自动更新行为:
BLOOM_UPDATE_NONE(0):命中也不调整过滤器;BLOOM_UPDATE_ALL(1):只要输出脚本的任一数据元素命中,就把该输出点序列化后插入过滤器;BLOOM_UPDATE_P2PUBKEY_ONLY(2):仅当命中的脚本是标准的 pay-to-pubkey 或多签形式时,才插入输出点。
区别在于控制过滤器变“脏”、误报率上升的速度。只收普通地址付款的钱包其实不需要自动更新;可能收到 pay-to-pubkey 等付款的才适合用第三档。客户端应监测实际误报率,定期换上干净的新过滤器。
4.5 过滤器格式
比特币使用 32 位 Murmur 哈希函数的第 3 版,用 nHashNum * 0xFBA4C795 + nTweak 派生出多个不同的哈希函数。原文给出按元素个数与目标误报率计算过滤器字节数和哈希函数个数的公式,并说明 36000 字节上限的来历:它对应 20000 个元素、误报率低于 0.1%,或 10000 个元素、误报率低于 0.0001% 的过滤器。
五、谁必须动
普通持币人:无直接变化。用全节点的用户完全不受影响。
SPV 钱包开发者与用户:要按钱包内容构造过滤器、选误报率、处理 merkleblock 与部分默克尔树的校验,并在钱包新增密钥时更新或重建过滤器。用更低的带宽和内存,换来对全节点“不隐瞒”的有限信任与可调节的隐私泄露。
全节点:若支持此功能,要实现三条过滤器消息、过滤匹配与 merkleblock 的构造,并承担为每个连接维护过滤器的计算成本;后来节点通过 NODE_BLOOM 服务位宣告自己是否提供此服务。
共识层:完全不动。过滤只发生在交易分发环节,区块与交易的合法性规则没有任何改变。
六、现在算不算数,以及一个常见误读
状态为 Deployed。BIP-37 定义的 filterload / filteradd / filterclear 与 merkleblock 曾让一代 SPV 钱包在手机上跑起来,是比特币轻客户端的第一次实用化。后来出于隐私与服务成本等考虑,生态又发展出 BIP-157 / BIP-158 的紧凑区块过滤路线,部分节点也不再默认提供布隆过滤服务——但这属于部署取舍,不改变 BIP-37 本身已部署的档案状态。
常见误读:
“布隆过滤器没命中的交易,就一定和我的钱包无关;命中的就一定是我的。”
后半句错,前半句才是布隆过滤器的性质。原文开篇就写明:布隆过滤器可以有假阳性(误报),但不会有假阴性(漏报)。也就是说,没命中可以放心排除,命中只表示“可能有关”,客户端仍须自己核对。而且误报率定得越准、带宽越省,能把交易与你的 IP 及彼此关联起来的观察者就看得越清——省流量与隐私之间没有免费的午餐。
七、延伸
- 阅读版:https://bips.dev/37
- 官方原文:https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
- 相关:BIP-111 NODE_BLOOM 服务位、BIP-157 客户端区块过滤、BIP-158 紧凑区块过滤器
- 仓库总目录:https://github.com/bitcoin/bips
