qub 协议规范

qub 是一个用于加密时间承诺的协议:一个将文字封印到未来某个日期、并在该日期到来时精确证明所说内容与时间的系统。

它由三种原语共同支撑。drand 是一个去中心化随机性信标——公开日期由物理规律强制执行,而非任何一方的善意。永久公开存储是一个防篡改的公共存储——qub 一旦被封印,任何一方都无法编辑或删除。ML-DSA-65 是一种后量子数字签名——每个 qub 都绑定到一组密钥对,其私钥永远不会离开作者的设备。

这些原语共同构成了一种时间锁定、防篡改且可归属的声明——一份其价值随着世界伪造过去的能力提升而增长的凭证。

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


qub 协议规范

字段
版本 1.0(协议版本 0x01,外层封装版本 0x01
日期 2026-05-01
状态 草案
审阅至 2026-05-01

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

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


1. 记法与约定

记法 含义
u8u64i64 指定位宽的无符号 / 有符号整数
[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,              // 0x01 = public (only value in MVP)
    content_type:   u8,              // 0x01 = text (only value in MVP)
    plaintext:      Vec<u8>,         // UTF-8 qub body
    sender_label:   Option<String>,  // Decorative display name; not authenticated
    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>,     // V1.1 — when reality renders judgment (verdict-uplift-plan §3.1)
    sender_label:        Option<String>,  // Decorative; not authenticated in MVP
    reply_to:            Option<[u8; 32]>,// Parent qub_id for reply chains; not in qub_id preimage; not signed (see §9.3)
    body:                Vec<u8>,         // Content payload (UTF-8 for text, CBOR for pact)
    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 = 0x01content_type = 0x01,sig_alg = 0x00,所有 Option 字段均不存在。

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

2.3 SealedQub(规范线缆格式)

使用规范 CBOR 序列化(§3)。写入永久存储。这是链上工件。

SealedQub {
    version:           u8,              // Protocol major version (0x01 for v1)
    qub_id:            [u8; 32],        // Same as QubEnvelope.qub_id
    visibility:        u8,              // 0x01 = public; v1 viewers reject other values
    unlock_at:         i64,             // Unix seconds UTC
    outcome_at:        Option<i64>,     // V1.1 — 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
    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 control characters.
}

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>,       // V1.1 — 由 QubEnvelope.outcome_at / SealedQub.outcome_at 转携而来;驱动公开页的判定等待区块(verdict-uplift-plan §5.1)
    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 配置

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

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 编码。
重复键 报错拒绝。 解析器禁止静默接受重复的映射键。
简单值 仅允许 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 (V1.1 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 (V1.1 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)

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 编码: V1.1 将预映像从 92 字节扩展至 100 字节,以将可选的 outcome_at 字段折入绑定。不存在的 outcome_at 编码为 8 个零字节;协议验证器在所有位置都拒绝 outcome_at <= 0,因此该哨兵值不会与合法值冲突。详见 §3.2(线缆格式)以及代码库内的 tasks/verdict-uplift-plan.md,其中阐述了引入该字段的判决机制。

drand_round 编码: V1.2 将预映像从 100 字节扩展至 108 字节,以将 drand_round(目标 drand 轮,§4.3)折入绑定,并将域分隔符提升为 QUB_ID_V2。这会将时间锁轮次绑定进 qub 身份:网关无法将密文重新绑定到与所显示 unlock_at 所暗示不同的(例如已过去的)轮次。公开流程(§8)还会进一步验证 tlock 密文 stanza 中所记录的轮次与 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 = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
参数 来源 示例
unlock_at 用户选择的 UTC Unix 秒 1735689600(2025-01-01 00:00:00 UTC)
chain_genesis_time drand 链信息(genesis_time 1595431050
chain_period_seconds drand 链信息(period 30

ceil() 操作选择公开时间 ≥ unlock_at 的第一个 drand 轮。这确保 qub 不会在所选公开时间之前变得可解密。

边界情形:(unlock_at - chain_genesis_time) 可被 chain_period_seconds 整除,则结果即为该精确轮次——qub 恰好在该轮的公开时间解锁。

校验: 封印时 unlock_at 必须位于未来。unlock_atcreated_at 不得超过 10 年(以限制长期 drand 依赖风险;UI 应当对超过 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), value: String (≤ 2,000) }   // NFC on both sides
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }

三个映射的规范 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:// 开头。任何其他协议——httpftpjavascriptdatafile 等——一律拒绝。
  2. 长度上限。 ≤ 2,048 字节(浏览器 URL 实用上限)。
  3. NFC + 敌意码点检查。titlereflection 的规则——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.
 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 = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
    (Computed here, before qub_id, because drand_round is bound into the qub_id
    preimage — §4.1, V1.2.)
 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,
    unlock_at, drand_chain_id, drand_round.
12. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12a. Generate K = 32 random bytes (CSPRNG) and N = 12 random bytes (CSPRNG).
     Compute W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
     per §13. The bytes uploaded to permanent storage are the OuterWrapper CBOR W,
     never the bare SealedQubCbor. K leaves the device only as the URL
     fragment in step 16.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit W (the OuterWrapper bytes) to the qub upload service; the service
    signs and uploads to permanent storage. The service is byte-blind to the inner
    SealedQubCbor and never receives K.
16. Receive arweave_tx_id from the service. Construct delivery URL as
    `<origin>/c/<arweave_tx_id>#<base64url(K)>` (or `<origin>/s/<short_code>#<base64url(K)>`
    when a short code is allocated). Browsers do not transmit URL fragments
    to servers, so K is never observed by qub.social or any storage gateway.

存储标签层(带外)。 qub 上传服务在已封装负载之外附加一组刻意精简的存储交易标签。Content-Type=application/octet-stream 是规范性必需的。参考服务在创作者选择展现时额外附加三个可选标签:Intent(经过白名单校验的撰写意图——例如 quotereplycommitment)、Author(创作者的 §9.3 公钥指纹,64 字符小写十六进制),以及 Parent-Tx-Id(回复链中父 qub 的存储交易 ID,43 字符 base64url)。

Author 标签是按 qub 选择加入的:参考创作者应用仅在用户在封印时显式启用公开归属时附加它。当此开关关闭——这是默认值——不写入 Author 标签,且 qub 在链上是非归属的:永久存储中没有任何东西将该次上传与创作者的句柄、邮箱或其他 qub 关联起来。当开关开启时,Author 指纹经由 §9.5 的认证链解析为创作者所选的 @handle。回复链关系与 Intent 是非身份性的。外层封装(§13)保护内部主体不被密文相关性识别——防止采集者识别并批量解密在 drand 轮发布后呈 qub 形态的上传。

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

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


8. 公开协议

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

 1. Viewer opens delivery URL. Extract arweave_tx_id from path AND
    K = base64url_decode(fragment) from the URL fragment. If the fragment
    is absent or malformed → display "this URL is missing its decryption
    key" and stop; the viewer MUST NOT contact the storage gateway
    without K, since fetching wrapped bytes the viewer cannot decrypt
    serves no purpose and only leaks the access attempt.
 2. Check denylist. If tx_id is denylisted → display block message. Stop.
 3. Fetch OuterWrapper bytes from permanent storage (with multi-gateway fallback).
 3a. Unwrap: parse the bytes as OuterWrapper (§13), verify the wrapper
    `version` byte is `0x01`, and compute SealedQubCbor =
    unwrap_sealed_qub(OuterWrapper, key=K). Any AEAD authentication
    failure (wrong K, tampered ciphertext, swapped qub_id-as-AAD,
    swapped nonce) → display "this URL's decryption key does not match
    the stored qub" and stop. Authentication failures are
    indistinguishable to the viewer per §13.5.
 4. Parse SealedQubCbor → SealedQub.
 5. Validate: SealedQub.version is known (0x01). Reject unknown versions.
 6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
 6a. Round-binding check (V1.2). Recompute expected_round =
    ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
    Reject unless SealedQub.drand_round == expected_round AND the round baked
    into the tlock ciphertext stanza (read via the age/tlock header, no signature
    required) == expected_round. 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.
13. Verify: QubEnvelope.content_type is known and renderable.
    Known values: 0x01 (text), 0x03 (pact). 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 appropriate renderer (see §10 for text, §6 for pact).
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 字节

查看者必须拒绝未知的 sig_alg 值。

9.3 签名预映像构造

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

signature = Sign(author_secret_key, sig_input)

域分隔符: "QUB_AUTHOR_SIG_V1" 为 17 个 ASCII 字节:[0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]。不进行填充。

尾字节: 预映像的第 91 字节必须为 0x00。参考实现将其作为常量 ORG_ID_PRESENT_INDIVIDUAL = 0x00 暴露于 crates/qub-core/src/signing.rs;为验证而重建 sig_input 的查看者必须输出相同的字节。

签名作用域——覆盖与未覆盖的内容。 sig_input 承诺到信封的四个字段:versionqub_idbody_hashunlock_at(外加固定的域分隔符与 org_id_present 字节)。这四个中有三个是结构性不变量:qub_id 本身经由 §4.1 的预映像从 versioncontent_typecreated_atunlock_atoutcome_atdrand_roundbody_hash 推导而来,因此对这些字段的任何改动都会产生不同的 qub_id,并以传递方式使签名失效。因此被直接认证的字段为:

字段 被签名认证 方式
version sig_input 的直接输入
qub_id 直接输入
body_hash 直接输入
unlock_at 直接输入
content_type qub_id 预映像传递
created_at qub_id 预映像传递
outcome_at qub_id 预映像传递
drand_round qub_id 预映像传递(V1.2)
body body_hash = SHA3-256(body) 传递
author_pubkey —(隐式) 按定义,验证签名通过的密钥即为作者
sender_label 仅显示文本;可在不破坏签名的情况下修改
reply_to 线程指针;可在不破坏签名的情况下修改
cosigner_pubkey / cosigner_signature 在相同 sig_input 上独立签名(见 §9.7)
drand_chain_idtlock_ciphertextvisibility 外层 SealedQub 字段,不在信封内部——由它们自身的结构性不变量(轮次 / 链一致性)覆盖,但不受作者签名覆盖。(drand_round 现已经由 qub_id 预映像以传递方式绑定——见上文。)

未被认证字段的安全影响。

向终端用户展示 sender_labelreply_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 (same formula as §9.3).
6. Verify(author_pubkey, sig_input, author_signature).
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.
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. 第三方验证

任何第三方都可以在无需 qub 配合的情况下验证一个公开的 qub。验证流程为:

1. Obtain arweave_tx_id (from delivery URL or direct knowledge).
2. Fetch SealedQubCbor from any storage gateway.
3. Confirm storage block inclusion (block height, block timestamp).
4. Parse SealedQubCbor → SealedQub.
5. Fetch drand round signature for SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify QubEnvelope.qub_id == SealedQub.qub_id.
10. Verify QubEnvelope.unlock_at == SealedQub.unlock_at.
11. If sig_alg != 0x00: verify author_signature (see §9.4).
12. All checks pass → qub is verified.

验证能证明什么:

证明 所确立的事实
承诺 该密文在存储区块时间戳之前已存在。
完整性 明文主体与已承诺哈希一致,未被更改。
时间 在 drand 轮(对应所选公开时间)之前内容不可读(取决于 tlock 与 drand 的安全假设)。

验证不能证明什么:

非证明 原因
作者 sender_label 仅为装饰性。若 sig_alg < 0x01,任何人都可能封印此内容。
意图 qub 证明的是内容与时间,而非创作者主观上想表达什么。
事件前时机 存储区块包含可能比实际上传滞后若干分钟。承诺时间戳是区块时间,并非用户按下"封印"那一刻。

12. 版本控制

12.1 协议版本

SealedQubQubEnvelope 中的 version 字段(u8)标识协议主版本。

12.2 版本历史

版本 描述
v1 0x01 公开文本 qub(content_type 0x01)、约定双边协议(0x03structured/v1 schema,ML-DSA-65 作者 + 共同签名者)、tlock、SHA3-256

12.3 向前兼容

v1 查看者遇到带有未知可选 CBOR 映射键(不属于 §3.2 规范顺序的键)的 QubEnvelope 时,应当忽略这些键并使用已知字段继续验证。这允许未来的小幅补充(例如新元数据)在不需要主版本升级的情况下加入。

v1 查看者遇到 sig_alg = 0x01(ML-DSA-65)但缺少 ML-DSA-65 验证支持时,应当以"存在但不可验证的签名"提示展示 qub 内容,而不是完全拒绝该 qub。参考实现目前会拒绝除 0x000x01 之外的任何 sig_alg 值,因为 v1 注册表中没有其他有效算法——在第三个算法被注册之前,严格拒绝与软失败在外观上是相同的。一旦 §9.2 接纳了新条目,上述软失败行为就具有实质意义,参考查看者也会在那时更新为软失败。

12.4 外层封装版本

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

OUTER_WRAPPER_VERSION_* 算法 状态
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM,12 字节随机数,16 字节认证标签,AAD 绑定到 qub_id v1 默认
0x020xFF 保留 未来

查看者必须以清晰错误拒绝未知封装版本。在出现具体迁移驱动因素(例如 NIST 指南倾向不同 AEAD)之前,协议有意保持封装版本空间狭窄;在引入算法的同一修订中将分配 0x02 槽位。


13. 外层加密封装

13.1 理由

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

外层加密封装通过在规范 SealedQubCbor 与写入永久存储的字节之间插入一层附加的对称 AEAD 来关闭该通道。256 位密钥 K 存在于传递链接的 URL 片段以及用户设备上;浏览器不会将 URL 片段发送至服务器,因此 qub.social、每个存储网关以及它们前面的每个 CDN 在观测意义上对 K 都是盲的。永久存储中的每个 qub 因此都是一段不透明密文,没有创作者选择分享的 URL 就无法恢复其明文。

净效应:

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
  ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12a, this section)
OuterWrapper CBOR bytes              ← uploaded to permanent storage (§7 step 15)

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

13.3 OuterWrapper 数据结构

struct OuterWrapper {
    version:    u8,           // 0x01, see §12.4
    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
    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.4
    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
    return P                            // P is the inner 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)──▶  uploaded to permanent storage as-is
SealedQubCbor bytes  ──(private)─▶  AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded

公开 qub 受时间锁定但不受链接控制:在其 drand 轮签名发布之前始终不可读(tlock 层不变),但解锁后,任何持有 arweave_tx_id 的人都可将其解密——无需 URL 片段,因为不存在 K。这是为服务器必须驱动的那些场景所作的有意权衡:公开通知邮件、第三方嵌入,以及更丰富的公开后 SEO,都需要一个无需服务器从不持有的密钥即可使用的链接(§13.6)。

生产者必须考虑的后果:

私有(已封装)仍为默认;公开是创作者按每个 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  = 4695445  (= (1736294400 - 1595431050) / 30, 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 — V1.2):
  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)
  0x000000000047A595  ||  // drand_round as u64 big-endian (4695445)
  body_hash           ||  // 32 bytes
  title_hash              // 32 bytes (all-zeros sentinel; title absent)

Expected output:
  qub_id = SHA3-256(preimage)
         = 3a9fcb31b750d985c262fada6d4f777f
           d6a28be831d941d85c131f5a4bbaf8a4

对此输入,实现必须产出相同的 body_hashqub_id 值。该测试向量应当作为编写的第一个单元测试。上述规范值由参考实现计算,必须逐位匹配。历史预映像布局(发布前——没有任何线上 qub 依赖这些值):92 字节的 V1.0 qub_id 为 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0;100 字节的 V1.1 qub_id(折入 outcome_at_or_zero 之后)为 b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed。V1.2 折入 drand_round 并将域分隔符提升为 QUB_ID_V2

14.2 公开轮次映射

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

Calculation:
  (1735689600 - 1595431050) / 30 = 4675285.0
  ceil(4675285.0) = 4675285

drand_round = 4675285

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 文件:

该固件当前固定三个用例:

用例 覆盖范围
basic-text-public 最小的真实 SealedQub 形态;无任何可选字段。确立 v1.0 典型 qub 的规范封装形态。
with-recipient-pubkey 设置了 recipient_pubkeySealedQub(第 2 阶段路径)。内部 CBOR 键集不同、qub_id 不同。
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 对每种原语恰好绑定一个算法:

验证者目前对每种原语硬编码公钥与签名长度。线缆格式不暴露任何敏捷性接口。

15.2 目标形态

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

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

15.3 触发条件

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

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