指南

指南 · 第 6 部分,共 6 部分

包含性证书

你已经在 label 309 之下发布了一个 Merkle 根,以及支撑它的那份链下叶子列表。这个根本身是一个无可挑剔的承诺——可它并不是一样你能递给同事、附进合同、或提交到法庭卷宗里的东西。包含性证书正是为此而生的交付物:一份小巧、自包含的文件,它把一个或多个叶子钉定到那个已发布的根上,把重新推导出该根所需的每一个兄弟节点都嵌入其中,并指明用其区块时间见证整件事的那笔 Cardano 交易。任何人都可以离线验证它,针对任意浏览器,无需账号,也无需信任出具它的任何人。

如果你了解 OpenTimestamps,那么这就是同一个思路——一份可携带的存在性证明回执——只是带着两点刻意的不同。授时权威是 Cardano 区块链的区块时间,而不是某个日历服务器。而且这份证明的生成与验证完全在客户端进行:没有网关、没有出具方服务器,整个过程中也无需信任我们。它是 .ots 文件在存在性证明上的对应物,锚定在 Cardano 之上,可独立验证。

一份证书证明什么——又不证明什么

一份证书恰好给出两条密码学主张,每一条都可由任何人独立核验:

  1. 包含性。某个给定的 leaf 位于一棵大小为 tree_size、根为 rootRFC 9162 SHA-256 Merkle 树的第 index 个位置上。这一点通过从叶子、它的索引、树的大小,以及内嵌的兄弟路径重新算出根来加以证明。它是自包含的——单凭证书文件本身就足够了。
  2. 锚定性。那个 root 原封不动地出现在交易 tx_hash 所携带的 label 309 记录的 merkle[].root 字段里。这一点通过在任意公开 Cardano 浏览器上读取那笔交易、并逐字节比对来加以证明。它需要的是证书文件再加上一个浏览器——别无他物。

两者合在一起,便证明了该叶子的内容在 tx_hash 的区块时间或更早之前就已存在。

一份证书不证明什么

时间是由公开区块链断言的,并未以密码学方式绑入证明之内——这与 OpenTimestamps 和 Chainpoint 完全一样,你信任的是链的区块时间,而绝非证书的出具者。一份证书并非 eIDAS 意义上的“合格”电子时间戳 (RFC 3161 / 合格 TSA);它是一份区块链锚定的时间戳——一项对时间主张的有力佐证,与其他区块链时间戳同属一类,文件中的措辞也正是这样写的。它对于内容由谁创作只字不提(那是一个可选的记录签名——参见签名),它也不证明该内容此前不曾被人知晓。它证明的是“在某个期限之前已存在”,而非作者归属,也非首创性。

为什么这份证明不带签名

一份严格的 IETF COSE Receipt 是一个 COSE_Sign1,其载荷为 Merkle 根,并由某个权威签名。而这里的权威是区块链,并非我们持有的某把密钥——所以用我们的密钥去给根签名,会重新引入服务器信任,并破坏整个标准赖以立足的可独立验证特性。因此,这份证书改为完全按规范所述发出 IETF 包含性证明的 CBOR 结构,并以区块链锚定取代签名的位置。证明的数学与 IETF 编码逐字节一致;它是经过深思熟虑、不带签名、由区块链锚定的。

JSON 证书

主交付物是 label-309-inclusion-certificate-v1:一份人类可读、机器也可读的 JSON 文件。一份文件可覆盖一个或多个叶子,而每一项都内嵌了它完整的兄弟路径,因此这份文件可以永久重新验证——无需 Arweave 抓取、无需网关,也无需原发布者在场。

