qub 协议规范

qub 是一个用于加密时间承诺的协议:一个将信息封存到未来日期的系统,并在以后精确验证被封存的内容,其释放由 drand 回合控制,并且——当存储事务或透明日志证明可用时——提供一个关于密文提交时间的独立时间戳上限。

三个原语让它工作。 随机数生成器 是一个去中心化的随机信标——揭示日期是通过密码学强制执行的,而不是依赖 qub 的善意。 耐用存储 保留已确认的封存字节,而当前的发布路径安排单独的永久存储事务;成功的一般上传日志附加还可以加入批量的、有锚定的承诺。 ML-DSA-65 是一种后量子数字签名——当启用作者身份时,qub 与一个密钥对绑定,该密钥对的私钥从不离开作者的设备。

这些原语共同构成了一种声明,该声明具有时间锁定和防篡改特性,可选择地具名,并可独立加时间戳——这是一种随着世界伪造过去能力提升而其价值增长的收据。

本文档的其余部分是实现互操作所需的规范性规范。


qub 协议规范

字段 价值
文件发布 1.0.0 (protocol-v1.0.0)
线协议 0x01
外包装 0x01
生效日期 2026年9月23日
状态 当前
已审核 2026年9月23日

本文档是 qub 定时承诺系统的规范性协议规范。它定义了可互操作实现所需的数据结构、序列化规则、推导公式与验证流程。

范围:协议层有意保持语言中立——qub 主体是不透明的明文 / markdown / 约定字节,区域感知的渲染由查看者负责(qub.social 网页应用、<qub-embed> iframe、MCP 客户端等)。


1. 记法与约定

