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 相同的規則——拒絕雙向覆寫 / 零寬 / 標籤區塊 / BOM / C0 / C1 碼點。定義與 Rust crate::handle::contains_hostile_text_codepoint 及 TS workers/api/src/utils/unicode.ts::isHostileCodepoint 一致(保持同步)。
  4. 無空白、無 ASCII 控制字元。 URL 任何位置出現空白 / DEL / 低於 0x20 的位元組皆拒絕——封堵雙向規則未涵蓋的 \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 在鏈上是非歸屬的:永久儲存中沒有任何東西將該次上傳與創作者的 handle、信箱或其他 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 handle、信箱地址、社交帳號或 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 朝既定目標落地,而非從零開始重新爭論協商介面。