contract.cert.json
{
  "format": "label-309-inclusion-certificate-v1",
  "generated_at": "2026-06-16T12:00:00.000Z",   // informational only, never trusted
  "anchor": {
    "chain": "cardano",
    "network": "mainnet",
    "tx_hash": "…64hex…",
    "metadata_label": 309,
    "block_time": 1781611200,                    // POSIX seconds — explorer-asserted
    "block_time_iso": "2026-06-16T12:00:00.000Z",
    "block_height": 12345678,                    // optional; explorer-asserted
    "explorer_urls": [
      "https://cardanoscan.io/transaction/…",
      "https://adastat.net/transactions/…"
    ]
  },
  "merkle": {
    "tree_alg": "rfc9162-sha256",
    "root": "…64hex…",
    "tree_size": 1024,                           // === the on-chain leaf_count
    "leaves_list_uri": "ar://<txid>"             // optional source reference
  },
  "items": [
    {
      "leaf": "…64hex…",                         // the content hash committed as a leaf
      "leaf_alg": "sha2-256",                    // how to hash a file to reproduce `leaf`
      "index": 42,
      "proof": ["…64hex…", "…64hex…"],           // siblings, leaf→root; [] for a single-leaf tree
      "verified": true,                          // proof recomputes to merkle.root at build time
      "label": "contract.pdf"                    // optional note/filename
    }
  ],
  "claim": "Each listed hash was included in a Merkle tree whose root was published on the Cardano blockchain in the referenced transaction under metadata label 309; therefore each hash provably existed on or before the stated block time.",
  "verification": {
    "method": "RFC 9162 (Certificate Transparency) SHA-256 inclusion proof. For each item, recompute the Merkle root from leaf+index+tree_size+proof and compare to merkle.root; then confirm merkle.root equals the merkle[].root in the label 309 record of anchor.tx_hash on any public Cardano explorer.",
    "requires_trust_in_cardanowall": false,
    "time_asserted_by": "Cardano blockchain (block time), via public explorers"
  }
}

有几处细节值得了解:

  • rootleaf 以及 proof[] 的每一项,都是渲染为十六进制的原始 32 字节值。生产端输出小写;验证端两种大小写皆可接受,并拒绝任何非十六进制字符或长度为奇数的字符串。
  • 文件中存下的 "verified": true 是生产端在构建时的结果。验证端从不信任它——它会自己重新计算证明,并报告自己的结论。一个未能在树中找到的叶子,会被记为 "verified": false 并附上一个 "error" 字段,绝不会被悄无声息地丢弃,这样文件对于“漏项”一事是诚实的。
  • block_time 是包含此交易的那个区块、由浏览器断言的 POSIX 时间戳。block_time_iso 是它的 UTC 呈现形式——仅为方便而已。

CBOR 包含性证明(与 COSE / RFC 9162 对齐)

在 JSON 之外,每一项还可以导出为一个紧凑的 .cbor 交付物,其证明结构与 draft-ietf-cose-merkle-tree-proofs 逐字节一致。这意味着任何符合 RFC 9162 / COSE 的可验证数据结构验证器,都能直接读取这份证明的数学部分——互操作的内核是标准的,而非自创的。它不携带绝对区块时间,也不携带任何法律文字(那些都在 JSON 里);它只是可携带的证明核心,并且出于上文所述的原因,它由区块链锚定且不带签名。

那份精简的 IETF 包含性证明——bstr .cbor [tree_size, leaf_index, inclusion_path],其可验证数据结构(vds)值对应 RFC 9162 SHA-256 时为 1——可以单独提取出来,供一个纯 COSE 验证器使用;Cardano 锚定则作为一个小的映射,紧挨在它旁边,取代 Sign1 签名的位置。

用工具链构建与验证

证书格式是公开、与网关无关的工具链的一部分:@cardanowall/sdk-ts SDK(连同 Python 与 Rust 的逐字节孪生实现)、cardanowall CLI,以及各应用中的验证界面。构建与验证的数学是纯粹且离线的——只有获取新鲜的链上事实那一步才会触网。

用 CLI

certificate build 接收叶子列表、目标(原始叶子十六进制,或要哈希的文件),以及它据以解析锚定事实的那笔交易。certificate verify 会逐项重新跑一遍包含性证明,并打印出你仍需在链上确认的那个锚定:

terminal
# Build a certificate for two files against a published root.
cardanowall certificate build \
  --leaves-list leaves.cbor \
  --tx <tx-hash> \
  --file contract.pdf --file exhibit-a.png \
  --out contract.cert.json

# Re-verify the proofs offline — no network, no trust in the producer.
cardanowall certificate verify contract.cert.json