记法 含义
u8、u64、i64 指定位宽的无符号 / 有符号整数
[u8; N] 长度为 N 字节的定长字节数组
Vec<u8> 变长字节数组
Option<T> T 类型的值,或不存在
String UTF-8 文本字符串,已 NFC 规范化
`
SHA3-256(x) 字节串 x 的 NIST SHA3-256 哈希(FIPS 202)
ceil(x) 向上取整函数:不小于 x 的最小整数
CBOR 简明二进制对象表示(RFC 8949)
大端 最高有效字节在前

预映像构造中的所有整数均编码为大端定宽字节数组(i64 → 8 字节,u8 → 1 字节),除非另有说明。

所有时间戳均为 UTC Unix 秒。


2. 数据结构

2.1 ComposeQub(创作者内存状态)

不序列化为 CBOR。不写入永久存储。仅在创作者应用本地存在。

ComposeQub {
    draft_id:       [u8; 16],        // Random, generated locally
    created_at:     i64,             // Unix seconds UTC
    unlock_at:      Option<i64>,     // Unix seconds UTC; None while composing
    visibility:     u8,              // 0x00 = private; 0x01 = public
    content_type:   u8,              // 0x01 text; 0x03 pact; 0x04 verdict
    plaintext:      Vec<u8>,         // Raw body bytes (UTF-8 for text)
    sender_label:   Option<String>,  // Display name; V2-signed when authorship is enabled
    title:          Option<String>,  // Plaintext countdown title; bound via title_hash
    reply_to:       Option<[u8; 32]>,// Parent qub_id; V2-signed when authorship is enabled
    outcome_at:     Option<i64>,     // Optional future judgment time; bound to qub_id
    status:         DraftStatus,     // Composing | Sealed | Uploaded | Failed
}

2.2 QubEnvelope(解密后的负载)

使用规范 CBOR 序列化(§3)。被加密后置于 SealedQub 内部。这是在解密后用于证明内容完整性的结构。

QubEnvelope {
    version:             u8,              // Protocol major version (0x01 for v1)
    qub_id:              [u8; 32],        // Derived (see §4.1)
    content_type:        u8,              // Content type registry (see §6)
    created_at:          i64,             // Unix seconds UTC
    unlock_at:           i64,             // Unix seconds UTC
    outcome_at:          Option<i64>,     // When reality renders judgment; bound to qub_id
    sender_label:        Option<String>,  // Not in qub_id; V2-signed when authorship is enabled
    reply_to:            Option<[u8; 32]>,// Parent qub_id; not in qub_id; V2-signed when present
    body:                Vec<u8>,         // UTF-8 text or canonical CBOR pact/verdict body
    body_hash:           [u8; 32],        // SHA3-256(body) (see §4.2)
    sig_alg:             u8,              // Signature algorithm (see §9.2)
    author_signature:    Option<Vec<u8>>, // Set when sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Set when sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
    cosigner_signature:  Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}

基线(无符号文本 qub): version = 0x01, content_type = 0x01, sig_alg = 0x00; 签名和共同签署人字段缺失。其他可选的元数据字段可能存在。

其他 v1 配置: content_type = 0x03(约定主体,见 §6.1);sig_alg = 0x01(ML-DSA-65),同时存在 author_signature 与 author_pubkey(见 §9.3);针对共同签名的约定,cosigner_pubkey 与 cosigner_signature 同时出现(见 §9.7);针对回复链 qub,将 reply_to 设为父 qub 的 qub_id(关于签名作用域的影响见 §9.3)。

2.3 SealedQub(规范线缆格式)

使用规范 CBOR(§3)序列化。这是内部传输产物:公共交付裸露存储这些字节,而私人交付则将它们包装在 OuterWrapper 储存前(§13)。

SealedQub {
    version:           u8,              // Protocol major version (0x01 for v1)
    qub_id:            [u8; 32],        // Same as QubEnvelope.qub_id
    visibility:        u8,              // 0x00 = private/wrapped; 0x01 = public/bare
    unlock_at:         i64,             // Unix seconds UTC
    outcome_at:        Option<i64>,     // Surfaced on the verdict-watch CTA
                                        //   before reveal; mirrors QubEnvelope.outcome_at;
                                        //   bound to qub_id via the §4.1 preimage.
    drand_chain_id:    String,          // drand chain hash (hex string)
    drand_round:       u64,             // Target drand round number
    drand_chain_version: Option<u8>,    // W3 — chain-migration version. Absent / 0 = quicknet
                                        //   (the only chain today). Lets a future chain swap
                                        //   be expressed on the wire without a breaking format
                                        //   change. NOT part of the §4.1 qub_id preimage, so its
                                        //   addition never alters an existing qub's identity.
    tlock_ciphertext:  Vec<u8>,         // tlock-encrypted QubEnvelope CBOR bytes
    recipient_pubkey:  Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
                                        //   but not interpreted by the v1 reference viewer
    title:             Option<String>,  // Plaintext title surfaced on the viewer
                                        //   countdown before reveal. Bound to qub_id
                                        //   via title_hash (§4.1). 1..=100 NFC code
                                        //   points, no hostile/control code points.
}

2.4 RevealedQub(查看者应用状态)

不序列化为 CBOR。仅在查看者应用本地存在。在成功解密并验证后构造。

RevealedQub {
    qub_id:              [u8; 32],
    arweave_tx_id:       String,
    visibility:          u8,
    content_type:        u8,
    created_at:          i64,
    unlock_at:           i64,
    outcome_at:          Option<i64>,       // Carried from both wire layers; drives the verdict-watch block
    drand_chain_id:      String,
    drand_round:         u64,
    sender_label:        Option<String>,
    title:               Option<String>,    // Carried forward from SealedQub.title
    reply_to:            Option<[u8; 32]>,
    body:                Vec<u8>,
    body_hash:           [u8; 32],
    body_hash_verified:  bool,
    author_signature:    Option<Vec<u8>>,
    author_pubkey:       Option<Vec<u8>>,
    signature_verified:  Option<bool>,
    cosigner_pubkey:     Option<Vec<u8>>,
    cosigner_signature:  Option<Vec<u8>>,
    cosigner_verified:   Option<bool>,
}

3. 规范 CBOR 配置

所有 SealedQub 与 QubEnvelope 序列化必须符合本配置。对于相同的逻辑结构,两个实现必须产出相同的字节。

3.1 编码规则

规则 规范
标准 RFC 8949 §4.2.1(核心确定性编码要求)
映射键排序 先按编码后的字节长度排序(短在前长在后),再按字典序排序(长度相同时按字节逐字节比较)
整数编码 最短形式:0–23 嵌入初始字节;24–255 用 2 字节;256–65535 用 3 字节;以此类推。
长度编码 仅使用定长。 不允许不定长数组、映射、字节串或文本串(禁止附加信息 = 31)。
标签 不允许 CBOR 标签(禁止主类型 6)。
浮点数 不允许浮点(禁止主类型 7 中值为 0xF9–0xFB 的项)。
文本串 UTF-8 编码,已 NFC 规范化(Unicode Normalization Form C)。
字节串 原始字节。CBOR 层不进行 base64 编码。
重复键 报错拒绝。 解析器禁止静默接受重复的映射键。
未知键 报错拒绝。 解析器禁止容忍该类型规范键集之外的映射键——两个不同的规范字节串决不能解码为同一值(encode(decode(x)) == x),且对已签名负载而言,额外的键将成为两份签名都承诺到的隐藏内容。Schema 演进只通过 version 进行,决不通过额外的键。
简单值 仅允许 true(0xF5)、false(0xF4)与 null(0xF6)。
可选字段 不存在的可选字段从 CBOR 映射中完全省略(而非编码为 null)。存在的可选字段按排序后的键序加入。

3.2 已验证的规范键序

以下键序是规范性的。实现必须严格按照此顺序输出键。非发布构建中调试断言应当验证该顺序。

QubEnvelope(版本 0x01,未签名,所有可选字段不存在):

"body"                (5 encoded bytes)
"qub_id"              (7 encoded bytes)
"sig_alg"             (8 encoded bytes)
"version"             (8 encoded bytes)
"reply_to"            (9 encoded bytes)   ← only if present (reply chains)
"body_hash"           (10 encoded bytes)
"unlock_at"           (10 encoded bytes)
"created_at"          (11 encoded bytes)
"outcome_at"          (11 encoded bytes)  ← only if present (verdict mechanic)
"content_type"        (13 encoded bytes)
"sender_label"        (13 encoded bytes)  ← only if present
"author_pubkey"       (14 encoded bytes)  ← only if present
"cosigner_pubkey"     (16 encoded bytes)  ← only if present (pact cosign)
"author_signature"    (17 encoded bytes)  ← only if present
"cosigner_signature"  (19 encoded bytes)  ← only if present (pact cosign)

QubEnvelope 键序推导: 每个键都是 CBOR 文本串。编码长度 = 1 字节头 + 字符串长度(对于长度小于 24 字节的字符串)。先按总编码长度排序,长度相同时按字典序排序。

SealedQub(版本 0x01,公开,无接收者):

"title"             (6 encoded bytes)   ← only if present
"qub_id"            (7 encoded bytes)
"version"           (8 encoded bytes)
"unlock_at"         (10 encoded bytes)
"outcome_at"        (11 encoded bytes)  ← only if present (verdict mechanic)
"visibility"        (11 encoded bytes)
"drand_round"       (12 encoded bytes)
"drand_chain_id"    (15 encoded bytes)
"recipient_pubkey"  (17 encoded bytes)  ← only if present
"tlock_ciphertext"  (17 encoded bytes)
"drand_chain_version" (20 encoded bytes) ← only if present (W3; absent = quicknet)

PactTerms(约定主体,content_type 0x03):

"notes"         (6 encoded bytes)  ← only if present
"terms"         (6 encoded bytes)
"title"         (6 encoded bytes)
"party_a"       (8 encoded bytes)
"party_b"       (8 encoded bytes)
"pact_version"  (13 encoded bytes)

PactTerm(terms 数组的一行):

"key"    (4 encoded bytes)
"value"  (6 encoded bytes)

PartyIdentifier(party_a / party_b 映射):

"label"    (6 encoded bytes)
"contact"  (8 encoded bytes)  ← only if present

3.3 字节编码参考

类型 CBOR 编码 示例
SHA3-256 哈希(32 字节) 0x58 0x20 + 32 字节 body_hash、qub_id
时间戳(i64) 主类型 0(正)或 1(负),最短编码 Unix 秒
版本(u8,值 1) 0x01(单字节)
内容类型(u8,值 1) 0x01(单字节)
sig_alg(u8,值 0) 0x00(单字节)
ML-DSA-65 签名(3,309 字节) 0x59 0x0C 0xED + 3,309 字节 author_signature、cosigner_signature
ML-DSA-65 公钥(1,952 字节) 0x59 0x07 0xA0 + 1,952 字节 author_pubkey、cosigner_pubkey

4. 规范性推导

4.1 qub_id

qub_id 唯一标识一个 qub,并将 QubEnvelope 与 SealedQub 绑定。它由信封内容确定性地推导而来。

qub_id = SHA3-256(
    "QUB_ID_V2"          ||  // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
    version              ||  // u8 (1 byte)
    content_type         ||  // u8 (1 byte)
    created_at           ||  // i64 big-endian (8 bytes)
    unlock_at            ||  // i64 big-endian (8 bytes)
    outcome_at_or_zero   ||  // i64 big-endian (8 bytes; 0 when outcome_at is absent)
    drand_round          ||  // u64 big-endian (8 bytes)
    body_hash            ||  // [u8; 32] (32 bytes)
    title_hash               // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output

域分隔符编码: 字符串 "QUB_ID_V2" 为 9 个 ASCII 字节。附加单个 0x00 填充字节以达到 10 字节对齐。实现必须严格使用以下 10 字节:[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]。

outcome_at 编码: 预发布实现版本将原像从 92 字节扩展到 100 字节,以折叠可选项 outcome_at 字段进入绑定。缺失 outcome_at 被编码为8个零字节;协议验证器拒绝 outcome_at <= 0 到处都是,所以这个哨兵不会与合法值冲突。见§3.2(线格式)和树内 tasks/verdict-uplift-plan.md 对于推动这个领域的裁决机制。

drand_round 编码: 后来的预发布实现修订将预映像从100字节扩展到108字节以进行折叠 drand_round (目标 drand 回合,第 4.3 节)纳入绑定,并将域分隔符提升到 QUB_ID_V2这将时间锁轮次绑定到 qub 身份:网关不能将密文重新绑定到与显示的不同(例如已过去的)轮次 unlock_at 暗示。解锁程序(§8)还会验证嵌入 tlock 密文段的轮次是否匹配 unlock_round(unlock_at)因此,显示的解锁时间可以证明就是解密门的轮次。

特性:

4.2 body_hash

body_hash = SHA3-256(body)

其中 body 是原始 Vec<u8> 内容负载。对于文本 qub,这是 UTF-8 编码的 qub 主体。

4.2.1 title_hash

title_hash = SHA3-256(NFC(title).utf8_bytes)   if title is present
title_hash = [0u8; 32]                         if title is absent

其中 title 是在公开前查看者倒计时上展示的可选明文标题(见 §3.2)。NFC 规范化在哈希时执行,使摘要在视觉上等价的码点序列之间保持稳定。全零标记保留给不存在的情形;空字符串在规范 CBOR 边界上作为"不存在"的非规范编码而被拒绝(规范编码会完全省略该字段)。

4.3 解锁轮映射

drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
参数 来源 例子
unlock_at 用户选择的 Unix 秒 UTC 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time drand 链信息 (genesis_time) 1595431050
chain_period_seconds drand 链信息 (period) 30

这是参考 tlock 映射(drand 的 CurrentRound). drand 发布回合 N 在 chain_genesis_time + (N - 1) * chain_period_seconds,所以公式选择了 循环电流于 unlock_at ——观众到达时,第一个签名的回合 unlock_at 可以使用。

对齐属性(在实际中重要的情况): 什么时候 (unlock_at - chain_genesis_time) 可以被整除 chain_period_seconds,所选轮次的签名已发布 正好在 unlock_at以前从未有过它. 这一直适用于参考部署:quicknet 的创世时间(1692803367) 可被其 3 秒周期整除,而且参考应用将解锁时间固定到整分钟。对于未对齐的 unlock_at,所选轮次的签名严格在不到一个周期之前发布 unlock_at — 承诺的时间精度为一个信标周期。

旧版预发布映射和解锁端容差: 原始映射是 ceil((unlock_at - chain_genesis_time) / chain_period_seconds),对于上述按周期对齐的情况——选择了整整一个周期发布的版本 之前 unlock_at,使密文可以比正常提前一个时期间被解密。这两个映射准确地相差 +1 当增量除以周期时,并且在其他方面同意。因为 drand_round 被折叠进不可变的 qub_id 原像 (§4.1),在遗留映射下封存的产物无法重新推导;因此,执行 §8 步骤 6a 回合交叉检查的验证者必须接受已存储的内容 drand_round 等于 任一 派生回合 或 派生的轮次减一(并且必须要求 tlock 节的轮次完全等于存储的轮次)。公差最多将最早的门控签名扩大一个周期。当 pact 分阶段服务重新派生阶段性 pact 时,也会应用相同的公差。 qub_id (在阶段和在共同签名时):如果当前映射的轮次没有再现已提交的内容 qub_id 并且增量划分了周期,它会在轮次减一时重试,并将最终的协议封存到任意轮次 qub_id 实际上绑定——绝不盲目地绑定到重新计算的轮次,这样会使产物永久不可导出。

验证: unlock_at 必须在封印时刻的未来。 unlock_at 不得超过 10 年 created_at (为了限制长期随机依赖风险;用户界面应对超过2年的解锁日期发出警告)。


5. 线缆格式 Newtype

线缆格式 newtype 在编译期防止将 CBOR 字节与 JSON、原始明文或其他字节编码混淆。

类型 包含 制作 被吞噬
SealedQubCbor SealedQub 的规范 CBOR serialize_sealed_qub() 内线装置;以裸露形式存放用于公众交付或包裹形式用于私人交付,然后由观众回收
QubEnvelopeCbor QubEnvelope 的规范 CBOR serialize_qub_envelope() tlock 加密输入,tlock 解密输出

5.1 构造规则

// Production code — only through CBOR serialisers:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);

// There is deliberately NO From<Vec<u8>> implementation.
// You cannot accidentally wrap arbitrary bytes in a wire format type.

// Accessing raw bytes:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();

5.2 构造时的校验

from_encoded() 应当校验输入以有效的 CBOR 映射头开始。完整的结构校验在解析时进行,而非构造时,以避免重复解析。


6. 内容类型注册表

值 类型 最大主体大小 备注
0x00 保留(无效) — 禁止使用
0x01 纯文本(UTF-8,受限 Markdown) 50 KB 付费 / 10 KB 免费 渲染规则见 §10。免费 / 付费切分由上传服务强制执行;协议层硬性上限为 50 KB。
0x02 保留(未来) — 为未来的内容类型保留;在 v1 中无效。查看者必须按下文规则拒绝。
0x03 约定(双边协议,CBOR 主体) 100 KB 主体为规范 CBOR 的 PactTerms(§6.1)。共同签名见 §9.7。
0x04 判定(创作者自评,CBOR 主体) 8 KB 主体为规范 CBOR 的 VerdictBody(§6.2)。仅由系统侧的 verdict 意图发出。父子关系挂在 Parent-Tx-Id Arweave 标签上,不在主体中。详见 verdict-uplift-plan §3.4。

查看者必须以清晰的用户可见错误拒绝未知内容类型。查看者禁止尝试将未知类型作为文本渲染。

6.1 约定主体(content_type = 0x03)

约定主体是 PactTerms 值的规范 CBOR 编码:

PactTerms {
    pact_version:  u8,                    // 0x01 for structured/v1
    title:         String,                // ≤ 200 bytes, NFC
    terms:         Vec<PactTerm>,         // ≤ 20 rows
    party_a:       PartyIdentifier,       // initiator
    party_b:       PartyIdentifier,       // counter-signer
    notes:         Option<String>,        // ≤ 5,000 bytes, NFC; absent key if none
}

PactTerm       { key: String (≤ 100 bytes), value: String (≤ 2,000 bytes) } // NFC
PartyIdentifier{ label: String (≤ 100 bytes), contact: Option<String (≤ 320 bytes)> }

三个映射的规范 CBOR 键序见 §3.2。序列化后的约定 CBOR 总大小不得超过 100 KB(与 §6 一致)。

Schema 鉴别字段。 对 structured/v1 约定,terms 中的第一行必须为 { key: "pact_schema", value: "structured/v1" }。没有此标记的行属于"自定义"约定,不接受结构化校验或 schema 感知的渲染。

冻结的确认槽位。 structured/v1 约定恰好在以下键下携带四个确认行:

"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"

每行的 value 是按 (role, kind) 对所选定的八个冻结英文字符串之一,其中 role ∈ { seller, buyer, provider, client },kind ∈ { standard, capacity }。这些字符串本身是规范性协议数据——双方的 ML-DSA-65 签名通过 body_hash 承诺到这些精确字节。它们不会被本地化;签名所覆盖的主体是语言中立的。任何措辞变更都需要新的 schema 版本(structured/v2)。

这八个字符串、它们的查找方式(acknowledgement_for(role, kind))以及每条的理由由参考实现固定。符合规范的实现必须输出字节相同的确认值;覆盖全部四种角色组合的黄金固件 SHA3-256 body-hash 测试可捕获任何漂移。

查看者展示顺序。 这些确认字符串包含诸如 "described above" 等措辞,前提是描述 / 范围行先于确认行渲染。查看者必须按 CBOR 顺序渲染 terms 数组;重新排序会破坏文本语义。

对方联系方式。 当 Party B 的 contact 为有效的电子邮件地址时,qub 上传服务在暂存阶段会自动发出审阅 / 共同签名邀请邮件,并将最终的共同签名绑定到对同一地址的验证(§9.7)。Party B 联系方式缺失的约定仍可被共同签名,但只能经由带外渠道完成——服务会拒绝无法产出匹配的 15 分钟邮箱验证标记的共同签名请求。

6.2 判定主体(content_type = 0x04)

判定主体是 VerdictBody 值的规范 CBOR 编码:

VerdictBody {
    verdict_version: u8,                  // 0x01 for structured/v1
    outcome:         u8,                  // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
    reflection:      Option<String>,      // ≤ 2,000 bytes NFC; "what changed, what did you learn"
    evidence_url:    Option<String>,      // ≤ 2,048 bytes; HTTPS only; absent key when omitted
}

规范 CBOR 键序:

"outcome"          (8 encoded bytes)
"reflection"       (11 encoded bytes)  ← only if present
"evidence_url"     (13 encoded bytes)  ← only if present
"verdict_version"  (16 encoded bytes)

序列化后的判定 CBOR 总大小不得超过 8 KB(与上方注册表行一致)。

结果枚举。 线上字节与意图无关;四类分桶 Right / Partial / Wrong / Unfalsifiable 覆盖每种带判定意图的结果空间。按意图区分的标签(如 Right 对应"说中了" / "已兑现" / "按计划完成" / "由事实印证"等)属于查看者侧的渲染问题,需对照父 qub 的意图来解析——线上格式保持语言中立与意图中立。1..=4 范围外的值在解码时必须被拒绝。

父子链接。 判定 qub 不在主体中携带父引用。父 qub 的 Arweave 交易 id 在上传时作为 Parent-Tx-Id 存储标签发出(§7 存储标签层)。这样主体仍是自洽的、已签名的自评陈述;审计链("针对什么是对的?")通过 Arweave 标签查询建立。

证据 URL 安全(规范性)。 当 evidence_url 存在时,校验器(撰写侧、线上侧、Worker 边缘)必须强制执行:

  1. 仅限 HTTPS。 字符串必须以字节序列 https:// 开头。任何其他协议——http、ftp、javascript、data、file 等——一律拒绝。
  2. 长度上限。 ≤ 2,048 字节(浏览器 URL 实用上限)。
  3. NFC + 敌意码点检查。 同 title 与 reflection 的规则——bidi 覆写 / 零宽 / tag-block / BOM / C0 / C1 码点一律拒绝。定义与 Rust 端 crate::handle::contains_hostile_text_codepoint 及 TS 端 workers/api/src/utils/unicode.ts::isHostileCodepoint 一致(三者须保持同步)。
  4. 不得含空白与 ASCII 控制符。 URL 中任意位置的空白 / DEL / 低于 0x20 的字节一律拒绝——封堵 bidi 规则未覆盖的 \n/\t 注入向量。
  5. 主机段不得为空。 https:// 与首个 /、? 或 # 之间的内容必须非空。

禁止服务端抓取。 Worker 禁止代理、抓取或预览该 URL。协议仅存储一个字符串;渲染发生在查看者侧,带 rel="nofollow noopener noreferrer" target="_blank" 并在链接文本旁显示可见的主机名。

反思。 可选的创作者自撰反思文本("有什么变化、学到了什么")。校验规则与 title 相同的 NFC + 敌意码点检查。空 / 仅空白的输入在构造时折叠为缺失。

Schema 版本。 v1 仅支持 verdict_version = 0x01。未来 schema 修订将提升此字节,并按 §12 与新的协议版本同步落地。


7. 封印协议

完整的封印流程。每一步都是规范性的。

 1. User composes plaintext and metadata in ComposeQub.
 2. Validate:
    a. body is non-empty.
    b. body size ≤ max for content_type and user tier (see §6).
    c. unlock_at is in the future.
    d. unlock_at ≤ created_at + 10 years.
    e. content_type is a known, supported value.
    f. visibility is 0x00 (private) or 0x01 (public).
 3. Compute body_hash = SHA3-256(body).
 4. Set created_at = current Unix seconds UTC.
 5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
    compute drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
    (§4.3). (Computed here, before qub_id, because drand_round is bound into the
    qub_id preimage—§4.1.)
 6. Compute qub_id (see §4.1), folding in drand_round from step 5.
 7. Construct QubEnvelope with all fields.
 8. Serialise QubEnvelope using canonical CBOR → bytes B.
    Assert: serialised output matches canonical profile (§3).
 9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C and matching qub_id, version,
    visibility, unlock_at, drand_chain_id, and drand_round.
11. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12. Select the delivery shape from visibility:
    a. Private (0x00): generate K = 32 random bytes and N = 12 random bytes
       using a CSPRNG. Compute W = wrap_sealed_qub(SealedQubCbor,
       qub_id=qub_id, key=K, nonce=N) per §13. Upload payload = W.
    b. Public (0x01): upload payload = bare SealedQubCbor; do not generate K.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit the selected upload payload to the qub upload service. For a private
    browser seal, the service is byte-blind to the inner SealedQubCbor and never
    receives K. The Builder `/api/v1/seal` route is an explicit exception: it
    receives plaintext and caller-supplied K in memory, then persists neither.
16. Receive arweave_tx_id from the service. For private delivery, construct
    `<origin>/c/<arweave_tx_id>#<base64url(K)>` (or the equivalent short-code
    path). For public delivery, omit the fragment. Browsers do not transmit URL
    fragments to servers, so K from the browser-seal path is not observed by
    qub.social or any storage gateway.

