同一种数据换壳不换树:EIP-8016给SSZ加一个兼容并集类型 图 1
同一种数据换壳不换树:EIP-8016给SSZ加一个兼容并集类型 · 图 1

标签联合的树位漂移问题

SSZ 是以太坊共识层的序列化格式:每种数据结构都被摊平成一棵默克尔树,字段在树上的位置用广义索引(gindex)寻址,轻客户端和跨协议验证器靠一条默克尔证明就能核对某个字段的值。问题出在联合类型:以太坊里像交易这类对象天然有多个变体(不同 EIP-2718 类型、不同功能子集),传统做法是标签联合——一个判别值加一个联合体。麻烦在于不同变体内部字段数、字段序不同,同一个逻辑字段(比如 nonce 或签名)在不同变体的树形状里落点不同,gindex 随变体漂移。验证器要为每个版本维护一张字段到树位的映射表,版本一多,维护负担和出错面都在涨。

EIP-8016(2025年8月起草,草案状态)给出一个约束更强的新类型:CompatibleUnion,规定能进联合的分支必须互相同意——同一字段在所有分支里永远是同一个 gindex。

记法与规则

类型写作 CompatibleUnion({selector: type}),花括号里是判别值到分支类型的映射,例如 CompatibleUnion({1: Square, 2: Circle})。能被选进来的分支类型不是随便挑的:各分支的默克尔化必须提供稳定的 gindex 指派,也就是说公共字段在各分支容器里排同一位置、复合字段的子树形状一致。默克尔化时,联合的树在判别值之外复用同一棵载荷子树——分支间共用的字段天然共享证明路径,一条针对公共字段的证明跨所有变体有效,验证器不必关心当前活跃的是哪个分支。

两个细节容易被忽视。其一,CompatibleUnion 永远按变长类型处理,哪怕所有分支恰好等长——一致性优先于个别情形的紧凑编码。其二,默认值与选择器合法域都有明确定义,空联合、非法选择器等边界在规范里逐条钉死,避免实现间分歧。类型本身不进执行层,服务对象是共识层与未来的 SSZ 化交易结构。

谁会掏这个钱

约束不是免费的:分支要维持 gindex 对齐,等于给协议设计者的字段排布加了跨版本兼容合约,重构容器内部顺序的成本变高。掏钱的场景主要有两类。一是多版本共存的验证:同一份规范里交易容器有好几代形态,轻客户端用一套映射读全部版本,升级不再逼着验证软件同步换表。二是选择性披露与跨变体证明:验证器从根出发对公共字段取证,证明不暴露其余字段也不依赖具体变体,这类需求在隐私增强与互操作桥里越来越常见。反过来,若两个候选分支的公共字段位置永远对不齐,那就干脆不该塞进同一个 CompatibleUnion——错误配置在类型校验期就报错,把树位漂移这种运行期噩梦提前到编译期。

一条对照直觉

拿表格类比:普通联合像每个部门用各自版式的表格,汇总系统要为每张表写一份列位说明书;兼容联合则规定所有部门表格的第一列永远是工号、第二列永远是姓名,版式细节可以不同,但要查工号,全公司一套坐标。汇总系统(验证器)从此只记一个位置。代价也直白——想让销售部把工号挪到第三列?不行,那会砸掉所有跨表查询,这正是 gindex 稳定性约束的用意。

一次证明的旅程

跟随一条轻客户端请求走到底。它想核对某笔交易的金额字段:传统方案里,桥必须先知道这笔交易是哪个变体,从版本映射表里查出金额在该变体树里的广义索引,才能生成证明路径——表错一格,证明就指向错误叶子。兼容联合之下,桥直接按公共约定的 gindex 生成路径,不管载荷是第几代交易容器,金额永远长在同一树枝;轻客户端拿根哈希加路径验证,连判别值都不必看。多版本共存的升级过渡期最见价值:旧容器还没退役、新容器已经上线,同一套验证代码读新旧两代数据,不必维护分叉的映射表,也不必给每个版本单独写审计逻辑。类型层面的约束把这类错误从生产事故清单挪进了编译期报错清单。

快速问答

问:这和 Protobuf 的 oneof 什么关系?概念近邻,但 SSZ 关心的是默克尔树形状一致性,比字节编码兼容性更严。问:现在链上用了吗?仍是草案,共识层容器尚未采用,价值要看未来交易 SSZ 化的最终结构。问:分支加字段会破坏兼容吗?只要公共字段树位不变即兼容,这正是设计验收标准。

风险提示:本文仅解读协议草案,不构成投资建议;类型语义以官方规范文本为准。