退出码 0 表示每一项的证明都重新算回了根;非零的退出码则标示出某处包含性失败、输入有误,或发生 IO 错误——所以它能直接接入 CI。每一单项也可以被提取成规范的 { tree_alg, tree_size, index, leaf, proof[] } 形态,再用 cardanowall merkle verify 加以核验。

用 TypeScript SDK

certificate API 是纯粹的——你用平台自带的 fetch 取来叶子列表的字节,再把它们交进去;加密路径从不触网:

build-and-verify.ts
import { certificate, merkle } from '@cardanowall/sdk-ts';

// `leaves` comes from decodeLeavesList(...) over the fetched leaves-list bytes.
const cert = certificate.buildInclusionCertificate({
  anchor,                       // chain facts resolved from the tx
  merkle: { treeAlg: 'rfc9162-sha256', root, treeSize: leaves.length },
  leaves,
  targets: [{ leaf, leafAlg: 'sha2-256', label: 'contract.pdf' }],
});

// Pure re-verification from the certificate alone — no Arweave, no chain.
const result = certificate.verifyInclusionCertificate(cert);
console.log(result.ok);          // true when every item's proof recomputes to root
console.log(result.anchorClaim); // the anchor you confirm on a public explorer

// Each item also re-verifies through the plain Merkle predicate.
const item = cert.items[0];
const ok = merkle.merkleSha2256VerifyInclusion(
  leafBytes, item.index, cert.merkle.tree_size, proofBytes, rootBytes,
);

verifyInclusionCertificate 报告证明的结论,并回显所声称的那个锚定;在链上确认那个锚定,是你另外的、明确的一步——结果对象里就是这么说的。同一个模块在 Python(certificate)与 Rust(certificate)SDK 中被逐字节地镜像实现。

在浏览器里,于一个交易页面上

一条携带了 merkle[] 承诺的 label 309 记录,会在它的交易页面上显示一个包含性面板。粘贴一个或多个十六进制哈希,或拖入原始文件以在客户端把它们哈希;该页面会在你的浏览器里直接从内容寻址存储中抓取叶子列表,重新算出每一份证明,逐项给出绿/红的结论,并提供 JSON、CBOR 以及一份可打印的 PDF 供下载。这份 PDF 把完整的 JSON 作为文件附件嵌入其中,因此它本身就是那个可被机器验证的交付物——而不只是它的一张图片。这一切都不会触及任何私有服务器。

端到端的验证算法

要独立验证一份证书——用你自己的代码,不借助我们提供的任何工具——就照这样做:

  1. **拒绝格式错误的字段。**每一个 root/leaf/proof[] 项都必须是偶数长度、且能解码出 32 字节的十六进制;tree_size 与每一个 index 都必须是安全整数,并满足 index < tree_size 以及 1 ≤ tree_size ≤ 2³² − 1
  2. **重新算出每一项的根。**依据 RFC 9162 §2.1.3.2,把叶子(leaf = SHA-256(0x00 ‖ leaf_digest))与它的兄弟路径(node = SHA-256(0x01 ‖ L ‖ R))层层折叠,在严格小于当前子树规模的最大二次幂处切分,再把结果与 merkle.root 逐字节比对。单叶子的树有一份空证明。
  3. **在链上确认锚定。**在任意公开 Cardano 浏览器上抓取 anchor.tx_hash,读取它的 label 309 元数据,并确认 merkle.root 等于该记录的 merkle[].root。你在那里读到的区块时间,就是这份证书所断言的时间戳。

第 1–2 步是那个自包含的部分——它们从不离开你的机器。第 3 步是唯一一次网络读取,而它去往的是你自己选定的浏览器,绝不去往证书的出具者。

一份文件,永久可验证

正因为路径上的每一个兄弟节点都被内嵌其中,所以即便叶子列表、原来的网关或出具者早已不在,一份证书依旧能继续验证。那两项核验——从文件重新算出根、在链上确认这个根——就是任何人永远所需的全部。关于这个根和叶子列表最初是怎么构建出来的,参见用 Merkle 批量锚定;关于链上一侧这道核验所嵌入的完整验证器模型,参见验证