存储标签层(带外)。 Qub 上传服务在所选上传负载旁边附加了一组刻意较小的存储事务标签。 Content-Type=application/octet-stream 在规范上是必需的。当创建者选择显示它们时,参考服务还会附加三个可选标签: Intent (允许列表验证的撰写意图—announcement, thesis, prediction, letter, secret, commitment, proof,或系统发出的 verdict), Author (创建者的 §9.3 公钥指纹,64 字符小写十六进制),以及 Parent-Tx-Id (用于回复链的父 qub 存储交易 ID,43 字符 base64url)。

这 Author 标签是 每个 qub 选择加入:参考创建者应用程序仅在用户在盖章时明确启用公开署名时才附加它。当开关关闭时——默认设置——不会附加 Author 标签已写入且区块在链上未标注出处:永久存储中没有任何内容将上传内容与创作者的用户名、电子邮件或其他区块关联。当切换开启时, Author 指纹解析到创作者选择的 @handle 通过§9.5认证链。回复链关系和 Intent 是非识别性的。对于私人传递,外层包装 (§13) 对可识别的内层进行加密 SealedQub 因此,仅收集存储的包装器和获取公共 drand 签名仍不足以在没有 K 的情况下恢复主体;存储标签仍然是故意公开的元数据。

参考服务有意不附加 App-Name、App-Version 或 Type 标签:任何此类单值筛选都会让 GraphQL 查询返回整个 qub 语料库,这与封装的"仅主体保密"作用域不一致。

符合规范的验证者在执行 §11 的第三方验证时禁止依赖任何存储标签;主体哈希 / qub_id / 签名只承诺到内部 CBOR,决不承诺到标签集。


8. 公开协议

完整的公开流程。每一步都是规范性的。

 1. Viewer opens delivery URL. Extract arweave_tx_id from the path and retain
    the optional URL fragment. Do not assume a missing fragment is an error:
    public/bare delivery intentionally has no K.
 2. Check denylist. If tx_id is denylisted → display block message. Stop.
 3. Fetch the stored bytes (with multi-gateway fallback).
 3a. Resolve the delivery shape structurally:
    a. If the bytes parse as OuterWrapper, require a well-formed 32-byte K in
       the URL fragment, require wrapper version 0x01, and unwrap per §13.
       Any missing/malformed K or AEAD failure is a terminal error.
    b. Otherwise require the bytes to parse as bare SealedQubCbor; no K is
       required. If neither shape parses, report an integrity error.
 4. Parse SealedQubCbor → SealedQub.
 5. Validate: SealedQub.version is known (0x01), visibility is known, and the
    delivery shape matches it (wrapped = 0x00 private; bare = 0x01 public).
    Reject any mismatch or unknown value.
 6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
 6a. Round-binding check. Recompute expected_round from
    SealedQub.unlock_at per §4.3. Reject unless SealedQub.drand_round ==
    expected_round OR SealedQub.drand_round == expected_round - 1 (the
    legacy pre-release mapping—see §4.3), AND the round baked into the tlock
    ciphertext stanza (read via the age/tlock header, no signature required)
    == SealedQub.drand_round exactly. The stanza round is the one that
    actually gates decryption; without this check a malicious creator could
    bind the ciphertext to an already-past round while displaying a future
    countdown, so anyone reading the stored bytes could decrypt before
    unlock_at. Implementations with no chain identity (test mocks) skip this
    check.
 7. Once current time ≥ SealedQub.unlock_at:
    a. Fetch drand round signature for SealedQub.drand_round from drand network.
    b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
 8. Parse B → QubEnvelope.
 9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
    Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
    Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
    Fail → integrity error.
12a. Verify: QubEnvelope.outcome_at == SealedQub.outcome_at (both absent, or
    both present and equal). Fail → integrity error.
12b. Content re-derivation. Recompute qub_id per §4.1 from the decrypted
    fields — (QubEnvelope.version, content_type, created_at, unlock_at,
    outcome_at, SealedQub.drand_round, QubEnvelope.body_hash,
    title_hash(SealedQub.title)) — and verify it equals SealedQub.qub_id.
    Fail → integrity error. The pairwise checks in steps 10-12a only prove
    the two layers agree with EACH OTHER; a forger who rewrites a bound
    field consistently on both surfaces (a pre-reveal title swap, or a
    post-round body swap with a recomputed body_hash re-encrypted to the
    same round under the same qub_id) passes them all. Only re-deriving
    the identity from content closes this.
13. Verify: QubEnvelope.content_type is known and renderable.
    Known values: 0x01 (text), 0x03 (pact), 0x04 (verdict).
    Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using the appropriate renderer (see §10 for text and §6 for pact/verdict).
17. Construct RevealedQub for display.

9. 作者签名

9.1 理由

qub 永久存储于永久存储。作者签名必须长期保持不可伪造,这就是 v1.0 采用后量子的 ML-DSA-65 方案(FIPS 204)而非某个其安全性可能在 qub 的永久生命周期内退化的经典方案的原因。

9.2 算法注册表

sig_alg 计划 密钥大小 签名尺寸 状态
0x00 无签名(未签名) — — 活跃
0x01 ML-DSA-65(FIPS 204) 1,952 字节 3,309 字节 活跃
0x02 Ed25519 32 字节 64 字节 保留常量;协议v1中不支持。

Protocol-v1 观察者必须拒绝所有超出范围的值 {0x00, 0x01},包括 内向的 0x02 值。保留可以防止意外重用;它不是 激活。激活它需要遵循第15条中描述的受控变更。

9.3 签名预映像构造

曾存在两个预映像版本。所有签名都必须使用 V2,验证者也必须仅接受 V2。 旧版 V1 预映像(下文出于历史参考予以记录)曾在向 V2 迁移期间作为仅供验证的回退方案被接受;该回退方案现已废弃,仅能验证为 V1 的签名如今将被拒绝。

V2(当前版本——由所有新的作者签名产生,也由约定暂存 / 共同签名流程中的两个签名产生):

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V2"  ||    // domain separator (17 bytes)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 bytes)
    body_hash            ||    // [u8; 32] (32 bytes)
    unlock_at            ||    // i64 big-endian (8 bytes)
    0x00                 ||    // u8 (1 byte): MUST be 0x00 in v1.x
    sender_label_hash    ||    // [u8; 32]: SHA3-256(NFC(sender_label)),
                               //   or 32 zero bytes when absent
    reply_to_or_zero           // [u8; 32]: parent qub_id, or 32 zero
                               //   bytes when absent
)

// Total preimage: 155 bytes → 32-byte hash

signature = Sign(author_secret_key, sig_input)

sender_label_hash 遵循与 title_hash(§4.2.1)相同的不存在哨兵约定:32 个零字节并非有效的 SHA3-256 输出,因此不存在的情形绝不会与实际存在的标签相冲突。所有字段均为定宽,因此该预映像无需长度前缀即可无歧义解析。

V1(旧版——已废弃;不再产生,验证时也不再接受):

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V1"  ||    // domain separator (17 bytes)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 bytes)
    body_hash            ||    // [u8; 32] (32 bytes)
    unlock_at            ||    // i64 big-endian (8 bytes)
    0x00                       // u8 (1 byte): MUST be 0x00 in v1.0
)

// Total preimage: 91 bytes → 32-byte hash

V1 预映像省略了 sender_label 与 reply_to。它曾在向 V2 迁移期间作为仅供验证的回退方案被接受;该回退方案此后已废弃——验证者必须仅接受 V2 预映像。此处保留其定义仅供历史参考,并用于说明下文的域分隔符。仅能对 V1 验证通过的签名必须视为验证失败。

域分隔符: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" 各为 17 个 ASCII 字节([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32])。不进行填充。不同的分隔符对两种构造进行了域分隔,因此对其中一个预映像的签名绝不会验证为另一个。

org_id_present 字节: 紧随 unlock_at 之后的字节必须为 0x00。参考实现将其作为常量 ORG_ID_PRESENT_INDIVIDUAL = 0x00 暴露于 crates/qub-core/src/signing.rs;为验证而重建 sig_input 的查看者必须输出相同的字节。

签名作用域——覆盖与未覆盖的内容。 V2 的 sig_input 直接承诺到 version、qub_id、body_hash、unlock_at、sender_label 与 reply_to(外加固定的域分隔符与 org_id_present 字节)。qub_id 本身经由 §4.1 的预映像从 version、content_type、created_at、unlock_at、outcome_at、drand_round 与 body_hash 推导而来,因此对这些字段的任何改动都会产生不同的 qub_id,并以传递方式使签名失效。因此被认证的字段如下:

字段 由签名认证 如何
version ✓ 直接输入到 sig_input
qub_id ✓ 直接输入
body_hash ✓ 直接输入
unlock_at ✓ 直接输入
sender_label ✓ 通过直接输入 sender_label_hash (V2 预映像 — 唯一被接受的形式)
reply_to ✓ 通过直接输入 reply_to_or_zero (V2 预映像 — 唯一被接受的形式)
content_type ✓ 传递地,通过 qub_id 原像
created_at ✓ 传递地,通过 qub_id 原像
outcome_at ✓ 传递地,通过 qub_id 原像
drand_round ✓ 传递地,通过 qub_id 原像
body ✓ 传递地,通过 body_hash = SHA3-256(body)
author_pubkey —(隐含) 按定义,验证签名的密钥就是作者的密钥
cosigner_pubkey / cosigner_signature — 独立签署了相同的 sig_input (参见§9.7)
drand_chain_id, tlock_ciphertext, visibility — 位于信封之外的 SealedQub 字段——受各自的结构不变量(轮次/链一致性)约束,但不受作者签名保护。(drand_round 现在通过 qub_id 前像间接绑定——见上文。)

为何 V2 是唯一被接受的预映像。

向终端用户展示 sender_label 或 reply_to 的实现必须把已认证的身份(公钥指纹、认证)作为主要身份信号呈现,而非标签。

9.4 验证流程

1. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
   only accepted form — the legacy V1 fallback is retired (§9.3), so a
   signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."

签名验证是最昂贵的操作(尤其是 ML-DSA-65)。它应当在所有更廉价的检查(哈希、qub_id、unlock_at)都通过之后再执行。

9.5 身份认证

身份认证——将 author_pubkey 映射到人类可识别的身份声明(如 qub 句柄、邮箱地址、社交账号或 passkey 凭据)——是查看者侧的渐进增强,对签名验证并非必需。解析认证以得出显示身份的查看者必须按下列优先级使用:

handle > email > social > fingerprint

指纹回退是 SHA3-256(author_pubkey) 的小写十六进制;对任何已签名的 qub 始终可用。查看者可以将其缩写以供显示——参考查看者渲染 qub: 后跟首尾各四个字节(qub:<8 hex>…<8 hex>)。

符合规范的验证者可在不联系 qub API、除永久存储与 drand 外不使用任何网络、也不进行任何服务器端查找的情况下,完成 §9.4 中的全部检查。认证解析是仅在签名验证成功之后执行的、独立的尽力而为步骤。

