内容与哈希
Label 309 如何把一条记录绑定到它的内容 —— hashes 映射的结构、摘要究竟承诺了什么,以及把大量条目锚定在同一个根下的 Merkle 承诺。
内容哈希就是那个断言。一条 Label 309 记录关于存在性的一切主张,都源自对内容字节的一个密码学摘要,并以元数据标签 309 锚定在链上。本页定义这个摘要如何被携带、它究竟承诺了什么,以及一个 32 字节的根如何代表任意大的一批条目。
hashes 映射
记录中的每个条目都携带一个 hashes 映射:一个从算法标识符到 32 字节原始摘要的 CBOR 映射。
hashes = {
"sha2-256": h'…32 bytes…', ; key = algorithm id, value = raw digest
}键是取自哈希注册表的文本字符串标识符;值是原始字节串,绝不进行十六进制编码。该映射 MUST 至少包含一个条目,而且每一种已注册的哈希算法都恰好产生 32 字节:
| 标识符 | 算法 | 参考 | 摘要 |
|---|---|---|---|
sha2-256 | SHA-256 | FIPS 180-4 | 32 B |
blake2b-256 | BLAKE2b-256 | RFC 7693 | 32 B |
这两个标识符都是验证器必须实现的,因此一条只用其中任意一个的单哈希记录在任何地方都能通过校验。验证器遇到无法识别的标识符时,会以一个稳定的错误码拒绝该记录,而不是悄悄跳过这个条目。完整的注册表,包括预留的后量子槽位,参见算法注册表。
使用 CBOR 映射,而不是并列数组或一组 {alg, digest} 子对象,会带来三个属于线格式约定的后果。算法重复在构造上就不可能出现,因为 CBOR 映射的键是唯一的。规范排序是自动的,因为规范 CBOR 按键的编码字节排序,于是表达同一哈希集合的两个生产方会发出字节完全一致的映射,对其施加的任何记录级签名也都是稳定的。而且这种形态不需要逐条目校验:结构校验器只检查每个键是否已注册、每个值是否具有对应算法的摘要长度。
哈希承诺了什么
摘要承诺的是内容字节 —— 生产方正在为之加盖时间戳的那串精确字节序列。hashes 映射中的每个条目都必须是这同一串字节序列在其指定算法下的摘要;如果一条记录的各条目描述的是不同的明文,则它不合规。当验证器能够拿到内容字节时,它 MUST 重新计算每一个摘要,只要有任何一个不匹配就拒绝该记录。
当一条记录携带加密信封(enc)时,哈希绑定的是明文,绝不是密文。这是有意为之:存在性证明(PoE)之所以存在,是为了让作者日后能够揭示明文,并证明它在某个给定时间已经存在。对密文做哈希只能证明某个加密块曾经存在,对底层内容则什么都说明不了。所以一条密封记录仍然能精确证明被加盖时间戳的是哪段明文 —— 接收方解密,重新计算明文摘要,再把它们与链上承诺逐一比对。因此一个携带 enc 的条目必须至少携带一个内容哈希条目;否则就没有可供重新计算来比对的明文主张了。
即便密封,绑定的仍是明文
一条密封记录的链上摘要是明文的摘要。密文本身存放在一个内容寻址的 ar:// 或 ipfs:// URI
处,因此存储网关返回的字节相对于那个地址是可验篡改的,无需信任网关;接收方解密后重新计算明文哈希,从而把这一环重新闭合回链上的主张。
一个哈希,还是多个
单个内容哈希就完全合规。对于注册表中所有 256 位哈希,已知最优的第二原像攻击在经典计算下都处于 2^256 或接近这个量级 —— 一个可靠的 256 位哈希就已经覆盖了一条记录在其归档生命周期内的现实威胁模型,结构校验器也不会对单条目记录发出任何告警。
生产方 MAY 从一个独立的设计家族再添加第二个条目,作为可选的纵深防御 —— 把 sha2-256(SHA-2:Merkle–Damgård 结构)与 blake2b-256(BLAKE2:基于一个 ChaCha 派生置换的 HAIFA 构造)搭配起来。由于这两个家族在结构上毫无血缘关系,一条同时携带两者的记录只有在两个家族同时被密码分析攻破时才会被削弱。代价是每个条目多出一个 32 字节摘要外加它那个简短的标识符;要不要这么做由生产方决定,且从不强制。
Merkle 批量承诺
单个内容哈希只能锚定单项内容。要锚定一个任意大的集合 —— 一套 500 个文件的 CI 构件集、一条 IoT 事件流、一批审计日志 —— Label 309 定义了一个顶层 merkle[] 数组。每个条目都承诺一个有序的 32 字节叶子列表,并在链上发布一个 32 字节的根;有序的叶子本身存放在链下。
merkle = [
{
"alg": "rfc9162-sha256",
"root": h'…32 bytes…', ; canonical root over the ordered leaves
"leaf_count": 4, ; binds the on-chain root to the leaf-list size
"uris": [ … ], ; OPTIONAL — where the off-chain leaves list lives
},
]已注册的承诺算法是 rfc9162-sha256:即 RFC 9162 §2.1.1 的 Merkle Tree Hash,以 SHA-256 作为底层哈希。它是一种列表承诺构造,与内容哈希注册表不同 —— 一个 Merkle 根承诺的是一个叶子列表结构,而一个 sha2-256 摘要承诺的是明文字节 —— 因此它独占一个数组,而不是放在 hashes 里。链上的 leaf_count 把根绑定到链下列表的大小,从而封堵了这样一种替换:重建一棵规模不同、却在某个叶子位置上共享同一个根的树。
树的构造
这种构造用一个单字节的域分隔前缀来区分叶子节点与内部节点 —— 叶子用 0x00,内部节点用 0x01 —— 这样攻击者就无法构造出一个与某个叶子相碰撞的内部节点。对于一个由 32 字节值组成、且 n ≥ 1 的有序列表 L = (d_0, …, d_{n-1}),Merkle Tree Hash 递归定义如下:
MTH(L) = SHA-256(0x00 || d_0) when n == 1
MTH(L) = SHA-256(0x01 || MTH(L[0:k]) || MTH(L[k:n])) when n > 1
where k is the largest power of 2 strictly less than n一个关键的后果:单个叶子哈希为 SHA-256(0x00 || d_0),而不是那个裸叶子本身。因此单叶子树的根永远不等于该叶子本身。想为单项内容加盖时间戳的生产方 MUST 直接使用一个普通的 sha2-256 或 blake2b-256 条目,而不是用单叶子 Merkle 树。空树(n == 0)是被禁止的。
这种构造对顺序敏感 —— 重排叶子会得到不同的根 —— 所以生产方必须把叶子列表当作一个有序序列,并在发布、归档以及日后任何证明生成过程中保持这个顺序。
链下的叶子列表
没有叶子列表,根就毫无用处,所以生产方会把有序的叶子持久化在链下。规范的产物是一份 cardano-poe-merkle-leaves-v1 文档,以规范 CBOR(RFC 8949)编码:一个 32 字节的根、一个有序的 32 字节叶子数组,以及叶子数量。
leaves-list = {
"format": "cardano-poe-merkle-leaves-v1",
"tree_alg": tstr, ; registered list-commitment algorithm id
"root": bytes .size 32, ; raw 32 bytes, not hex
"leaves": [ + bytes .size 32 ], ; ordered raw 32-byte leaves
"leaf_count": 1..4294967295, ; 1 .. 2^32-1; MUST equal the length of `leaves`
? "leaf_alg": tstr, ; informative; no verification semantics
}验证器解析链下列表,用上文的构造从它的 leaves 重新计算根,再与链上的 merkle[i].root 逐字节比对;文件中的 leaf_count 必须同时等于链上的 leaf_count 和 len(leaves)。这个规范 CBOR 容器是 唯一 的规范性叶子列表形式 —— 不存在任何 JSON 投影或其他序列化方式,因此交换叶子列表的两个实现,交换的始终是可逐字节比对的文档。
包含性证明
批量化的意义在于选择性披露:证明某一条目曾经在已承诺的列表中,而无需重新发布 —— 甚至无需揭示 —— 其余部分。一个叶子的包含性证明,是从该叶子到根这条路径上各兄弟节点哈希的有序列表:一条 O(log n) 的兄弟路径。验证器按 RFC 9162 把该叶子与这些兄弟节点逐级向上折叠,当且仅当重建出的根与已发布的根逐字节相等时才接受该证明。
由于 RFC 9162 的树不会被填充到 2 的幂,一棵不平衡树里靠右边缘的叶子,其路径可能比满侧叶子的路径更短。因此权威的判定是算法性的 —— 这次折叠是否复现了根 —— 而绝不是去比较证明的长度。
为什么批量化很重要
一笔交易和一个 32 字节的根,就能代表成千上万乃至数百万个叶子。任何持有 O(log n)
证明的人,日后都能出示「这一条目在我的列表里」,而每一个未披露的叶子都保持私密 ——
根对它所承诺的那些叶子不泄露任何信息。