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 编码。
重复键 报错拒绝。 解析器禁止静默接受重复的映射键。
未知键 报错拒绝。 解析器禁止容忍该类型规范键集之外的映射键——两个不同的规范字节串决不能解码为同一值(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 (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 签名预映像构造

曾存在两个预映像版本。所有签名都必须使用 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_labelreply_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 直接承诺到 versionqub_idbody_hashunlock_atsender_labelreply_to(外加固定的域分隔符与 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 直接输入
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 预映像传递(V1.2)
body body_hash = SHA3-256(body) 传递
author_pubkey —(隐式) 按定义,验证签名通过的密钥即为作者
cosigner_pubkey / cosigner_signature 在相同 sig_input 上独立签名(见 §9.7)
drand_chain_idtlock_ciphertextvisibility 外层 SealedQub 字段,不在信封内部——由它们自身的结构性不变量(轮次 / 链一致性)覆盖,但不受作者签名覆盖。(drand_round 现已经由 qub_id 预映像以传递方式绑定——见上文。)

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

向终端用户展示 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 (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. 第三方验证

任何第三方都可以在无需 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 时,必须以解码错误拒绝(§3.1)。向前兼容依托 version 字段,而非键容忍:未来的新增——即便是小幅元数据——都在新的 version 值下发布;v1 查看者会以清晰的"较新协议"错误拒绝它,而不是静默丢弃签名所承诺到的内容。

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 朝既定目标落地,而非从零开始重新争论协商接口。


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

状态。 本节是一份设计规范。下文的传输格式、哈希与信任模型对实现而言是规范性的,但尚无透明日志代码发布。W5 外部评审已完成:§16.15 记录了已裁定的决策以及由此产生的约束性发布约束。实现可以在这些约束下进行。§16 与 §15 在同样意义上保持前瞻性——它固定目标,使实现落地于一个既定的设计,而非在代码评审中从零重新推导信任模型。它严格为增量式——每个现有 qub 都保留其各自的永久存储交易,且 SealedQub / QubEnvelope 传输格式不发生任何变化

16.1 理由与持久性层级

如今,一个 qub 的持久性及其时间承诺都依托于单笔逐 qub 的永久存储交易(§11)。这将封印延迟与永久存储的最终确认耦合在一起,使逐 qub 上传成为一项产品成本上限(ARWEAVE_DAILY_CEILING),并且无法在多个 qub 之间提供防篡改的排序。透明日志在该单一层级之下与之外增加了两层:

层级 名称 保证 时机
T1 R2 优先的同步确认 持久性底线——封印字节在封印返回之前已写入持久化存储(< 300 ms p95)。 每个 qub,同步进行(§16.10)。
T2 批量化透明日志收录 通用、仅追加、防篡改的承诺 + 全序,锚定至永久存储。 每个 qub,延迟 + 批量进行(§16.5–16.7)。
T3 逐 qub 永久存储永久性 为该 qub 单独提交一笔永久存储交易。 付费增值,以及永久存储不可用时的回退(§16.8)。

T2 使逐 qub 永久存储成为一种选择(T3),而非唯一的持久性路径。ARWEAVE_DAILY_CEILING 不再作为产品上限,降格为仅作用于专用锚定钱包的断路器(§16.7);用户封印决不会因超出它而被拒绝。

持久性诚信(已裁定——§16.15 Q6)。 持久性不会退化:T1 的 R2 写入是同步且一次写入的,因此一个未购买 T3 的免费层 qub,在封印返回的那一刻便已完全持久。变粗的是可证明的承诺时间上界:对一个免费 qub,它变为锚定区块时间,而非逐 qub 交易的区块时间。在低流量下——这是符合现实的早期发布与非高峰状态——完整的每日节奏是典型底线,而非罕见的边缘情形。因此产品的框定是一个上界,且不承诺任何数值化延迟——"现已封印且持久;将在下一次日志锚定时(通常为每日)添加一个独立的公开时间戳"——而精确到小时的承诺证明是付费 T3 属性,在层级对比界面及条款中披露(§16.11、§16.15 Q6)。任何时间界限都仅为内部 SLO,决不作为对外营销的 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 字节选择的形态,因为在默认上传路径上 Worker 对字节是盲的:POST /api/v1/upload 仅接收 qub_idunlock_at 作为不受信任的客户端断言——body_hashdrand_roundcreated_atdrand_chain_version 全部封印在 §13 外层封装之内,而 Worker 从不持有其密钥。只有服务器封印路径(POST /api/v1/seal)才从明文推导 body_hash / drand_round。因此,单一携带 body_hash + drand_round 的叶形态,对绝大多数真实 qub 而言会承诺运营方从未验证过的值。这一拆分使每个被承诺的值都保持诚实:

编码长度 类型 出现 含义
seq 4 u64 必需 全局从 0 起的叶索引;收录证明所承诺到的位置。
kind 5 u8 必需 0x01 已认证(服务器封印)或 0x02 已断言(客户端封印 / 字节盲上传)。
ref 4 bstr[32] 必需 叶引用 id。已认证 → 原始 qub_id。已断言 → 盲化后的 id SHA3-256(qub_id ‖ log_blind_secret)(§16.2.1)。
chash 6 bstr[32] 必需 内容地址 SHA3-256(stored_bytes)——Worker 在两条路径上都始终能诚实计算的唯一内容关联。
unlock_at 10 i64 必需 复制(已认证)或断言(已断言);进入叶之前校验 > 0
received_at 12 i64 必需 R2 确认时的 Worker 挂钟时间。非证据性(运营方断言;§16.6)。用于自描述,决不作为证明。校验 > 0
body_hash 10 bstr[32] kind=0x01 0x02 上省略——Worker 在 §13 之下无法获得它。
drand_round 12 u64 kind=0x01 0x02 上省略。

一个 kind=0x02 叶有意既不承诺 body_hash 也不承诺 drand_round:它认证的是一段位于内容地址 chash 的不透明密文的承诺与排序,并声称 qub_idunlock_at——而非其明文或轮次。已断言 qub 的明文/轮次部分来自现有的 §11 .qub 包验证,而非来自日志(§16.11)。drand_chain_version 不在叶中(在默认路径上它位于封装之内);链粒度归属于锚(§16.7)。编码方纪律:拒绝全零的 refchash,并拒绝非正值的 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)。一个批次是一同被锚定的叶集合;其触发条件可配置,而非协议冻结的:tree_size 前进至少达到 LOG_BATCH_MAX_LEAVES(默认 4096),或时长达到锚定节奏,或在一笔付费 T3 封印落地时强制刷新。root_i 是覆盖叶 0 .. tree_size_i 的累积 Merkle 树哈希。

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

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

设计中恰好存在一个温态的 qub 签名密钥,且它是固定的:逐封印的回执密钥(§16.10)。其公钥在 LogProfile(随验证者分发)中被承诺,anchor_owner 交叉签名,因此验证者针对与锚相同的固定根来校验回执。这是 §16.15 Q2 的裁定结果——一个未固定的、运营方可轮换的回执密钥将是可抵赖的(运营方可否认该密钥属于自己),这会使回执针对其本应威慑的运营方级对手所具有的问责价值归零。因此:qub 持有无任何_未固定的_日志签名密钥;回执密钥是固定的且由 anchor_owner 交叉签名的。

SignedTreeHead 是规范 CBOR(键按编码长度排序):size:u64root:bstr[32]batch:u64prev:bstr[32](前一个 sth_hash;创世 = 32 个零字节)、log_id:bstr[32]first_seq:u64anchored_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 检查(见下文)在过渡期内限定损失。

两面性窗口(一等信任参数)。 一个叶只有在其覆盖锚被永久存储确认之后才具有抗两面性。该窗口为 received_at → 锚确认(≤ 节奏 + 永久存储最终确认)。在此窗口内,唯一的保证是固定的封印回执(§16.10)与 qub 的运营完整性。三件问责物使这一点诚实而非含糊(见证模型是 §16.15 Q2 的裁定):

  1. 固定的已签名封印回执——在上传响应中返回的 SCT 类比物(§16.10),由固定的、经 anchor_owner 交叉签名的回执密钥签名。一个在其锚定之前被丢弃的叶,会给受害者留下一份不可抵赖的回执以供公布,从而关闭无声遗漏漏洞。
  2. 已发布的监控方法学 + prev 链遍历——锚的 prev 链按 头→创世 遍历;一个 fork(同一 size 上有两个 root 不同的锚,或一条断裂的 prev)即是可公布的不当行为证据。两面性检测是一项已声明的运营承诺,而非无声的假设。
  3. 双重自行发布的树头——每个新树头 {sth_hash, tree_size} 都张贴到一个专用的、qub 所有的公开、仅追加的 GitHub 仓库(承重的、防篡改的自行发布支柱),并以一篇社交帖子仅作尽力而为的佐证。一次失败的张贴必须触发呼叫(不得无声失败)。

诚信界限(约束性约束)。 由于 qub 控制着两个张贴界面,这是自行发布,而非独立见证。任何产品、营销或法律界面都不得声称该日志被"独立见证";被允许的主张是两面性可被检测并留下一份不可抵赖的回执。一个真正独立的第三方见证者被推迟至未来的某次 §15 治理升级。

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

16.7 锚交易格式与节奏

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

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

节奏: 默认每日,随流量重新审视(大小触发条件会在负载下自动缩短实际节奏);一笔付费 T3 封印会强制一次锚定,使付费客户决不等待一整天。锚定钱包是专用且低频的,与上传钱包分离——它必须是其自己的 JWK(一个独立密钥,而非上传钱包上的一个逻辑角色),使上传钱包的泄露无法伪造锚——并带有每日锚交易的硬性预算(被降格的 ARWEAVE_DAILY_CEILING)。保管姿态被直白陈述:一个作用域狭窄、带紧致断路器且余额低的热密钥,而非"冷密钥"——一个每日自动签名的钱包不可能是冷的,规范也不假装它是。

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)。

一条代码路径服务于三个消费者:付费 T3 逐 qub 永久性、永久存储不可用时的回退(将 DataItem 入队,无论如何返回 R2 优先的确认——这关闭了当前 ARWEAVE_UNAVAILABLE 503 死胡同),以及写入 AnchorBundle签名方案(已裁定——§16.15 Q8):v1 以 RSA-PSS(签名类型 1)签名,复用现有的永久存储钱包 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/proofver:u8leaf:bstr(精确的叶 CBOR——验证者自行重新计算 leaf_hash,决不信任所提供的哈希)、index:u64size:u64audit:[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:u8first_size:u64second_size:u64first_root:bstr[32]second_root:bstr[32]nodes:[bstr[32]]first_anchorsecond_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. 同步执行 await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)——持久性底线;同时关闭 W1 的预缓存竞态(此前是在永久存储提交之后的一个 ctx.waitUntil)。
  3. 同步执行 await LogDO.append(leaf)——一次同 colo 的 DO RPC;唯一写入者分配 seq、扩展条目链并更新前沿。(对共享状态的读-改-写 → DO,决不 KV。)该 append RPC做这件事——O(batch) 的 Merkle 批次关闭工作在 LogDO alarm 上脱离此 RPC 运行,否则 append 的 p95 会在每第 LOG_BATCH_MAX_LEAVES 笔封印时尖峰。
  4. 立即返回确认——附带封印回执(由固定的回执密钥签名,§16.6)以及 { tx_id, log_seq, anchor_status: "pending" }。多秒级的永久存储扇出被移出关键路径。
  5. 一个 ctx.waitUntil 将延迟工作入队:逐 qub 永久存储提交(现为尽力而为 / 付费;失败时它路由至打包器回退队列,而非向用户返回 503)外加现有的临时元数据写入。批次关闭与锚定在 LogDO alarm 与每日锚定 cron 上独立运行。循环内无 ctx.waitUntil;现有的幂等性分片键得以保留。

延迟预算(已裁定——§16.15 Q7)。 < 300 ms p95 目标是一道被测量的发布门,而非假设。诚实的关键路径是 前半段 KV 读取 + 一次 R2 PUT + 两个串行化的 Durable Object——现有的 QuotaDO 封印配额扣减以及新的 LogDO append——因此预算必须计入两次同 colo 的 DO 往返,而非一次。发布一个镜像 QuotaDOLogDO 延迟告警,并将 p95 回归视为发布阻塞项。

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

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

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

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

主张上限(约束性发布约束——已裁定 §16.15 Q1)。 对免费 / 默认(kind=0x02)qub,上述限定范围的 kind=0x02 主张是任何产品、营销、条款或证明渲染界面可以宣称的上限。任何界面都不得陈述或暗示日志证明了一个默认 qub 的内容或解锁轮次——日志证明的是一段不透明密文的 排序 + 无需信任的承诺时间上界。内容与轮次证明专门来自现有的 §11 .qub 包验证,而它与日志无关。这是对文案的一道硬性发布阻塞项,而非一种文体偏好;它正是使字节盲默认路径保持诚实的裁定。

16.12 版本控制与 W3 协调

不存在 SealedQub 传输升级,因此不存在协议版本升级(§12.1):日志是一个承诺到既有字段与字节的旁路件,因此它不进入 §12.2 的协议版本历史。W3 的可选 drand_chain_version 不受触动,仍是唯一的可选 SealedQub 字段。日志转而引入它自己的独立版本空间——LOG_VERSION_1ANCHOR_FORMAT_1InclusionProof.ver——镜像 §12.4 的封装版本独立性(封装携带一个独立于协议版本的版本字节,而日志版本遵循相同的分离)。

证明交付默认为按需拉取,并带一个可选的搭载项。 证明在封印时无法存在(锚尚未写入),因此封印时的 .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 的拆分:kind=0x02 既不承诺 body_hash 也不承诺 drand_round。字节盲路径上无 *_body_hash 字段(它会是对集成方而言最像样的虚假"已验证"信号,且 §11 已能从包中提供这一便利)。不要要求日志认证 qub 必须服务器封印(那会迫使明文经过 Worker,并摧毁加密粉碎护城河)。任何自描述短路都应归属于 .qub 包 / 证明信封中、作为验证者重新计算的字段,决不作为叶字段。所有者确认的主张上限:§16.11。
  2. 两面性 / 遗漏问责——已裁定。 封印回执密钥固定于 LogProfile + 由 anchor_owner 交叉签名(关闭此前"无签名密钥"的矛盾;§16.6)。发布见证模型:固定回执 + 监控方法学 + prev 链遍历 + 双重自行发布的树头(qub 所有的公开 GitHub 仓库,社交尽力而为),营销为可检测 + 带回执决不独立见证。一个真正的第三方见证者被推迟至某次 §15 治理升级。
  3. 固定的锚所有者信任根 + 轮换——已裁定。 采纳 LogProfile 固定(§16.6);验证者检查 anchor_tx.owner == anchor_owner 并在本地验证 交易 数据 → tx_id 绑定。轮换治理是一项待构建的 §15 扩展(新增 §15.3 触发条件),而非复用;有计划的轮换交叉签名,由泄露驱动的轮换回退至 §15 升级,并以 fork 检查限定损失。
  4. 私有 qub 叶盲化——已裁定。 对私有 qub 保留盲化(ref = SHA3-256(qub_id ‖ log_blind_secret)),对公开 qub 用原始 qub_id(已在 §16.2.1),chash 作为独立关联。log_blind_secret 是一个关联/Sybil 级密钥,仅向前轮换(§16.2.1)。
  5. received_at——已裁定。 将它保留在叶中,已承诺但显式非证据性;决不在任何界面上作为证明或争议佐证而呈现。任何监控合理性检查都与永久存储区块时间 T 比对,而非与运营方控制的 anchored_at 比对(§16.6)。
  6. 免费层可证明时机——已裁定(所有者签署确认)。 持久性不会退化;只有可证明的承诺时间上界变粗为锚区块时间。免费层文案不用任何数值化 SLA("……在下一次日志锚定时添加,通常为每日");精确到小时的证明是付费 T3 属性,在层级对比界面 + 条款中披露(§16.1)。
  7. Workers 上的累积树——已裁定。 单棵累积 RFC 9162 树 + 前沿缓存的唯一写入者 LogDO(相对于约 1k 次写入/秒的 DO 上限有充裕余量;将 Merkle-of-shard-roots 分片推迟至接近它时)。阻塞性前置条件: 以坐标 (level, index) 为键的 R2 节点存储 + 擦除 DO 的冷叶测试向量(§16.9);< 300 ms 是跨两个串行化 DO 的一道被测量的发布门(§16.10)。
  8. ANS-104 签名方案 + deep-hash——已裁定。 RSA-PSS(签名类型 1,复用专用锚定钱包 JWK);Ed25519 推迟至 §15 的 PQ 路径。手写的 SHA-384 deep hash 被门控于:两个方向的跨实现固件、一次仅供静态使用的参考打包器互操作检查、共享 crypto.subtle 往返,以及打包后永久存储接受度监控(§16.8)。

约束性发布约束(带入实现 + 产品/法律评审):