9.6 体积影响

Ed25519 ML-DSA-65
签名 64 字节 3,309 字节
公钥 32 字节 1,952 字节
每个 qub 总计 96 字节 5,261 字节
存储成本差(约 $5/MB) ~$0.0005 ~$0.026

对于 500–2,000 字节的文本 qub,ML-DSA-65 大约让存储大小翻三倍。绝对成本可忽略不计。

9.7 共同签名者验证(约定双边协议)

对于双边协议(content_type = 0x03),第二层签名证明双方对相同条款表示同意。

信封字段:

两个字段必须同时存在或同时缺失。若仅其中一个存在,查看者必须报告完整性错误。

验证流程:

1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
   Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3 (V2 only — the
   legacy V1 fallback is retired; all pact clients produce V2 signatures).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."

特性:

邮箱绑定门控(运维层面)。 当暂存的约定携带 Party B 邮箱联系方式(§6.1)时,qub 上传服务必须拒绝共同签名请求,除非存在与暂存 id 及该联系方式归一化后的邮箱哈希同时匹配的短期邮箱验证标记。该标记在 /api/v1/auth/verify 处于魔法链接令牌携带 staging_id 且已验证地址匹配 SHA-256(normalise_email(party_b.contact)) 时写入——其中 normalise_email(addr) 保留本地部分的大小写,仅将域名部分小写化(按 RFC 5321 §2.3.11),且此处的 SHA-256 为 NIST FIPS 180-4 哈希(与 §4 推导中使用的 SHA3-256 不同)——并在签发后 900 秒(15 分钟)失效。这是一个运维层面的反假冒门控,不属于链上 qub 证明的一部分——重放 §11 的第三方验证者只需永久存储与 drand,无需任何服务器端查找。该标记仅存在于服务器端,决不属于已签名主体的一部分。

体积影响(ML-DSA-65 作者 + 共同签名者):

组件 大小
作者签名 3,309 字节
作者公钥 1,952 字节
共同签名者签名 3,309 字节
共同签名者公钥 1,952 字节
加密开销总计 10,522 字节
存储成本差 ~$0.05

10. Markdown 渲染与清洗

本节是安全关键章节。查看者使用受限的 Markdown 子集渲染文本 qub(content_type = 0x01)。

10.1 允许的元素

10.2 禁用的元素

元素 处理方式
原始 HTML(<div>、<script> 等) 完全剥离。HTML 不会通过。
图片(![alt](url)) 剥离。输出中删除图片语法。
链接([text](url)) URL 渲染为可见的纯文本。不自动建立链接。未经用户显式操作不可点击。
危险 URL 协议 javascript:、data:、vbscript:、file:——剥离。
iframe、嵌入、对象 剥离。
HTML 实体 仅在安全时解码为可显示字符。

10.3 实现

实现必须使用严格的白名单解析器,而非黑名单。推荐做法:

  1. 用 pulldown-cmark(或等价工具)解析 Markdown。
  2. 遍历 AST 并丢弃任何不在白名单(§10.1)中的节点。
  3. 对于链接节点:将 URL 作为可见文本输出,而非可点击的 <a> 元素。
  4. 将过滤后的 AST 转换为类型化中间表示(例如仅含安全变体的 MarkdownNode 枚举)。原始 HTML 在该 IR 中结构上不可表达。
  5. 从类型化 IR 渲染到目标视图层(例如响应式视图组件、DOM 节点)。任何阶段都不进行 HTML 字符串拼接或使用 innerHTML。

黑名单方法是脆弱的,因为新的 Markdown 扩展或解析器怪癖可能引入未被过滤的元素。类型化 AST 方法让 XSS 在结构上不可能存在——没有任何变体可以携带任意 HTML。

10.4 大小与结构限制


11. 第三方验证

任何持有存储字节(以及私有/封装量子比特的 K)的第三方都可以 在没有量子比特合作的情况下验证加密产物。独立地 带时间戳的存在性主张还需要经过验证的 per-qub 永久存储包含或经过验证的 §16 透明日志证明。

1. Obtain the stored bytes. For a private delivery, also obtain K from the
   delivery-link fragment.
2. Resolve the delivery shape (§8 step 3a): unwrap OuterWrapper with K, or
   accept bare SealedQubCbor. Require shape/visibility agreement.
3. Parse SealedQubCbor → SealedQub; validate protocol version, visibility,
   content type, chain identity, and structural bounds.
4. Recompute expected_round from unlock_at (§4.3); require the stored round
   (allowing the documented legacy minus-one case) and ciphertext-stanza round
   to agree exactly as §8 step 6a specifies.
5. Obtain the drand signature for SealedQub.drand_round and verify its BLS
   signature against the pinned chain public key.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify envelope/sealed equality for qub_id, unlock_at, and outcome_at.
10. Recompute qub_id from the decrypted fields, sealed drand_round, and title;
    require it to equal the carried qub_id (§4.1).
11. If sig_alg != 0x00, verify the V2 author signature (§9.4). If cosigner
    fields are present, verify their pairing, key separation, and signature
    (§9.7).
12. For an existence-time claim, independently verify either:
    a. the permanent-storage transaction's data-to-id binding, owner, block
       inclusion, and block timestamp; or
    b. a §16 inclusion proof through its pinned anchor and anchor block time.
13. Report each verified claim separately; do not collapse an absent storage
    or anchor proof into a successful timing verdict.

什么验证证明:

证明输入 它所建立的
有效捆绑包/密封产物 + drand 签名 恢复的正文与 body_hash 一致;绑定至 qub_id 的元数据完好;密文绑定到所声明的 drand 轮次;且该轮次已经结束。这不能确定密文的创建时间。
有效的 V2 作者/共同签署人签名 相应密钥的持有人对§9.3中签名的表面进行了认证。
独立验证的每量子比特存储交易 精确存储的密文存在时间不会晚于其区块时间戳。
有效的锚点透明度日志证明 §16.11中的叶子种类特定要求,包括来自锚区块的上限承诺时间。

验证不能证明的内容:

非证明 为什么
作者身份 这 sender_label 是装饰性的。没有 sig_alg ≥ 0x01,任何人都可以封存此内容。
意图 该产物证明的是字节和加密关系,而不是创作者的主观意图。
已有的承诺来自 .qub 独自 创作者可以在绑定回合结束后组装一个有效的捆绑包。嵌入的 drand 签名证明了回合已经结束,而不是证明密文在此之前存在。
精确封印按钮时间 存储或锚定区块时间戳是一个可以独立验证的上限,并且可能落后于用户的本地操作。 sealed_at / received_at 断言是非证据性的。

已实施的透明度日志 (§16) 将验证扩展到 qubs 上 防篡改 订购 和无信任的 上界承诺时间 (那 锚块时间),按叶子类型范围(§16.11)。它不会添加作者身份或 意图;对于默认的盲字节上传路径,它本身并不证明 body_hash 或 drand_round,这些问题仍然来自产物检查。


12. 版本控制与发布管理

文档发布、内部线协议和外部包装是分开的 版本空间。因此,仅文档的澄清不会默默地 更改字节,未来的电汇迁移不能伪装成编辑内容 修订。

12.1 文档发布版本

本规范使用语义文档版本(MAJOR.MINOR.PATCH) 和 一个不可变的 Git 标签名 protocol-v<release>.

发布状态是其中之一 草稿 (尚未规范), 当前 (唯一的 推荐的实施目标),或者 已被取代 (为历史保留 验证)。未版本化的 /protocol 路由显示当前版本; 发布标签保留其确切的源代码以及随之发布的每个语言版本。 更改状态或版本号需要更新此表和发布 在相同的审查更改中的历史。

文件发布 生效日期 状态 线协议 包装器 源
1.0.0 2026年9月23日 当前 0x01 0x01 protocol-v1.0.0

12.2 协议版本

这 version 在两者中的字段 (u8) SealedQub 和 QubEnvelope 标识主要协议版本。

12.3 协议版本历史

版本 价值 描述
v1 0x01 私有/封装和公共/裸交付;文本(0x01), 条约 (0x03)和判决(0x04) 机构;ML-DSA-65 V2 作者/共签署人签署;drand quicknet tlock;SHA3-256。

12.4 向前兼容性

遇到带有未知 CBOR 映射键(键未按 §3.2 规范顺序排列)的 QubEnvelope 的 v1 查看器必须以解码错误拒绝它(§3.1)。前向兼容性依赖于 version 字段,而不是键容差:未来的添加——即使是微小的元数据——也会在新的下发布 version 值,v1 查看器会以明确的“更新的协议”错误拒绝,而不是悄悄丢弃签名所承诺的内容。

遇到 v1 查看器 sig_alg = 0x01 (ML-DSA-65) 但是如果缺少 ML-DSA-65 验证支持,应显示带有“签名存在但不可验证”提示的 qub 内容,而不是完全拒绝 qub。今天的参考实现会拒绝所有 sig_alg 除……之外的值 0x00 和 0x01 因为 v1 注册表不包含其他有效算法 — 在第三个算法注册之前,严格拒绝和软失败在观察上是相同的。一旦 §9.2 允许新的条目,上述软失败行为将变得关键,并且参考查看器届时将更新为软失败。

12.5 外部包装版本

§13中描述的OuterWrapper携带它自己的 version 字节, 独立的 的 SealedQub.version 和 QubEnvelope.version两个版本空间分别独立演进:未来的后量子安全对称替代方案会提升封装字节而不接触内部协议版本,而未来的协议层新增(例如,一个新的信封字段)会提升内部版本而不接触封装字节。

OUTER_WRAPPER_VERSION_* 价值 算法 状态
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM,使用12字节随机数、16字节认证标签,绑定附加认证数据(AAD) qub_id 启用私人投递
— 0x02–0xFF 保留 未来

观众必须以明确的错误拒绝未知的封装版本。该协议故意将封装版本范围保持狭窄,直到出现具体的迁移驱动(例如,NIST 指南倾向于使用不同的 AEAD);一个 0x02 槽将在引入该算法的同一版本中分配。


13. 外层加密封装

13.1 理由

协议各层(QubEnvelope → tlock → SealedQub)使已封印 qub 变为时间锁定:主体在 unlock_at 与 drand 轮签名发布之前不可读。然而,在解锁后,轮签名是公开的,且 SealedQub 的规范 CBOR 形态可被识别,因此索引过永久存储交易的采集者可批量解密整个 qub 语料库。

对于私有传输,外层加密包装通过在规范层之间插入额外的对称 AEAD 层来关闭该通道 SealedQubCbor 以及存储的字节。在浏览器密封路径中,256 位密钥 K 生活 只有 在交付 URL 的 URL 片段中以及用户设备上;浏览器不会将 URL 片段传输到服务器,因此 qub.social、每个存储网关以及其前面的每个 CDN 在观察上都无法看到 K因此,私有 qub 的存储表示是不可见的密文,其明文在没有创建者选择共享的 URL 的情况下无法恢复。公共传递故意省略了这一层(§13.8)。

净效应:

13.2 分层

plaintext body                       ← QubEnvelope.body (§2.2)
  ↓ canonical CBOR (§3)
envelope CBOR
  ↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
  ↓ canonical CBOR (§3)
SealedQubCbor bytes                  ← inner wire artifact
  ├─ public (visibility=0x01) ───────────────▶ stored directly (§13.8)
  └─ private (visibility=0x00)
       ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12, this section)
     OuterWrapper CBOR bytes         ← stored private payload

协议层的封印与公开(§7、§8)在封装边界以下保持不变;封装在 seal() 调用点附加,在 unlock() 调用点解除。

13.3 OuterWrapper 数据结构

