省字节的整数写法:BigSize与闪电网络的可扩展字段 图 1
省字节的整数写法:BigSize与闪电网络的可扩展字段 · 图 1

闪电网络的消息规范里有一种存在感极低、出场率极高的编码:BigSize。一条闪电消息的开头是两字节类型号,正文按 TLV 结构组织,而每个 TLV 字段的编号和长度都用 BigSize 写。它的任务只有一个——把整数写得尽量短,同时给未来的新字段留出插槽。这份规格抄自比特币 P2P 协议里的 CompactSize,只改了一个方向:多字节部分从 little-endian 换成了 big-endian。

一字节能写完就不用两字节

BigSize 是一组分段的映射,规范的写法是:

uint8(x)                小于 0xfd 时
0xfd + be16(uint16(x))  小于 0x10000 时
0xfe + be32(uint32(x))  小于 0x100000000 时
0xff + be64(x)          其余情况

其中加号代表拼接,be16 之类表示按大端摆放的无符号整数。数值小于 0xFD 时直接占一字节;小于两的十六次方时用一字节前缀 0xFD 加两字节大端整数,共三字节;小于两的三十二次方时前缀 0xFE 加四字节,共五字节;更大的数用 0xFF 加八字节大端整数,共九字节。一个 HTLC 的毫秒数、一条短通道编号、一个时间戳,大多落在一到五字节的区间里。对动辄要整个塞进洋葱包载荷的闪电消息来说,每个字段省下几字节的意义是能把路由信息多塞几层。

规范还有一个硬性要求:必须最小编码。一个值如果小于 0x10000,却用了五字节的写法,即使能解码也算不合规,接收方应当把非最小编码视为错误并断开。这条规定的目的不是省流量,而是唯一化——同一个值只允许有一种合法字节写法。

省字节的整数写法:BigSize与闪电网络的可扩展字段 图 2
省字节的整数写法:BigSize与闪电网络的可扩展字段 · 图 2

TLV:先报编号再报长度

闪电消息正文里的可选字段全部装进 TLV 流:先是一个 BigSize 编码的 type,再是一个 BigSize 编码的 length,后面紧跟 length 字节的 value。type 在两万以下由规范保留,两万及以上留给各家实现的自定义扩展。发送方必须让 type 严格递增、不得重复;接收方遇到不认识的 type,按规范直接跳过而不是报错。这就是闪电能在不破坏旧节点兼容的前提下陆续加入新特性的机械原理:老软件不认识新编号,跳过就好。

唯一化在这里重新登场。闪电协议大量对消息内容做签名,签名方和解签方必须能对同一份逻辑内容算出完全相同的字节串。TLV 的顺序、编码宽度如果各写各的,验签就会随机失败。所以规范要求 type 递增、长度和类型都取最短形式,让编码成为内容的规范表示。

和大端小端的爱恨情仇

比特币节点之间传输的数字几乎都按 little-endian 摆放,CompactSize 的多字节部分也是小端。BigSize 特意翻转成 big-endian,规范附录为此准备了成套测试向量,专门验证实现没有惯性抄错方向。另一个易错点是编号冲突:闪电的 ping 消息、通道消息类型号用固定数字表排列,而 TLV 的 type 空间是另一套坐标系,两者互不相干;读实现源码时把它们混为一谈是新手常见事故。

一条判断线

想快速检验某个字段该不该进 TLV,问三句:它是不是可选的?它上线时有多少老实现还没升级?它是否参与签名?三句全中,就必须走 TLV 加最小编码这条路;反之如果字段在消息一开始就定死了位置,老老实实按固定偏移写反而少一层解析歧义。闪电近年的新特性几乎都长在这条判断线上,读规范时把 TLV 当默认答案、把固定字段当历史遗产,基本不会猜错方向。

快速问答

问:为什么闪电不直接用定长六十四位整数,省事?

答:闪电消息经常要塞进容量有限的路由洋葱里,每个字段定长八字节会白白吃掉载荷空间,BigSize 的按需伸展就是为此服务的。

问:BigSize 能表示负数吗?

答:不能,它是无符号整数编码。闪电规范里需要负数的场景用的是另一套有符号整数写法,附录另有测试向量。

常见误区

一是把收到未知 type 当协议错误断线,规范要求的是跳过;二是解码时不做最小性检查,让非规范编码混进后续签名计算;三是以为 TLV 可以随便插在最前面,实际上扩展字段必须排在所有已定义固定字段之后,否则老解析器会读错位。

风险提示:本文为协议编码科普,不构成任何投资建议;实现细节请以所用客户端的文档为准。