指南 · 第 6 部分,共 6 部分
包含性证书
你已经在 label 309 之下发布了一个 Merkle 根,以及支撑它的那份链下叶子列表。这个根本身是一个无可挑剔的承诺——可它并不是一样你能递给同事、附进合同、或提交到法庭卷宗里的东西。包含性证书正是为此而生的交付物:一份小巧、自包含的文件,它把一个或多个叶子钉定到那个已发布的根上,把重新推导出该根所需的每一个兄弟节点都嵌入其中,并指明用其区块时间见证整件事的那笔 Cardano 交易。任何人都可以离线验证它,针对任意浏览器,无需账号,也无需信任出具它的任何人。
如果你了解 OpenTimestamps,那么这就是同一个思路——一份可携带的存在性证明回执——只是带着两点刻意的不同。授时权威是 Cardano 区块链的区块时间,而不是某个日历服务器。而且这份证明的生成与验证完全在客户端进行:没有网关、没有出具方服务器,整个过程中也无需信任我们。它是 .ots 文件在存在性证明上的对应物,锚定在 Cardano 之上,可独立验证。
一份证书证明什么——又不证明什么
一份证书恰好给出两条密码学主张,每一条都可由任何人独立核验:
- 包含性。某个给定的
leaf位于一棵大小为tree_size、根为root的 RFC 9162 SHA-256 Merkle 树的第index个位置上。这一点通过从叶子、它的索引、树的大小,以及内嵌的兄弟路径重新算出根来加以证明。它是自包含的——单凭证书文件本身就足够了。 - 锚定性。那个
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 抓取、无需网关,也无需原发布者在场。
{
"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"
}
}有几处细节值得了解:
root、leaf以及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 会逐项重新跑一遍包含性证明,并打印出你仍需在链上确认的那个锚定:
# 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 取来叶子列表的字节,再把它们交进去;加密路径从不触网:
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 作为文件附件嵌入其中,因此它本身就是那个可被机器验证的交付物——而不只是它的一张图片。这一切都不会触及任何私有服务器。
端到端的验证算法
要独立验证一份证书——用你自己的代码,不借助我们提供的任何工具——就照这样做:
- **拒绝格式错误的字段。**每一个
root/leaf/proof[]项都必须是偶数长度、且能解码出 32 字节的十六进制;tree_size与每一个index都必须是安全整数,并满足index < tree_size以及1 ≤ tree_size ≤ 2³² − 1。 - **重新算出每一项的根。**依据 RFC 9162 §2.1.3.2,把叶子(
leaf = SHA-256(0x00 ‖ leaf_digest))与它的兄弟路径(node = SHA-256(0x01 ‖ L ‖ R))层层折叠,在严格小于当前子树规模的最大二次幂处切分,再把结果与merkle.root逐字节比对。单叶子的树有一份空证明。 - **在链上确认锚定。**在任意公开 Cardano 浏览器上抓取
anchor.tx_hash,读取它的 label 309 元数据,并确认merkle.root等于该记录的merkle[].root。你在那里读到的区块时间,就是这份证书所断言的时间戳。
第 1–2 步是那个自包含的部分——它们从不离开你的机器。第 3 步是唯一一次网络读取,而它去往的是你自己选定的浏览器,绝不去往证书的出具者。
一份文件,永久可验证
正因为路径上的每一个兄弟节点都被内嵌其中,所以即便叶子列表、原来的网关或出具者早已不在,一份证书依旧能继续验证。那两项核验——从文件重新算出根、在链上确认这个根——就是任何人永远所需的全部。关于这个根和叶子列表最初是怎么构建出来的,参见用 Merkle 批量锚定;关于链上一侧这道核验所嵌入的完整验证器模型,参见验证。