struct OuterWrapper {
    version:    u8,           // 0x01, see §12.5
    qub_id:     [u8; 32],     // copied from inner SealedQub; AEAD AAD
    nonce:      [u8; 12],     // 96-bit AEAD nonce
    ciphertext: Vec<u8>,      // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}

字段不变量。

CBOR 编码。 按 §3 规范 CBOR,使用相同的键排序规则(先按编码字节长度升序,再按字典序)。四个键为:

键 编码字节 顺序
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

因此 OuterWrapper CBOR 的首字节是 4 项映射的定长映射头(0xA4)。

13.4 AAD 与 qub_id 的绑定

封装将 qub_id 作为 AEAD 附加认证数据绑定。这是抵御以下三类攻击的承重结构性防御:

攻击 防御
将密文移动到不同的地方 qub_id 包装器中的字段 AAD 不匹配 → AEAD 认证失败
将 qub A 的 URL 片段与 qub B 的存储字节混合 错误的密钥(以及独立绑定的AAD)→AEAD认证失败
篡改 qub_id 上传后的包装字段 AAD 不匹配 → AEAD 认证失败

在封装明文中携带 qub_id 并不会显著削弱枚举免疫——qub_id 本身是 §4.1 预映像的 SHA3-256 哈希,从摘要无法恢复其预映像,且已采集到封装字节的枚举者从可见的 qub_id 中所学到的,并不多于其从上传存在这一事实本身可推断的。

13.5 封装与解封算法

wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
    require K.len() == 32 and N.len() == 12 and Q.len() == 32
    I := canonical_cbor_decode(S) as SealedQub
    require I.qub_id == Q               // reject mismatched caller AAD
    C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
    // C includes the 16-byte authentication tag at the end
    return canonical_cbor_encode(OuterWrapper{
        version:    0x01,
        qub_id:     Q,
        nonce:      N,
        ciphertext: C,
    })

unwrap_sealed_qub(OuterWrapper bytes W, key K):
    require K.len() == 32
    O := canonical_cbor_decode(W) as OuterWrapper
    require O.version == 0x01           // §12.5
    P := AES_256_GCM_decrypt(
            key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
         )
    // any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
    S := canonical_cbor_decode(P) as SealedQub
    require S.qub_id == O.qub_id       // explicit inner/outer cross-check
    return P                            // P is the validated SealedQubCbor

失败模式坍缩。 错误的 K、错误的随机数、AAD 不匹配以及被篡改的密文都会产生相同的 DECRYPT_FAILED 错误。这是一种有意为之的 AEAD 特性:区分失败模式会构成远端攻击者可通过发送畸形封装并对响应计时来探测的旁路。参考实现必须将所有 AEAD 失败坍缩为单一错误形态。

13.6 密钥材料与分发

封装密钥 K 是由 CSPRNG 为每个 qub 生成的 256 位均匀随机值。参考实现的来源为:

分发:K 必须编码为 URL 安全 base64(RFC 4648 §5,无填充)并作为片段分量附加到传递链接:

delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>

符合规范的浏览器不会将片段发送至任何服务器。在用户设备之外持久化完整传递链接(包含片段)的恢复渠道——例如服务器端历史索引、可选的邮件自动发送——是对默认加密粉碎姿态的明确折中,必须由用户显式同意。

片段丢失。 若用户丢失 URL 片段且没有恢复渠道,qub 将不可读。这是设计中的承重折中,且必须在封印时向用户披露。MVP 通过明确的"保存此 URL"文案以及对选择启用的用户提供的已验证邮箱恢复渠道,加强封印时的披露。

13.7 本节范围之外

13.8 公开 qub(省略封装)

外层包装是 在交付层可选. 创作者可以封印一个qub作为 公共的,在这种情况下,规范的 SealedQubCbor 进入存储管道 直接地,没有 OuterWrapper 图层而无键 K:

SealedQubCbor bytes  ──(public)──▶  stored as-is
SealedQubCbor bytes  ──(private)─▶  AES-256-GCM(K, …) ▶ OuterWrapper ▶ stored

一个公共酒吧是 时间锁定但不链接限制:它在其随机轮次发布之前保持不可读(tlock 层保持不变),但解锁后,任何拥有存储交易 ID 的人都可以解密它——不需要 URL 片段,因为没有 K. 这是服务器必须推动的表面交易:揭示通知的电子邮件、无片段的 oEmbed/自动嵌入链接,以及更丰富的揭示后 SEO,都需要一个无需服务器持有的秘密即可工作的链接 (§13.6)。私有 qub 仍然可以使用显式的 <qub-embed src="full_delivery_url"> 当出版商提供其完整的片段承载能力时形成。

生产者必须考虑的后果:

私有(已封装)仍为默认;公开是创作者按每个 qub 作出的显式选择。


14. 测试向量

14.1 qub_id 推导

Input:
  version      = 0x01
  content_type = 0x01
  created_at   = 1735689600 (2025-01-01 00:00:00 UTC)
  unlock_at    = 1736294400 (2025-01-08 00:00:00 UTC)
  outcome_at   = absent
  drand_round  = 4695446  (= floor((1736294400 - 1595431050) / 30) + 1, §4.3 mapping, drand mainnet params §14.2)
  body         = "Hello, future."  (UTF-8, 14 bytes)
  title        = absent

Intermediate:
  body_hash  = SHA3-256("Hello, future.")
             = 76ab8b3f843c6ed4f2d0fd75b9f457b4
               ad49dd4450f9c22723ae430e3af3211d
  title_hash = [0u8; 32]   (title absent — §4.2.1 sentinel)

Domain separator (10 bytes):
  [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]

Preimage (108 bytes—current protocol v1):
  domain_separator    ||  // 10 bytes
  0x01                ||  // version
  0x01                ||  // content_type
  0x0000000067748580  ||  // created_at as i64 big-endian (1735689600)
  0x00000000677DC000  ||  // unlock_at as i64 big-endian (1736294400)
  0x0000000000000000  ||  // outcome_at_or_zero (outcome_at absent)
  0x000000000047A596  ||  // drand_round as u64 big-endian (4695446)
  body_hash           ||  // 32 bytes
  title_hash              // 32 bytes (all-zeros sentinel; title absent)

Expected output:
  qub_id = SHA3-256(preimage)
         = 4a84e3dfaec32954949c30073f8e6506
           fd3204c1bb97f9162b81c7587afe412e

实现必须产生相同的 body_hash 和 qub_id 该输入的数值。这个测试向量应当是编写的第一个单元测试。上面的规范值是通过参考实现计算得出的,必须逐位匹配。历史上的预发布原型布局(前两个没有依赖于任何实时量子比特)之前使用了92字节 outcome_at (3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0) 并且在添加后为 100 字节 outcome_at_or_zero (b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed). 然后添加了当前的108字节布局 drand_round 和那个 QUB_ID_V2 域分隔符。早期的108字节向量使用了遗留的 ceil 循环映射(drand_round = 4695445) 并产生 3a9fcb31b750d985c262fada6d4f777fd6a28be831d941d85c131f5a4bbaf8a4——仍然有效 qub_id 对于该轮输入,上述例子遵循§4.3当前轮映射。

14.2 解锁轮映射

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

Calculation:
  (1735689600 - 1595431050) / 30 = 4675285.0
  floor(4675285.0) + 1 = 4675286

drand_round = 4675286

第4675286轮发布于 1595431050 + (4675286 - 1) * 30 = 1735689600——正好在 unlock_at,从未有过。(遗留预发布 ceil 映射给 4675285,发表于 1735689570—提前30秒;核验员根据§4.3接受该传统轮次。

14.3 规范 CBOR 往返

实现必须验证对所有有效输入都满足 serialize(parse(serialize(qub))) == serialize(qub)。这是一项性质测试,而非单一向量。

14.4 PactTerms CBOR(content_type 0x03)

Input:
  pact_version = 1
  title        = "Scooter deposit"
  terms        = [
    { key: "Item",    value: "Honda Metropolitan scooter" },
    { key: "Price",   value: "$100" },
    { key: "Deposit", value: "$10" }
  ]
  party_a      = { label: "Alice" }
  party_b      = { label: "Bob", contact: "bob@example.com" }
  notes        = absent

Canonical CBOR key order (PactTerms):
  "notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)

Canonical CBOR key order (PactTerm):
  "key"(4) < "value"(6)

Canonical CBOR key order (PartyIdentifier):
  "label"(6) < "contact"(8)

规范 CBOR 字节与 SHA3-256 body_hash 由参考实现计算。对此输入,实现必须产出字节相同的 CBOR。

实现还必须验证对所有有效 PactTerms 输入都满足 serialize(parse(serialize(pact))) == serialize(pact)(性质测试)。

14.5 外层封装跨语言向量

外层封装(§13)的规范固件位于 crates/qub-core/tests/vectors/wrapper_v1.json。每个用例将一个 (key, nonce, qub_id, sealed_cbor) 元组固定为不透明十六进制输入,并断言一个特定的 expected_wrapper_hex 输出。两套参考实现消费同一个 JSON 文件:

该夹具目前固定了三个低级包装器案例。它们测试确定性 OuterWrapper 编码和 AEAD 的互操作性独立于 §13.8 交付形状不变性;特别是,历史名称 basic-text-public 以及它的内部 visibility = 0x01 做 不 使生成的封装字节成为符合规定的公共传输。生产者仍然必须以裸形式存储公共内部字节,并且仅封装私有字节(0x00) 内部字节。

案例 报道
basic-text-public 历史低级别夹具名称。最小的实际值 SealedQub 形状,没有可选字段;仅测试包装字节,并且不是符合 §13.8 的存储传递。
with-recipient-pubkey SealedQub 与 recipient_pubkey 设置(保留的未来路径)。使用不同的内部 CBOR 密钥集;其独特的固定内容独立地产生不同的 qub_id (recipient_pubkey 它本身不在 §4.1 的原像中。
longer-body ~4 KiB 的主体——在内部信封和外部密文中都对多字节 CBOR 长度前缀进行练习。

对已记录的输入,实现必须产出字节相同的 expected_wrapper_hex。重新生成该固件需要 QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors,且仅用于刻意的格式变更。


15. 加密配置治理(未来)

本节对 v1 而言是参考性的,且在 qub 任一加密原语中首次进入第二个算法时变为规范性。

15.1 当前姿态

协议 v1 对每种原语恰好绑定一个算法:

验证器当前针对每个活动原语硬编码密钥和签名长度。 sig_alg 而 wrapper-version 字节是显式选择器,但 v1 不执行带内协商,并且只接受上述活动值。

15.2 目标形态

当第二个算法进入协议时,验证者将以名称化的 CryptoProfile(例如 ExqubV1)进行配置,列出每种原语所允许的精确取值集合——sig_algs、drand 链、封装版本、内容类型。该配置在验证时固定,决不在带内协商。任何不在活动配置内的取值都将被拒绝。

这保证添加 ML-DSA-87 或启用 Ed25519 不能追溯性地削弱已有的验证者配置:v1 验证者在 v2 配置发布之后仍然是 v1 验证者。

15.3 触发条件

当出现以下任一情况时,将 §15 提升为规范性状态:

在此之前,§15 是一个占位章节,用于固定迁移形态,使未来的 PR 朝既定目标落地,而非从零开始重新争论协商接口。


16. 透明日志与持久性层级(设计——评审完成)

状态。 本节已在 W5/UP-B1 第 1–8 阶段实现,下面明确说明生产端和信任根的范围。线格式、哈希和验证路径均已投入使用:核心 Merkle 与规范 CBOR 类型(qub-core)、TypeScript 对应实现与 ANS-104 打包器(workers/api/src/crypto/)、单写入者 LogDO 与按坐标索引的 R2 节点存储、/upload 日志追加尝试、每日锚定与打包器排空定时任务、GET /api/v1/qub/:tx_id/proof 包含证明端点、GET /api/v1/log/consistency RFC 9162 一致性证明端点、.qub 捆绑包携带的类型化包含证明(§17.5)、原生 ANS-104 锚点验证器(tools/qub-verify),以及双重自行发布树头的挂钩(§16.6)。成功的 /upload 始终先持久写入 R2;仅当已配置 LOG_DO 且内联追加成功时才受日志覆盖,也只有此时响应才包含 log_seq、receipt 和 anchor_status。如果 RECEIPT_SK 缺失或无效,收据的 sig_b64url 为空,不能提供不可否认性。当前 /seal 和 pact 发布路径会安排单独的 Arweave 交易,但不会追加日志叶;也没有代码会在追加失败后执行 /upload 注释所提议的后续协调。W5 外部审查已完成:§16.15 记录设计决策和发布约束,但不会扩大上述生产端覆盖范围。三个信任/部署事项仍受门控:(a) 专用锚点钱包(ANCHOR_JWK;LogProfile.anchor_owner 仍是 [0xAB; 32] 占位值);(b) 收据签名密钥及匹配的公钥固定值(RECEIPT_SK 可选,且 LogProfile.receipt_pubkey 当前为空);(c) 自行发布树头所用的 GitHub 仓库与令牌(§16.6)。在锚点和配置文件固定值完成配置前,独立验证器会如实报告证明状态,不会声称已完成锚定并固定信任根的验证。该设计完全采用附加方式,不会更改 SealedQub/QubEnvelope 线格式。

16.1 理由与持久性层级

当前的发布路径将确认与 Arweave 确认分离:它们派生并签署单独的交易,将产物和精确的提交状态保存在 R2 中,然后异步发布。透明日志为一般子集增加了一个独立锚定的排序层 /upload 请求的那些 LogDO 追加成功:

层 姓名 保证 什么时候
T1 R2-首次同步确认 持久性底线——在返回成功之前,将密封的字节和确切的发布状态写入持久存储。 已在当前出版路径中实施。
T2 批量透明度日志包含 仅追加、可防篡改的承诺 + 一旦包含并锚定后的全序。 当前生产者:成功 LogDO 来自追加 /upload; 响应携带收据元组。不是通用的。
T3 每份 Arweave 永久性 针对 qub 的单个 Arweave 交易。 目前为每个被接受的出版物准备,并异步发布;准确签署的交易将保留在可排放的发件箱中,直到交付。

这些等级描述了不同的证据和持久性特性,而不是当前的商业计划。目前的代码仍然为每一个被接受的发布安排单独的 Arweave 交易;它并没有把 T3 仅作为付费升级来提供。API 密钥/账户配额上限仍然是独立的应用控制。

耐用性 诚实 T1 写入是同步的,因此成功的响应在不等待 Arweave 网关的情况下建立应用层的持久性。它本身并不建立独立的时间戳。已确认的单个交易提供其区块时间的上限。对于携带完整 T2 收据元组的响应,下一个已确认的锚点可以提供下面描述的日志证明。如果元组缺失,则没有任何表面可以表明该 qub 已经在透明日志中。锚点和发布延迟在协议层面没有数值 SLA。

16.2 LogLeaf 结构(两种已确定的形态)

一条日志条目即一个 LogLeaf,按 §3.1 profile 以手写规范 CBOR 编码(定长、无标签、无浮点、最短形整数、NFC 文本、缺失时省略可选字段、键先按编码字节长度升序再按字典序排列)。§3.1 的 parse → re-encode → compare 规范守卫在哈希之前的编码路径上应用(不仅在解码时),因此两个实现不会因整数宽度或键序差异而对叶字节产生分歧。所有整数为 u8 / u64 / i64;所有摘要为 32 字节的字节串(bstr[32])。已存储的永久存储交易 id 是一个原始的 32 字节 SHA-256 摘要,以 bstr[32] 携带,决不是 base64url 文本字符串(与 §3.3 一致)。

这片叶子有 由一个选择的两个形状 kind 字节,因为通用的上传路径是字节盲的: POST /api/v1/upload 故意将两种已接受的有效负载形状都视为不透明并接收 qub_id 和 unlock_at 仅作为不可信的客户端声明。在默认私有路径上, body_hash, drand_round, created_at,和 drand_chain_version 还被隐藏在 §13 外层包装中,其钥匙工人从未持有。类型系统还为衍生的生产者定义了一个经过认证的形状 body_hash / drand_round 它本身。当前 /seal 路由有这些值但没有调用 LogDO, 因此目前生产仅排放已断言的(0x02) 来自成功的通用上传附加的叶子。拆分保持每个已提交的值真实,而不假装所证明的生产者已连接:

钥匙 封装长度 类型 存在 意义
seq 四 u64 必需的 全局零起点叶子索引;包含证明所提交的位置。
kind 五 u8 必需的 0x01 已证明的-有能力的(已定义,但当前未发出)或 0x02 断言(客户端密封 / 字节盲上传)。
ref 四 bstr[32] 必需的 叶参考ID。认证 → 原始 qub_id. 断言 → the 失明的 身份证 SHA3-256(qub_id ‖ log_blind_secret) (第16.2.1节)。
chash 六 bstr[32] 必需的 内容地址 SHA3-256(stored_bytes) ——这是唯一一个内容,工人无论走哪条路径都能始终诚实地计算出来。
unlock_at 十 i64 必需的 复制(已证明)或声称(声明的);已验证 > 0 在它进入叶子之前。
received_at 十二 i64 必需 R2-ack 上的工作时钟。 非证据性的 (由操作员断言;§16.6)。用于自我描述,从不作证明。已验证 > 0。
body_hash 十 bstr[32] kind=0x01 只有 省略于 0x02 ——根据第13条,该工人缺乏它。
drand_round 十二 u64 kind=0x01 只有 省略于 0x02.

一个 kind=0x02 叶有意既不承诺 body_hash 也不承诺 drand_round:它认证的是一段位于内容地址 chash 的不透明密文的承诺与排序,并声称 qub_id 与 unlock_at——而非其明文或轮次。已断言 qub 的明文/轮次部分来自现有的 §11 .qub 包验证,而非来自日志(§16.11)。drand_chain_version 不在叶中(在默认路径上它位于封装之内);链粒度归属于锚(§16.7)。编码方纪律:拒绝全零的 ref 或 chash,并拒绝非正值的 unlock_at / received_at,与 cbor.rs 中的 outcome_at > 0 哨兵守卫保持一致。

16.2.1 私有 qub 盲化

日志决不能沦为 §13 外层封装本要防止的枚举预言机(§13.1)。对一个私有(已封装)qub,asserted 叶承诺盲化后的标识符 SHA3-256(qub_id ‖ log_blind_secret),其中 log_blind_secret 是服务器持有的密钥,并省略 body_hash。第三方无法将这样一个叶与某个特定 qub_id 关联;而该 qub 的持有者,因其拥有传递链接、从而拥有 qub_id,可重新计算盲值以确认自己的收录。一个公开 qub(已可枚举、已按 §13.8 携带 Visibility: public 永久存储标签)承诺原始 qub_id。这是独立可验证性有意让位于一项承重隐私不变量的唯一之处;私有 qub 的独立关联是 chash(§16.9)。

log_blind_secret 保管(已裁定——§16.15 Q4)。 盲值保护的是叶的不可关联性,而非明文机密性(§13 封装独立地负责后者)。在 log_blind_secret 遭泄露时,对任何攻击者已持有或可重构的 qub_id(凡其拥有包/URL 的每个 qub,以及任何低熵或公开的 qub_id),它都能在一次哈希内重新计算叶 ref 并将其关联——这是对一个已知群体的直接关联,而非对一个未知空间的暴力搜索。将 log_blind_secret 归类为与其他服务器密钥同一保管层级的关联/Sybil 级密钥,且仅向前轮换(一次轮换会对未来的叶重新盲化;它无法追溯地解关联已锚定的叶)。

16.3 叶与节点哈希

采用 RFC 6962 §2.1 的域分隔哈希,并将 SHA-256 替换为 SHA3-256:

leaf_hash      = SHA3-256(0x00 || canonical_cbor(LogLeaf))
node_hash(l,r) = SHA3-256(0x01 || l || r)
empty tree     = SHA3-256("")          // defined but never anchored

域前缀字节 0x02(条目链,§16.4)与 0x03(STH 哈希,§16.6)已保留且与上述不相交。它们是单字节,因此不会与现有的 10 字节 ASCII 域分隔符(QUB_ID_V2 等)冲突。该树是 RFC 6962 的左满非平衡树(每个内部分裂点取严格小于子树叶数的最大 2 的幂),这使收录证明与一致性证明可共用同一套审计路径算法。参考规范携带显式的左/右推导伪代码,并固定一个非 2 的幂(5 叶)的测试向量,以便覆盖右边缘提升情形——这正是 4 叶向量会掩盖的情形。

16.4 哈希链接(内部)

LogDO 维护一条仅用于崩溃一致性的内部条目链。它从不发布、从不面向验证者:

entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1]  = SHA3-256("QUB_TLOG_GENESIS_V1")

已发布的仅追加权威是累积 Merkle 根 + 其锚(§16.5–16.6),而非运营方恰好服务叶时所采用的原始顺序:无论以何种顺序服务,该链都会重新计算,因此只有已锚定的根才固定规范位置。

16.5 累积 Merkle 树与批量化

存在一棵不断生长的 RFC 6962 树,覆盖按 seq 顺序排列的所有叶——而非孤立的逐批次树。(一种以进位叶链接的逐批次构造被否决:它不是真正的前缀关系,因此其"一致性证明"是不可靠的。)累积树提供真正的 RFC 9162 一致性证明,并使单个近期的锚即可为任何更早的 qub 证明收录。

这 LogDO Durable Object 是 单一作家 (blockConcurrencyWhile,镜像 QuotaDO / EntitlementDO) — 将内容追加到共享日志是对共享状态的读-改-写操作,因此必须通过 DO,而不是 KV。它缓存了树的右边缘前沿(O(log n) 哈希)所以关闭一个批次是 O(batch). A 批 是锚定在一起的叶子集合;其实现的触发器是一个 tree_size 至少提前 LOG_BATCH_MAX_LEAVES (默认 4096),年龄达到锚定节奏,或明确的管理/定时任务强制关闭。 root_i 是对叶子节点的累积Merkle树哈希 0 .. tree_size_i.

16.6 经由永久存储锚的已签名树头

永久存储锚交易就是已签名树头,并就树头本身而言替代了运营方签名:每日锚不需要任何 qub 密钥,因为永久存储交易的 owner 即是签名。护城河论点成立——承重的是那不可变的底层基质,而非某个 qub 持有的密钥,来支撑已锚定的根。

日志设计要求一次成功的附加操作 收据钥匙 (§16.10),固定在 LogProfile 并由...交叉签署 anchor_owner. 当前的实现尚未完成该信任根的配置: RECEIPT_SK 是可选的,缺失/无效的键将产生 sig_b64url: "",以及编译后的 LogProfile.receipt_pubkey 是空的。这样的收据可以描述附加的页,但 不 不可否认的签署收据。只有在验证者释放固定匹配的公钥并且锚点所有者对其进行交叉签名后,更强的设计声明才适用。没有完整收据元组的发布响应不会提出日志接受声明;具有空签名的响应会提出追加位置声明,但不会提出签名验证声明。

SignedTreeHead 是规范 CBOR(键按编码长度排序):size:u64、root:bstr[32]、batch:u64、prev:bstr[32](前一个 sth_hash;创世 = 32 个零字节)、log_id:bstr[32]、first_seq:u64、anchored_at:i64。其哈希为 sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead))。

固定的信任根。 log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address)。符合规范的验证者必须要求 anchor_tx.owner == LogProfile.anchor_owner,其中 anchor_owner(以及回执密钥的公钥)作为 LogProfile 烘焙进 qub_core——与 DrandTimelockProvider::quicknet() 中已有的 quicknet 常量并列——并随验证者二进制文件分发。验证者还必须在本地验证永久存储交易的 数据 → tx_id 绑定,而非信任网关的 /raw/ 响应。这关闭了流氓钱包的两面性漏洞:"已锚定在永久存储上"在验证者固定是哪个钱包之前毫无意义。

轮换是一项 §15 治理扩展,而非复用(已裁定——§16.15 Q3)。 §15.2 的 profile 面目前只枚举 sig_algs / drand 链 / 封装版本 / 内容类型,而 §15.3 的触发条件未列出其中任何一项——LogProfile / anchor_owner 尚未进入 §15 的面。因此轮换治理必须构建:§15.3 被扩展(见下文)以加入 LogProfile 触发条件,而一次轮换是一个已签名的 LogProfile 升级,随验证者更新一同发布。一次有计划的轮换携带 待退出 → 待接入 的交叉签名;一次由泄露驱动的轮换则无法携带(恰在此时,待退出的密钥不受信任/不可用),并回退至由 §15 治理的升级,同时借助 prev-锚 fork 检查(见下文)在过渡期内限定损失。

含糊窗口(一类信任参数)。 只有当其覆盖锚是 Arweave 时,叶子才是抗模棱两可的已确认窗户是 received_at → anchor confirmation (节奏 + Arweave 最终性,没有协议延迟保证)。在信任根配置之前,当前的实现提供了 qub 的操作完整性以及存在的任何未签名追加元数据;它不提供计划中的不可否认性保证。三个问责人产物定义了完整的设计(见证模型是 §16.15 Q2 的决议):

  1. 密封收据(依赖供应) — 当上传的日志追加成功时,SCT 类似物会返回 (§16.10)。只有在 sig_b64url 非空 和 匹配的公钥/锚点所有者关系已在验证器中固定。目前为空的生产配置文件固定不能支持该裁决。此控制不适用于省略的收据元组或未签名的收据。
  2. 已发布的监控方法 + 前链行走 ——锚 prev 链条是从头部→创世纪行走;一个分叉(一个上的两个锚) size 与不同的 root,或破碎的 prev) 是可发表的不当行为证据。模棱两可的检测是一项明确的操作承诺,而不是默默的假设。
  3. 双重自出版头部 —— 每个新头 {sth_hash, tree_size} 已发布到专门由qub拥有的 公共、仅追加的 GitHub 仓库 (承重防篡改自我出版腿),仅以社交帖子作为尽力的佐证。失败的发布必须发出页面通知(而不是默默失败)。*已实现(第8阶段)作为 publishHead 挂在锚上的 cron (workers/api/src/utils/heads-publish.ts): a PUT 而没有内容 API sha 是只追加的 (a 422 意味着头部已经发布,绝不会被覆盖);选择加入 / 部署门控 PUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO} 并且在存储库被配置之前处于不活跃状态。通过硬性 GitHub 失败页面 health_alert 通道并触发耐用故障指标(m:tlog_publish_head_fail); Arweave锚本身在发布失败时从不回滚。“不静默失败”由该持久化指标保证——即使最尽力的邮件页面无法发送,运维人员也必须在仪表盘上设置警报。基于“提前锚定”(cron仅在大小增加时才发布)的两个真实限制如下:一次临时的GitHub故障会导致 间隙 在该大小的已发布头序列中——有界的,而非静默的(它进行分页),并且因为每个头都提交一个超集树,§16.9 的一致性证明弥合了这一差距;关键是,该一致性证明是从……计算的 权威的 Arweave 锚定树,而非来自 GitHub 表面因此,GitHub 的缺口从不会削弱可验证性。填补已发布版本缺口的补充回填是一种延迟的增强。*

诚实约束(约束条件)。 因为 qub 控制了两个计划发布面,因此这是 自助出版,未被独立见证。任何产品、市场或法律表面均不得声称日志是“独立见证”的。在收据/档案/头门配置完成后,允许的声明是 含糊其辞是可以被检测到的,并且成功签署的附录会留下不可否认的收据. 在那之前,该主张不可使用。真正独立的第三方证人将推迟到未来§15治理升级时处理。

received_at 由运营方断言,且任何主张都不得依赖它——它决不在任何产品 / 法律 / API / 证明渲染界面上作为证明或争议佐证而呈现。永久存储锚区块时间 T 是唯一的无需信任时间戳("已记录于"的一个上界)。任何针对 received_at 的监控合理性检查都必须与 T 比对,而非与运营方控制的 anchored_at STH 字段比对;此类检查仅是针对一个诚实运营方时钟错误的守卫,而非针对恶意运营方的问责控制(§16.15 Q5)。

16.7 锚交易格式与节奏

AnchorBundle 是经由 §16.8 打包器写入的规范 CBOR 永久存储交易体:ver:u8、sth:bstr(规范 SignedTreeHead 字节)、prev_anchor:bstr(前一锚交易 id 的原始字节;创世时省略)、chain_hash:tstr(生效的 drand 链——quicknet),以及该批次按 seq 顺序排列的叶 CBOR 流,使锚自包含:监控方可零 qub 依赖地从交易体重新推导 root。(若叶流在高流量下变得很大,未来的修订可能仅按引用承诺一个叶范围;此处记下,但 v1 不采纳。)

永久存储标签有意可枚举——日志本就应被找到,与私有 qub 不同:App-Name: qub-tlog、Anchor-Format: 1、Log-Id: <hex>、Batch: <n>、Tree-Size: <n>、Root: <hex>、Prev-Anchor: <tx>、Content-Type: application/cbor。标签是不受信任的提示;CBOR 体才是唯一权威。

节奏: 默认情况下为每天,结合数量重新审视(大小触发器在负载下会自动缩短有效频率)。当前的生产者没有实现付费封印强制锚定挂钩。 Anchor 钱包 是专用且低速的,独立于上传钱包——它必须是它自己的 自己的 JWK (一个独立的密钥,而不是上传钱包上的逻辑角色),因此上传钱包被攻破也无法伪造锚点——每天有严格的锚点交易预算。托管立场明确说明:一个 窄范围热键,配有紧密断路器和低电平,而不是“冷”——一个每天自动签名的钱包不可能是冷钱包,并且规范也不假装它是冷钱包。

16.8 ANS-104 打包器

一个自研的 ANS-104 DataItem 编码器与 deep-hash 签名器,大约 300 行,仅用 Web Crypto,零 npm 依赖(两个 Turbo SDK 都通不过 npm ci --ignore-scripts 供应链门)。DataItem 字节布局:

signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data

签名采用永久存储的 deepHash——一个对 ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] 的递归 SHA-384 摘要(永久存储的传输要求,crypto.subtle.digest("SHA-384"))——再以钱包 JWK 经 crypto.subtle 对该 deep hash 做 RSA-PSS;id = base64url(SHA-256(signature))。此处的 SHA-384 被隔离为仅供永久存储传输使用的原语,决不是 qub 信任原语(§15 记录了这道围栏;qub 信任哈希通篇为 SHA3-256)。

ANS-104 代码路径服务于延迟回退/排水机制并进行写入 AnchorBundle 数据项。普通的发布路径首先创建一个精确签名的 Arweave 交易,并将其 JSON 保存在持久的发件箱中;直接发布是一种延迟优化,而排空路径会在应用其捆绑器回退之前重试相同的交易。 签名方案(已解决 — §16.15 问题8):v1 使用 RSA-PSS(签名类型1)进行签名 重用现有的 Arweave 钱包 JWK 机制(无需管理新的长期密钥,符合“减少一个秘密”的理念);Ed25519 推迟到§15 PQ 迁移路径。

手写的 deep hash 是 W5 中风险最高、天然覆盖率最低的代码,因此其门控是不可商量的(§16.15 Q8):

  1. 跨语言固件 tlog_v1.json(Rust + TS,§14.5 wrapper_v1.json 模式)覆盖 deep-hash、DataItem 字节 + id、叶哈希、一个 5 叶根 + 审计路径、一个 STH 哈希、一个收录证明,以及一个一致性证明——在签名与验证两个方向上(验证方向之所以重要,是因为 §16.6 的本地 交易 → tx_id 检查把 deep hash 拉入了每个独立验证者,而不仅是写入者)。
  2. 经由一个参考 ANS-104 打包器进行一次性的互操作往返,作为仅供静态测试的数据消费——决不作为 npm 运行时依赖(仅 Web-Crypto / 无安装脚本的姿态依然成立)。
  3. deep-hash + RSA-PSS 路径必须经由与生产环境相同的 crypto.subtle 原语往返,使自研编码器在字节层面兼容。
  4. 一个持续运行的打包后接受度监控确认每个 锚 / 回退 DataItem 实际获得永久存储接受,并带告警 + 断路器——因为 deep hash 同时服务于永久存储不可用回退队列,所以一次无声的回归会在它本应覆盖的那次中断期间,用被网络拒绝的条目填满该队列。

16.9 收录证明与一致性证明

两者均为 RFC 9162、SHA3-256,以规范 CBOR 服务。

InclusionProof——GET /api/v1/qub/:tx_id/proof:ver:u8、leaf:bstr(精确的叶 CBOR——验证者自行重新计算 leaf_hash,决不信任所提供的哈希)、index:u64、size:u64、audit:[bstr[32]]、root:bstr[32]、anchor:{ txid:bstr[32], batch:u64, sth:bstr[32], log_id:bstr[32], block_height?:u64, anchored_at?:i64 }。

ConsistencyProof——GET /api/v1/log/consistency?first=<size_a>&second=<size_b>:ver:u8、first_size:u64、second_size:u64、first_root:bstr[32]、second_root:bstr[32]、nodes:[bstr[32]]、first_anchor、second_anchor。一份单一无歧义的键列表,由测试向量固定。

独立验证(无 qub 服务器,扩展 §11):

1.  Parse .qub bundle → SealedQub; recompute qub_id (§4.1).
2.  Read leaf.kind.
3a. kind=0x01 (attested):
      assert leaf.ref == qub_id
      assert leaf.body_hash   == SHA3-256(body)
      assert leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserted):
      assert leaf.ref == SHA3-256(qub_id || blind)   // holder supplies blind
      OR treat ref as opaque and bind via leaf.chash == SHA3-256(stored_bytes)
4.  Recompute leaf_hash = SHA3-256(0x00 || leaf); fold `audit` per RFC 6962
    using index/size; require derived root == proof.root.
5.  Fetch anchor.txid from any gateway; verify the tx data → tx_id binding
    (do not trust a gateway /raw/ response); REQUIRE anchor_tx.owner ==
    LogProfile.anchor_owner.
6.  Parse AnchorBundle; require committed root == proof.root and size ==
    proof.size; read the Arweave block time T.
7.  Emit the claim scoped by leaf.kind (§16.11).

证明服务存储必须以坐标为键(已裁定——§16.15 Q7,阻塞性前置条件)。 冷叶证明生成在正确性上是中性的,仅当 R2 审计材料是一个以绝对树坐标 (level, index) 为键的持久化 Merkle 节点存储——而非逐 batch 的节点增量。在以坐标为键的存储下,任何 (leaf i, size N) 审计路径都是一组 O(log N) 次直接 R2 GET,跨批次边界无任何重新计算;在以批次为键的存储下则不然,而这正是本裁定所关闭的存储布局缺口。叶体同样可按 seq 内容寻址。一个 W5 测试向量必须证明在 LogDO 存储被擦除的情况下,仅用 R2 + 永久存储,对一个晚得多的根验证一个创世期的冷叶,使 §16.13 中的回收安全性主张有所支撑而非仅作断言。这些 O(log N) 次顺序 R2 GET 仅应位于异步证明端点上——决不位于封印热路径(§16.10)或逐 tick 的 cron 上。

16.10 R2 优先的确认排序

已实现的 POST /api/v1/upload 序列是:

  1. 前半部分网关(认证、验证、幂等性分片键)——保持不变。
  2. 创建、标记并签署确切的单个 Arweave 交易。这源自 tx_id 在本地,尽管交易创建可能会从网关获取奖励/锚点元数据。准备失败仍然会在确认之前导致请求失败。
  3. 同步地 在...写入所选的产物 qub-cache/<tx_id> 并持久化稳定的创建操作/发件箱记录。这些是持久性和重试的底线;结算前的失败将返回503。
  4. 什么时候 LOG_DO 已配置, 同步尝试 LogDO.append(leaf). 单个作者分配 seq,扩展条目链,并更新前沿。追加 RPC 仅执行此操作;批量关闭在警报上离路径运行。追加传输/应用程序故障当前 软失败:响应仍然可以在没有的情况下成功 log_seq, receipt,或者 anchor_status. 尽管有实现上的注释,但今天没有连接任何自动的后续日志对账功能。
  5. 返回确认。包括 { log_seq, anchor_status: "pending", receipt } 只有当 append 返回了完整成功的元组时。 receipt.sig_b64url 当收据签署者不可用时,该值为空;客户不得将该值称为已签署或不可否认。元组的缺失仅意味着持久发布,而不意味着透明日志已接受。
  6. 使用一个延迟任务来发布准确的已签名交易。成功会删除发件箱;失败则将其保留给有界排空的定时任务,并且不得更改已经确认的内容 tx_id临时元数据和其他尽力而为的附加组件也被推迟。

延迟边界 请求路径包括前端权限/配额工作、交易准备/签署、持久化 R2 写入,以及(在配置时) LogDO 尝试。 < 300 ms 在设计评审中表现为操作目标,而不是协议保证;当前的交易准备步骤可能执行网关元数据请求。延迟报警和启动门是操作控制,而不是验证者可用的证据。

16.11 信任模型——按叶 kind 限定范围的精确主张

对 kind=0x01(已认证): "此内容——主体与 body_hash 匹配,由 qub_id 标识——在位置 seq 被承诺到 qub 的仅追加日志,且其存在不晚于永久存储区块时间 T;在 drand 轮 R = unlock_round(unlock_at) 之前它在密码学上不可读。" 这是完整的 {tlock 轮次绑定 + Merkle 收录 + 已锚定的根} 三元组。

对 kind=0x02(已断言,默认): "一段内容地址为 chash、声称 qub_id 与 unlock_at 的不透明密文,在位置 seq 被承诺到仅追加日志,且其存在不晚于永久存储区块时间 T。" 轮次与主体两部分由现有的 §11 .qub 包验证(qub_core::unlock)提供,而非由日志提供;日志相对于一笔裸的逐 qub 交易所增添的是防篡改的排序、一个无需信任的承诺时间上界,以及抗两面性。

两类主张都按 §11 排除:无 sig_alg ≥ 0x01 的作者身份、意图,以及亚锚粒度的时机。两者都不允许任何主张依赖 received_at。

声明上限(约束性发布限制 — 已解决 §16.15 问题 1)。 对于一个断言(kind=0x02) 叶,上述范围权利要求是 天花板 关于任何产品、营销、条款或证明呈现表面可能断言的内容。任何表面都不得声明或暗示 日志 证明了一个字节盲上传的内容或解锁轮次 —— 日志证明的是 顺序 + 不透明密文的无信任上界承诺时间。内容和轮次证明完全来自现有的 §11 .qub捆绑验证,与日志无关。没有成功追加/接收的发布根本没有日志声明。

16.12 版本控制与 W3 协调

有 不 SealedQub 金线凸点 因此 未升级协议版本 (§12.2):日志是一个附加模块,它会提交到现有的字段和字节,因此它不会进入§12.3协议版本历史。W3是可选的 drand_chain_version 未被触及并仍然是唯一的可选项 SealedQub 领域。相反,日志引入了它自己的独立版本空间—— LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver —— 类似于 §12.5 的封装版本独立性(封装携带一个独立于协议版本的版本字节,日志版本也遵循相同的分离)。

证明交付默认为按需拉取,并带一个可选的搭载项。 证明在封印时无法存在(锚尚未写入),因此封印时的 .qub 包保持无证明。W7 的验证者拉取一次 GET …/proof,或在完全离线模式下经由对 Log-Id 的永久存储查询从公开的 AnchorBundle 重建证明。.qub 包(W7)预留一个可选的 inclusion_proof 成员——封印时缺失,由锚定后的重新导出填充以供冷归档——遵循与 W3 drand_chain_version 相同的"可选、默认省略、增量式"模式。

16.13 保留

LogDO 开放尾部、R2 证明服务基质、锚断路器计数器以及打包器回退队列的保留窗口,规定在 docs/DATA-RETENTION.md 中。原则:日志的热态逐条目存储(LogDO)在锚定后可回收;其审计材料——以坐标 (level, index) 为键的 Merkle 节点存储 + 以 seq 寻址的叶体(§16.9)+ 永久存储锚——是永久的。从 DO 回收一个冷叶决不会使已签发的证明失效,因为证明针对那个永久的 R2 节点存储与永久存储锚解析,而非针对 DO(且 §16.9 的擦除 DO 测试向量证明了这一点)。

16.14 测试向量

W5 交付跨语言固件 tlog_v1.json(§16.8)外加若干已演算的向量:一个 kind=0x01 与一个 kind=0x02 叶 → leaf_hash;5 叶累积根;一个收录证明;一个一致性证明;一个 AnchorBundle;以及一个 DataItem id。它们与 §14.5 的外层封装向量并列,并由 Rust(qub-core)与 TypeScript(Worker)两套实现共同运行。

16.15 审查决定(W5 — 已解决)

W5 外部审查(对抗性设计评审 + 所有者签字)已完成。下面的每个决定都已确定,并反映在上述 §16 文本中; 绑定启动约束 在结尾处重新陈述。可以根据它们进行实施。

  1. 默认路径(kind=0x02)叶节点诚实性——已解决。 按规范保留两种叶节点:kind=0x02 既不承诺 body_hash,也不承诺 drand_round。字节盲路径不得加入 *_body_hash 字段,因为它会成为集成方最容易误读的“已验证”信号,而 §11 已能从捆绑包提供这项便利。不得要求经日志认证的 qub 一律由服务器密封;那会迫使明文经过 Worker,破坏加密擦除防护。任何自描述的捷径都应放在 .qub 捆绑包/证明信封中,作为由验证器重新计算的字段,绝不能成为叶节点字段。所有者确认的声明上限见 §16.11。
  2. 模棱两可/遗漏责任——设计已解决,配置不完整。 设计要求密封收据钥匙必须固定入位 LogProfile 并由...交叉签署 anchor_owner,加上监控方法、前链遍历和双自出版头。已编译的配置文件和部署钩子仍然是占位符/可选项,如§16.6所述,因此在这些关卡关闭之前,更强的可检测 + 可回执声明尚不可用。决不能将其宣传为独立见证。真正的第三方见证将推迟到§15治理升级。
  3. 固定锚宿主信任根 + 轮换 — 已解决。 采纳 LogProfile 针(§16.6);验证者检查 anchor_tx.owner == anchor_owner 并在本地验证交易数据 → 交易ID 绑定。轮换治理是第§15条 扩展构建 (增加了§15.3触发器),不是重复使用;计划的轮换进行交叉标记,妥协驱动的轮换在分叉检查限制损害的情况下回退到§15提升。
  4. 私人 Qub 叶片失明——已解决。 继续为私有 qubs 遮眼 (ref = SHA3-256(qub_id ‖ log_blind_secret)), 生 qub_id 用于公共 qubs(已在 §16.2.1 中), chash 作为独立领带。 log_blind_secret 是一种相关性/雪崩级秘密,仅向前旋转 (§16.2.1)。
  5. received_at — 决议通过。 将其保存在叶子中,虽然承诺但明确不作为证据;从未作为任何表面上的证明或争议佐证出现。任何监控的合理性检查都与 Arweave 区块时间进行比较 T,而不是操作员控制的 anchored_at (§16.6)。
  6. 分层可证明时序——设计分辨率,而非当前布线。 审查过的设计将锚块时间分配给批处理层,将整点证明分配给付费的T3,前者没有数字服务等级协议(SLA)。当前的路由尚未实现这种商业区分:它们为每个被接受的发布安排单独事务,且日志覆盖仍按§16.1/§16.10所述为有条件的。产品文案必须描述实现情况,而不是这个未来的层级划分。
  7. 关于工人的累计树 — 已解决。 单个累积 RFC 9162 树 + 前沿缓存的单写入者 LogDO(相对于约 1k 写入/秒的 DO 上限有充足余量;在接近该上限时再推迟进行分片根的 Merkle 分片)。基于坐标键的 (level, index) R2 节点存储 + 擦除的 DO 冷叶测试向量已实现 (§16.9)。 < 300 ms 仍然是设计/操作目标,而不是协议承诺(§16.10)。
  8. ANS-104 签名方案 + 深度哈希 — 已解决。 RSA-PSS(签名类型1,重用专用锚点钱包JWK);Ed25519延迟到§15 PQ路径。手工制作的SHA-384深度哈希受制于双向交叉实现固定装置,仅静态参考打包器互操作性检查,共享的-crypto.subtle 往返,以及捆绑后 Arweave 接受监控 (§16.8)。

绑定启动约束 (执行落实 + 产品/法律审查):


17. 便携式验证包(.qub)

状态。 本节是 已实现 (W7 / UP-C2): qub_core::export 生成并解析捆绑包,并 tools/qub-verify 是一个公共的、自包含的命令行工具,用于离线验证。§11 和 §16.9 已经提到“the” .qub “bundle” 作为一个独立验证器使用的单位;本节指定了它的字节数和验证流程。它是严格附加的——bundle 打包现有的 §11 输入,并且不更改链上传输格式。

17.1 目的

§11规定,任何第三方都可以在不需要qub合作的情况下验证qub的加密产物。 .qub 捆绑包进行该验证 便携和离线它将封装的 CBOR 和解锁它的 drand 轮签名打包成一个独立的整体产物,这样接收者就可以验证内容完整性、轮绑定以及任何作者签名 完全没有网络请求 (无存储获取,无实时 drand 请求,无 qub API)。单独的包并不能证明其密文的创建时间;独立验证的存储交易或锚定的日志证明可以提供该独立存在时间的声明(§11, §17.5)。

17.2 捆绑格式

A QubBundle 是基于§3.1配置文件的手写规范CBOR(定长,无标签,无浮点数,最短形式整数,NFC文本,缺省时可选字段省略,键按编码字节长度升序排列,然后按字节顺序排序)。三个15字符的键顺序 d < i < s. 一个生的 .qub 文件就是这些字节;对于 URL 或复制粘贴传输,相同的字节使用 base64url(无填充)。

钥匙 封装长度 类型 存在 意义
version 八 u8 必需的 捆绑包格式版本(0x01).
sealed_at 十 i64 可选 创作者声明的密封时间(Unix 秒);自我描述,非证据性。
drand_round 十二 u64 必需 qub 所绑定的轮次。嵌入式封存 qub 的投影。
arweave_tx_id 十四 tstr 必需的 密封字节存储的交易ID(来源指针)。
drand_chain_id 十五 tstr 必需的 drand 链(十六进制)。嵌入封装量子比特的投影。
drand_signature 十六 bstr 必需的 用于 drand 信标的签名 drand_round —— 解锁密文的值。
inclusion_proof 十六 bstr 可选 在可获得锚定证明后(§17.5),§16 透明度日志的默克尔包含证明。
sealed_qub_cbor 十六 bstr 必需的 内部 SealedQubCbor 字节(在第13条之后解包),即第11条验证输入。

drand_round 和 drand_chain_id 是方便的投射 sealed_qub_cbor,以便工具可以在不解析内部 CBOR 的情况下读取它们。它们在构造时派生并 在解码上重新检查 针对解析后的密封 qub;其顶层字段与其有效载荷不一致的捆绑将被拒绝。编码器规则与其余的线格式相对应:拒绝空的 drand_signature 或 arweave_tx_id,并绑定每个可变长度字段。

17.3 内嵌 drand 签名所证明的内容

该捆绑包携带了 drand 签名,而无需验证者获取它。锁时解密(基于 drand 链的 tlock,第 8 节)只能在 真正的 绑定轮次的信标签名——这是链在该轮次结束时仅发布一次的一个值,并且是链公钥下有效的BLS签名。伪造或错误的签名会导致BLS验证失败或IBE/AEAD解密失败。因此,能够解密的捆绑包证明了: 密文与第R轮绑定,并且第R轮已经结束. 验证者固定链条(DrandTimelockProvider::quicknet())并应用第§11条回合绑定检查,因此捆绑包无法声称其密文未绑定到某个回合。

这是一个释放条件证明,而不是创建时间戳。在轮次 R 之后 经过,任何人都可以为 R 创建新的密文并打包其已公开的内容 签名。因此,仅凭该捆绑包不得被描述为证明该 在 R 之前存在的密文或内容,之前 unlock_at,或者在任何事件之前。

17.4 离线验证演练

qub-verify <file.qub> 完全从捆绑包运行标准§11程序,驱动 qub_core::unlock::unlock 固定的 DrandTimelockProvider:

1. Parse the .qub bytes → QubBundle (canonical-CBOR guard; bound every field;
   re-check drand_round / drand_chain_id against the embedded sealed qub).
2. BLS-verify bundle.drand_signature for the pinned chain and round, then
   tlock_decrypt(sealed.tlock_ciphertext, bundle.drand_signature) → QubEnvelope.
3. Verify SHA3-256(body) == body_hash               (§11 step 8).
4. Verify QubEnvelope.qub_id   == SealedQub.qub_id   (§11 step 9).
5. Verify QubEnvelope.unlock_at == SealedQub.unlock_at (§11 step 10).
6. Verify ciphertext round == unlock_round(unlock_at) and the chain binding.
7. If sig_alg != 0x00: verify author_signature (and any cosigner; §9.4).
8. Report integrity, round-elapsed/round-binding, authorship, and cosigner
   verdicts separately, plus the recovered body. Do not report a commitment
   timestamp unless step 9 succeeds.
9. Optional existence-time leg: verify an included §16 proof through its pinned
   anchor, or independently verify the referenced storage transaction. Report
   its block time as an upper bound on ciphertext existence.

CLI 退出 0 (已验证), 1 (验证失败——仍然被锁定,主体哈希不匹配,回合/链绑定损坏,或签名验证失败),或者 2 (使用 / 错误打包)。A --json 报告对自动化持有相同的结论。由于这个捆绑包是自包含的,验证器包(qub-core) 和 CLI (qub-verify) 是第三方唯一需要的软件;两者都是公开的,并且重用协议现有的验证路径——无需定制加密。

17.5 与透明度日志的关系

inclusion_proof 是 §16 Merkle 包含证明的可选槽位。仅验证捆绑包(§17.4)即可完整判断完整性、轮次绑定/轮次已过,以及可选的作者身份,但有意不提供独立带时间戳的存在性声明。填充且通过完整锚点验证的 inclusion_proof 会加入 §16.11 中针对具体叶节点类型的承诺与时间上限,而无需更改捆绑包格式版本。缺少证明只表示“未附包含证明”,不表示“无效”,也不一定表示“未锚定”。

在参考实现中,该槽位现在是强类型的:qub_core::export::QubBundle::inclusion_proof_typed() 通过同一个不透明 CBOR 字段返回携带完整 §16.9 结构(叶节点、审计路径、锚定根和 AnchorRef)的 Option<InclusionProof>,无需提升捆绑包格式版本。独立的 qub-verify CLI 在 --anchor 验证分支中使用它;在锚点钱包完成配置前(§16“状态”),对已填充但所有者仍为占位值的证明只报告已包含,不报告为完整的锚定验证。