qub 協議規範
qub 是一種用於密碼學時間承諾的協定:它能將文字封存至未來日期,日後精確驗證封存內容及控制揭露的 drand 輪次;若有儲存交易或透明度日誌證明,還能獨立驗證密文最晚於何時完成承諾。
這套機制由三項基礎技術構成。drand 是去中心化的隨機信標——揭露日期由密碼學強制執行,不必仰賴 qub 的善意。耐久儲存 會保存已確認的封存位元組;目前的發布路徑同時會排程個別永久儲存交易,而一般上傳若成功附加至日誌,還能加入批次錨定的承諾。ML-DSA-65 是後量子數位簽章——啟用作者身分時,qub 會綁定至一組金鑰對,私密金鑰永不離開作者的裝置。
這些原語共同構成了一個時間鎖定且防篡改的聲明,可選地可歸屬,並能獨立加上時間戳記——一個其價值隨著世界製造過去能力的提升而增長的收據。
本文件的其餘部分為實現互操作所需的規範性規格。
qub 協定規範
| 領域 | 價值 |
|---|---|
| 文件版本 | 1.0.0 (protocol-v1.0.0) |
| 線路協議 | 0x01 |
| 外包裝 | 0x01 |
| 生效日期 | 2026-09-23 |
| 狀態 | 目前 |
| 已審閱 | 2026-09-23 |
本文件是 qub 定時承諾系統的規範性協定規範。它定義了可互通實作所需的資料結構、序列化規則、推導公式與驗證流程。
範圍:協定層有意保持語言中立——qub 主體是不透明的明文 / markdown / 約定位元組,區域感知的渲染由查看者負責(qub.social 網頁應用、<qub-embed> iframe、MCP 客戶端等)。
1. 記法與約定
| 記法 | 含義 |
|---|---|
u8、u64、i64 |
指定位寬的無號 / 有號整數 |
[u8; N] |
長度為 N 位元組的定長位元組陣列 |
Vec<u8> |
變長位元組陣列 |
Option<T> |
T 型別的值,或不存在 |
String |
UTF-8 文字字串,已 NFC 正規化 |
| ` | |
SHA3-256(x) |
位元組串 x 的 NIST SHA3-256 雜湊(FIPS 202) |
ceil(x) |
向上取整函式:不小於 x 的最小整數 |
| CBOR | 簡明二進位物件表示(RFC 8949) |
| 大端 | 最高有效位元組在前 |
前映像構造中的所有整數均編碼為大端定寬位元組陣列(i64 → 8 位元組,u8 → 1 位元組),除非另有說明。
所有時間戳均為 UTC Unix 秒。
2. 資料結構
2.1 ComposeQub(創作者記憶體狀態)
不序列化為 CBOR。不寫入永久儲存。僅在創作者應用本地存在。
ComposeQub {
draft_id: [u8; 16], // Random, generated locally
created_at: i64, // Unix seconds UTC
unlock_at: Option<i64>, // Unix seconds UTC; None while composing
visibility: u8, // 0x00 = private; 0x01 = public
content_type: u8, // 0x01 text; 0x03 pact; 0x04 verdict
plaintext: Vec<u8>, // Raw body bytes (UTF-8 for text)
sender_label: Option<String>, // Display name; V2-signed when authorship is enabled
title: Option<String>, // Plaintext countdown title; bound via title_hash
reply_to: Option<[u8; 32]>,// Parent qub_id; V2-signed when authorship is enabled
outcome_at: Option<i64>, // Optional future judgment time; bound to qub_id
status: DraftStatus, // Composing | Sealed | Uploaded | Failed
}
2.2 QubEnvelope(解密後的負載)
使用規範 CBOR 序列化(§3)。被加密後置於 SealedQub 內部。這是在解密後用於證明內容完整性的結構。
QubEnvelope {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Derived (see §4.1)
content_type: u8, // Content type registry (see §6)
created_at: i64, // Unix seconds UTC
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // When reality renders judgment; bound to qub_id
sender_label: Option<String>, // Not in qub_id; V2-signed when authorship is enabled
reply_to: Option<[u8; 32]>,// Parent qub_id; not in qub_id; V2-signed when present
body: Vec<u8>, // UTF-8 text or canonical CBOR pact/verdict body
body_hash: [u8; 32], // SHA3-256(body) (see §4.2)
sig_alg: u8, // Signature algorithm (see §9.2)
author_signature: Option<Vec<u8>>, // Set when sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Set when sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
cosigner_signature: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}
基線(未簽名的文字 qub): version = 0x01, content_type = 0x01, sig_alg = 0x00;簽名和共同簽署欄位缺失。其他可選的元資料欄位可能存在。
其他 v1 配置: content_type = 0x03(約定主體,見 §6.1);sig_alg = 0x01(ML-DSA-65),同時存在 author_signature 與 author_pubkey(見 §9.3);針對共同簽名的約定,cosigner_pubkey 與 cosigner_signature 同時出現(見 §9.7);針對回覆鏈 qub,將 reply_to 設為父 qub 的 qub_id(關於簽章作用域的影響見 §9.3)。
2.3 SealedQub(規範線路格式)
使用標準 CBOR (§3) 進行序列化。這是內部線路產物:公開傳送以裸位元組形式儲存這些位元組,而私人傳送則將它們包裹起來 OuterWrapper 儲存前(§13)。
SealedQub {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Same as QubEnvelope.qub_id
visibility: u8, // 0x00 = private/wrapped; 0x01 = public/bare
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // Surfaced on the verdict-watch CTA
// before reveal; mirrors QubEnvelope.outcome_at;
// bound to qub_id via the §4.1 preimage.
drand_chain_id: String, // drand chain hash (hex string)
drand_round: u64, // Target drand round number
drand_chain_version: Option<u8>, // W3 — chain-migration version. Absent / 0 = quicknet
// (the only chain today). Lets a future chain swap
// be expressed on the wire without a breaking format
// change. NOT part of the §4.1 qub_id preimage, so its
// addition never alters an existing qub's identity.
tlock_ciphertext: Vec<u8>, // tlock-encrypted QubEnvelope CBOR bytes
recipient_pubkey: Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
// but not interpreted by the v1 reference viewer
title: Option<String>, // Plaintext title surfaced on the viewer
// countdown before reveal. Bound to qub_id
// via title_hash (§4.1). 1..=100 NFC code
// points, no hostile/control code points.
}
2.4 RevealedQub(查看者應用狀態)
不序列化為 CBOR。僅在查看者應用本地存在。在成功解密並驗證後構造。
RevealedQub {
qub_id: [u8; 32],
arweave_tx_id: String,
visibility: u8,
content_type: u8,
created_at: i64,
unlock_at: i64,
outcome_at: Option<i64>, // Carried from both wire layers; drives the verdict-watch block
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Carried forward from SealedQub.title
reply_to: Option<[u8; 32]>,
body: Vec<u8>,
body_hash: [u8; 32],
body_hash_verified: bool,
author_signature: Option<Vec<u8>>,
author_pubkey: Option<Vec<u8>>,
signature_verified: Option<bool>,
cosigner_pubkey: Option<Vec<u8>>,
cosigner_signature: Option<Vec<u8>>,
cosigner_verified: Option<bool>,
}
3. 規範 CBOR 配置
所有 SealedQub 與 QubEnvelope 序列化必須符合本配置。對於相同的邏輯結構,兩個實作必須產出相同的位元組。
3.1 編碼規則
| 規則 | 規範 |
|---|---|
| 標準 | RFC 8949 §4.2.1(核心確定性編碼要求) |
| 對映鍵排序 | 先按編碼後的位元組長度排序(短在前長在後),再按字典序排序(長度相同時按位元組逐位元組比較) |
| 整數編碼 | 最短形式:0–23 嵌入初始位元組;24–255 用 2 位元組;256–65535 用 3 位元組;以此類推。 |
| 長度編碼 | 僅使用定長。 不允許不定長陣列、對映、位元組串或文字串(禁止附加資訊 = 31)。 |
| 標籤 | 不允許 CBOR 標籤(禁止主類型 6)。 |
| 浮點數 | 不允許浮點(禁止主類型 7 中值為 0xF9–0xFB 的項)。 |
| 文字串 | UTF-8 編碼,已 NFC 正規化(Unicode Normalization Form C)。 |
| 位元組串 | 原始位元組。CBOR 層不進行 base64 編碼。 |
| 重複鍵 | 報錯拒絕。 解析器禁止靜默接受重複的對映鍵。 |
| 未知鍵 | 報錯拒絕。 解析器禁止容忍該類型規範鍵集之外的對映鍵——兩個相異的規範位元組串決不可解碼為同一個值(encode(decode(x)) == x),且對已簽署的負載而言,多出的鍵會成為兩份簽章共同承諾到的隱藏內容。Schema 演進一律透過 version 進行,決不透過額外的鍵。 |
| 簡單值 | 僅允許 true(0xF5)、false(0xF4)與 null(0xF6)。 |
| 可選欄位 | 不存在的可選欄位從 CBOR 對映中完全省略(而非編碼為 null)。存在的可選欄位按排序後的鍵序加入。 |
3.2 已驗證的規範鍵序
以下鍵序是規範性的。實作必須嚴格按照此順序輸出鍵。非發布建置中除錯斷言應驗證該順序。
QubEnvelope(版本 0x01,未簽署,所有可選欄位不存在):
"body" (5 encoded bytes)
"qub_id" (7 encoded bytes)
"sig_alg" (8 encoded bytes)
"version" (8 encoded bytes)
"reply_to" (9 encoded bytes) ← only if present (reply chains)
"body_hash" (10 encoded bytes)
"unlock_at" (10 encoded bytes)
"created_at" (11 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (verdict mechanic)
"content_type" (13 encoded bytes)
"sender_label" (13 encoded bytes) ← only if present
"author_pubkey" (14 encoded bytes) ← only if present
"cosigner_pubkey" (16 encoded bytes) ← only if present (pact cosign)
"author_signature" (17 encoded bytes) ← only if present
"cosigner_signature" (19 encoded bytes) ← only if present (pact cosign)
QubEnvelope 鍵序推導: 每個鍵都是 CBOR 文字串。編碼長度 = 1 位元組頭 + 字串長度(對於長度小於 24 位元組的字串)。先按總編碼長度排序,長度相同時按字典序排序。
SealedQub(版本 0x01,公開,無接收者):
"title" (6 encoded bytes) ← only if present
"qub_id" (7 encoded bytes)
"version" (8 encoded bytes)
"unlock_at" (10 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (verdict mechanic)
"visibility" (11 encoded bytes)
"drand_round" (12 encoded bytes)
"drand_chain_id" (15 encoded bytes)
"recipient_pubkey" (17 encoded bytes) ← only if present
"tlock_ciphertext" (17 encoded bytes)
"drand_chain_version" (20 encoded bytes) ← only if present (W3; absent = quicknet)
PactTerms(約定主體,content_type 0x03):
"notes" (6 encoded bytes) ← only if present
"terms" (6 encoded bytes)
"title" (6 encoded bytes)
"party_a" (8 encoded bytes)
"party_b" (8 encoded bytes)
"pact_version" (13 encoded bytes)
PactTerm(terms 陣列的一列):
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier(party_a / party_b 對映):
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 位元組編碼參考
| 類型 | CBOR 編碼 | 範例 |
|---|---|---|
| SHA3-256 雜湊(32 位元組) | 0x58 0x20 + 32 位元組 |
body_hash、qub_id |
| 時間戳(i64) | 主類型 0(正)或 1(負),最短編碼 | Unix 秒 |
| 版本(u8,值 1) | 0x01(單位元組) |
|
| 內容類型(u8,值 1) | 0x01(單位元組) |
|
| sig_alg(u8,值 0) | 0x00(單位元組) |
|
| ML-DSA-65 簽章(3,309 位元組) | 0x59 0x0C 0xED + 3,309 位元組 |
author_signature、cosigner_signature |
| ML-DSA-65 公鑰(1,952 位元組) | 0x59 0x07 0xA0 + 1,952 位元組 |
author_pubkey、cosigner_pubkey |
4. 規範性推導
4.1 qub_id
qub_id 唯一標識一個 qub,並將 QubEnvelope 與 SealedQub 繫結。它由信封內容確定性地推導而來。
qub_id = SHA3-256(
"QUB_ID_V2" || // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
version || // u8 (1 byte)
content_type || // u8 (1 byte)
created_at || // i64 big-endian (8 bytes)
unlock_at || // i64 big-endian (8 bytes)
outcome_at_or_zero || // i64 big-endian (8 bytes; 0 when outcome_at is absent)
drand_round || // u64 big-endian (8 bytes)
body_hash || // [u8; 32] (32 bytes)
title_hash // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output
域分隔符編碼: 字串 "QUB_ID_V2" 為 9 個 ASCII 位元組。附加單個 0x00 填充位元組以達到 10 位元組對齊。實作必須嚴格使用以下 10 位元組:[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]。
outcome_at 編碼: 一個預發行實作修訂將前映像從 92 位元組擴展到 100 位元組以折疊可選項 outcome_at 將欄位放入綁定中。缺失 outcome_at 被編碼為 8 個零位元組;協議驗證器拒絕 outcome_at <= 0 到處都是,所以這個哨兵不會與合法值碰撞。參見§3.2(線路格式)以及樹內 tasks/verdict-uplift-plan.md 對推動這個領域的裁決機制而言。
drand_round 編碼: 後來的一個預發布實現修訂將前映像從 100 位元組擴展到 108 位元組以進行折疊 drand_round (目標 drand round,§4.3)納入綁定,並將域分隔符提高到 QUB_ID_V2這將時間鎖回合綁定到 qub 身分:閘道無法將密文重新綁定到不同的(例如,已經過去的)回合,而不是所顯示的回合 unlock_at 暗示。解鎖程序 (§8) 另外會驗證鎖定在 tlock 密文段中的輪次是否匹配 unlock_round(unlock_at)因此,顯示的解鎖時間可以被證明是限制解密的回合。
特性:
- 更改任何與前像綁定的欄位—
version,content_type,created_at,unlock_at,outcome_at,drand_round,生的body位元組 (透過body_hash),或title(通過title_hash)—產生不同的qub_id。 - qub_id 是在加密前計算的。QubEnvelope 和 SealedQub 都包含相同的 qub_id。查看者在解密後會驗證它們是否匹配。
qub_id不依賴於sender_label,reply_to、簽名位元組或簽名公鑰。然而,在目前的 V2 簽名構造下,sender_label和reply_to由……直接驗證sender_label_hash和reply_to_or_zero(§9.3)每當有簽名時。- 在其他欄位固定時變更 SealedQub 的
title,會透過title_hash改變qub_id。因此,閘道無法替換倒數頁面顯示的純文字標題而不使 qub 身分失效。 - 在其他欄位固定時變更 SealedQub 的
outcome_at,會透過前像改變qub_id。閘道無法更改倒數頁面顯示的揭露前裁決日期而不使 qub 身分失效。 - 在其他欄位固定時變更
drand_round,會透過前像改變qub_id。閘道無法將時鎖密文重新綁定至其他輪次而不使 qub 身分失效;搭配 §8 對解鎖時間與密文章節輪次的檢查,顯示的unlock_at可證實對應至實際控制解密的輪次。
4.2 body_hash
body_hash = SHA3-256(body)
其中 body 是原始 Vec<u8> 內容負載。對於文字 qub,這是 UTF-8 編碼的 qub 主體。
4.2.1 title_hash
title_hash = SHA3-256(NFC(title).utf8_bytes) if title is present
title_hash = [0u8; 32] if title is absent
其中 title 是在公開前查看者倒數計時上展示的可選明文標題(見 §3.2)。NFC 正規化在雜湊時執行,使摘要在視覺上等價的碼點序列之間保持穩定。全零標記保留給不存在的情形;空字串在規範 CBOR 邊界上作為「不存在」的非規範編碼而被拒絕(規範編碼會完全省略該欄位)。
4.3 解鎖回合地圖
drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
| 參數 | 來源 | 例子 |
|---|---|---|
unlock_at |
使用者選擇的 Unix 秒數 UTC | 1735689600 (2025-01-01 00:00:00 協調世界時) |
chain_genesis_time |
drand 鏈資訊 (genesis_time) |
1595431050 |
chain_period_seconds |
drand 鏈資訊 (period) |
30 |
這是參考 tlock 映射(drand 的 CurrentRound). drand 發佈回合 N 在 chain_genesis_time + (N - 1) * chain_period_seconds,所以公式選擇了 環流在 unlock_at — 那一輪,其簽名是到達的觀眾見到的第一個 unlock_at 可以使用。
對齊屬性(實際上重要的情況): 何時 (unlock_at - chain_genesis_time) 可以整除 chain_period_seconds,已公布所選回合的簽名 正好在 unlock_at從未有過. 這對參考部署總是成立:quicknet 的創世時間(1692803367)能被其 3 秒週期整除,而且參考應用將 PIN 解鎖時間鎖定到整分鐘。對於未對齊的 unlock_at,所選輪次的簽名嚴格地在不到一個周期前發布 unlock_at — 承諾的時間精確度為一個信標周期。
傳統預發布映射和解鎖端容差: 原始映射是 ceil((unlock_at - chain_genesis_time) / chain_period_seconds),對於上述的週期對齊情況,選擇了整個週期已發布的輪次版本 之前 unlock_at,使密文能提前一個周期被解密。這兩個映射之間的差異正好是 +1 當增量除以週期,並且另行同意。因為 drand_round 被折疊進不可變的之中 qub_id 前像 (§4.1),在舊映射下封存的產物無法重新推導;因此,執行 §8 步驟 6a 回合交叉檢查的驗證者必須接受已儲存的 drand_round 等於 任何一個 衍生回合 或 派生的回合減一(並且必須要求 tlock 節的回合與儲存的回合完全相等)。容差最多將最早的閘控簽名擴展一個周期。在重新派生已排程的 pact 時,pact 階段服務應用相同的容差 qub_id (在階段和在共同簽名時):如果當前映射的回合未能重現已提交的 qub_id 而三角洲分隔了週期,它會以前一輪重新嘗試,並將最終的協定封存到任一輪 qub_id 實際上會綁定——絕不盲目地綁定到重新計算的輪次,否則會使該產物永遠無法導出。
驗證: unlock_at 必須在封印時間的未來。 unlock_at 不得超過 10 年 created_at (為了限制長期隨機數依賴風險;使用者介面應該對超過 2 年的解鎖日期發出警告)。
5. 線路格式 Newtype
線路格式 newtype 在編譯期防止將 CBOR 位元組與 JSON、原始明文或其他位元組編碼混淆。
| 類型 | 包含 | 製作 | 被消耗 |
|---|---|---|---|
SealedQubCbor |
SealedQub 的規範 CBOR | serialize_sealed_qub() |
內部線路產物;公開傳送時直接儲存,私人傳送時包裝後儲存,再由查看者還原 |
QubEnvelopeCbor |
QubEnvelope 的標準 CBOR | serialize_qub_envelope() |
tlock 加密輸入,tlock 解密輸出 |
5.1 構造規則
// Production code — only through CBOR serialisers:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);
// There is deliberately NO From<Vec<u8>> implementation.
// You cannot accidentally wrap arbitrary bytes in a wire format type.
// Accessing raw bytes:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();
5.2 構造時的校驗
from_encoded() 應校驗輸入以有效的 CBOR 對映頭開始。完整的結構校驗在解析時進行,而非構造時,以避免重複解析。
6. 內容類型註冊表
| 值 | 類型 | 最大主體大小 | 備註 |
|---|---|---|---|
0x00 |
保留(無效) | — | 禁止使用 |
0x01 |
純文字(UTF-8,受限 Markdown) | 50 KB 付費 / 10 KB 免費 | 渲染規則見 §10。免費 / 付費切分由上傳服務強制執行;協定層硬性上限為 50 KB。 |
0x02 |
保留(未來) | — | 為未來的內容類型保留;在 v1 中無效。查看者必須按下文規則拒絕。 |
0x03 |
約定(雙邊協議,CBOR 主體) | 100 KB | 主體為規範 CBOR 的 PactTerms(§6.1)。共同簽名見 §9.7。 |
0x04 |
判定(創作者自我評分,CBOR 主體) | 8 KB | 主體為規範 CBOR 的 VerdictBody(§6.2)。僅由系統端的 verdict 意圖發行。親子關係位於 Parent-Tx-Id Arweave 標籤上,而非主體中。見 verdict-uplift-plan §3.4。 |
查看者必須以清晰的使用者可見錯誤拒絕未知內容類型。查看者禁止嘗試將未知類型作為文字渲染。
6.1 約定主體(content_type = 0x03)
約定主體是 PactTerms 值的規範 CBOR 編碼:
PactTerms {
pact_version: u8, // 0x01 for structured/v1
title: String, // ≤ 200 bytes, NFC
terms: Vec<PactTerm>, // ≤ 20 rows
party_a: PartyIdentifier, // initiator
party_b: PartyIdentifier, // counter-signer
notes: Option<String>, // ≤ 5,000 bytes, NFC; absent key if none
}
PactTerm { key: String (≤ 100 bytes), value: String (≤ 2,000 bytes) } // NFC
PartyIdentifier{ label: String (≤ 100 bytes), contact: Option<String (≤ 320 bytes)> }
三個對映的規範 CBOR 鍵序見 §3.2。序列化後的約定 CBOR 總大小不得超過 100 KB(與 §6 一致)。
Schema 鑑別欄位。 對 structured/v1 約定,terms 中的第一列必須為 { key: "pact_schema", value: "structured/v1" }。沒有此標記的列屬於「自訂」約定,不接受結構化校驗或 schema 感知的渲染。
凍結的確認槽位。 structured/v1 約定恰好在以下鍵下攜帶四列確認列:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
每列的 value 是按 (role, kind) 對所選定的八個凍結英文字串之一,其中 role ∈ { seller, buyer, provider, client },kind ∈ { standard, capacity }。這些字串本身是規範性協定資料——雙方的 ML-DSA-65 簽章透過 body_hash 承諾到這些精確位元組。它們不會被在地化;簽章所涵蓋的主體是語言中立的。任何措辭變更都需要新的 schema 版本(structured/v2)。
這八個字串、它們的查找方式(acknowledgement_for(role, kind))以及每條的理由由參考實作固定。符合規範的實作必須輸出位元組相同的確認值;涵蓋全部四種角色組合的黃金固件 SHA3-256 body-hash 測試可捕獲任何漂移。
查看者展示順序。 這些確認字串包含諸如 "described above" 等措辭,前提是描述 / 範圍列先於確認列渲染。查看者必須按 CBOR 順序渲染 terms 陣列;重新排序會破壞文字語義。
對方聯絡方式。 當 Party B 的 contact 為有效的電子郵件地址時,qub 上傳服務在暫存階段會自動發出審閱 / 共同簽名邀請信件,並將最終的共同簽名繫結到對同一地址的驗證(§9.7)。Party B 聯絡方式缺失的約定仍可被共同簽名,但只能經由帶外管道完成——服務會拒絕無法產出匹配的 15 分鐘信箱驗證標記的共同簽名請求。
6.2 判定主體(content_type = 0x04)
判定主體是 VerdictBody 值的規範 CBOR 編碼:
VerdictBody {
verdict_version: u8, // 0x01 for structured/v1
outcome: u8, // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
reflection: Option<String>, // ≤ 2,000 bytes NFC; "what changed, what did you learn"
evidence_url: Option<String>, // ≤ 2,048 bytes; HTTPS only; absent key when omitted
}
規範 CBOR 鍵序:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
序列化後的判定 CBOR 總大小不得超過 8 KB(與上方註冊表列一致)。
結果列舉。 線路位元組是意圖中立的;四個分類 Right / Partial / Wrong / Unfalsifiable 涵蓋每一種帶判定意圖的結果空間。各意圖專屬的標籤(如 Right 對應「說中了」/「做到了」/「如期推出」/「經事實證實」等)是查看者端的渲染議題,會對照父 qub 的意圖解析——線路本身保持語言與意圖中立。1..=4 範圍外的值必須在解碼時拒絕。
親連結。 判定 qub 不會在其主體中攜帶對親的參照。親 qub 的 Arweave 交易 ID 在上傳時作為 Parent-Tx-Id 儲存標籤發出(§7 儲存標籤層)。如此一來,主體保持為自我評估的自包含已簽名陳述;稽核鏈(「對什麼為對?」)透過 Arweave 標籤查詢建立。
證據 URL 安全性(規範性)。 當 evidence_url 存在時,驗證者(撰寫端、線路端、Worker 邊緣)必須執行:
- 僅限 HTTPS。 字串必須以位元組序列
https://開頭。任何其他協定——http、ftp、javascript、data、file等——皆拒絕。 - 長度上限。 ≤ 2,048 位元組(瀏覽器 URL 實務上限)。
- NFC + 敵意碼點檢查。 與
title和reflection相同的規則——拒絕雙向覆寫 / 零寬 / 標籤區塊 / BOM / C0 / C1 碼點。定義與 Rustcrate::handle::contains_hostile_text_codepoint及 TSworkers/api/src/utils/unicode.ts::isHostileCodepoint一致(保持同步)。 - 無空白、無 ASCII 控制字元。 URL 任何位置出現空白 / DEL / 低於
0x20的位元組皆拒絕——封堵雙向規則未涵蓋的\n/\t注入向量。 - 非空的主機段。
https://與第一個/、?或#之間的所有內容必須非空。
伺服器端不抓取。 Worker 禁止代理、抓取或預覽該 URL。協定僅儲存字串;渲染在查看者端進行,使用 rel="nofollow noopener noreferrer" target="_blank" 並在連結文字旁可見顯示主機名稱。
反思。 創作者自行撰寫的反思文字(「有什麼改變、學到了什麼」),為選填。與 title 採用相同的 NFC + 敵意碼點驗證。空字串 / 僅含空白的輸入會在建構時收合為不存在。
Schema 版本。 v1 僅支援 verdict_version = 0x01。未來的 schema 修訂將遞增此位元組,並依 §12 隨新協定版本一同上線。
7. 封印協定
完整的封印流程。每一步都是規範性的。
1. User composes plaintext and metadata in ComposeQub.
2. Validate:
a. body is non-empty.
b. body size ≤ max for content_type and user tier (see §6).
c. unlock_at is in the future.
d. unlock_at ≤ created_at + 10 years.
e. content_type is a known, supported value.
f. visibility is 0x00 (private) or 0x01 (public).
3. Compute body_hash = SHA3-256(body).
4. Set created_at = current Unix seconds UTC.
5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
compute drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
(§4.3). (Computed here, before qub_id, because drand_round is bound into the
qub_id preimage—§4.1.)
6. Compute qub_id (see §4.1), folding in drand_round from step 5.
7. Construct QubEnvelope with all fields.
8. Serialise QubEnvelope using canonical CBOR → bytes B.
Assert: serialised output matches canonical profile (§3).
9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C and matching qub_id, version,
visibility, unlock_at, drand_chain_id, and drand_round.
11. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12. Select the delivery shape from visibility:
a. Private (0x00): generate K = 32 random bytes and N = 12 random bytes
using a CSPRNG. Compute W = wrap_sealed_qub(SealedQubCbor,
qub_id=qub_id, key=K, nonce=N) per §13. Upload payload = W.
b. Public (0x01): upload payload = bare SealedQubCbor; do not generate K.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit the selected upload payload to the qub upload service. For a private
browser seal, the service is byte-blind to the inner SealedQubCbor and never
receives K. The Builder `/api/v1/seal` route is an explicit exception: it
receives plaintext and caller-supplied K in memory, then persists neither.
16. Receive arweave_tx_id from the service. For private delivery, construct
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (or the equivalent short-code
path). For public delivery, omit the fragment. Browsers do not transmit URL
fragments to servers, so K from the browser-seal path is not observed by
qub.social or any storage gateway.
儲存標籤層(帶外)。 qub 上傳服務會在所選的上傳內容旁附加故意設計成很小的一組儲存交易標籤。 Content-Type=application/octet-stream 在規範上是必需的。當建立者選擇顯示它們時,參考服務還會附加三個可選標籤: Intent (允許清單驗證的組合意圖—announcement, thesis, prediction, letter, secret, commitment, proof,或系統發出的 verdict), Author (建立者的 §9.3 公鑰指紋,為 64 個字元的小寫十六進位),以及 Parent-Tx-Id (用於回覆鏈的父 qub 的儲存交易 ID,43 個字元的 base64url)。
這 Author 標籤是 每個 qub 選擇加入:參考建立者應用程式僅在使用者在封印時明確啟用公開歸屬時才附加它。當切換關閉時——預設值——則不會附加 Author 標籤已寫入,而區塊在區塊鏈上未被歸屬:永久儲存中沒有任何東西將上傳內容與創作者的帳號、電子郵件或其他區塊鏈關聯起來。當切換開啟時, Author 指紋解析為創造者所選 @handle 透過§9.5的證明鏈。回覆鏈關係和 Intent 是非識別性的。對於私人傳送,外層包裝 (§13) 會加密可識別的內部 SealedQub 因此,收集已儲存的封裝器和取得公眾 drand 簽名仍然不足以在沒有 K 的情況下恢復本體;儲存標籤依然故意是公開的元資料。
參考服務有意不附加 App-Name、App-Version 或 Type 標籤:任何此類單值篩選都會讓 GraphQL 查詢回傳整個 qub 語料庫,這與封裝的「僅主體保密」作用域不一致。
符合規範的驗證者在執行 §11 的第三方驗證時禁止依賴任何儲存標籤;主體雜湊 / qub_id / 簽章只承諾到內部 CBOR,決不承諾到標籤集。
8. 公開協定
完整的公開流程。每一步都是規範性的。
1. Viewer opens delivery URL. Extract arweave_tx_id from the path and retain
the optional URL fragment. Do not assume a missing fragment is an error:
public/bare delivery intentionally has no K.
2. Check denylist. If tx_id is denylisted → display block message. Stop.
3. Fetch the stored bytes (with multi-gateway fallback).
3a. Resolve the delivery shape structurally:
a. If the bytes parse as OuterWrapper, require a well-formed 32-byte K in
the URL fragment, require wrapper version 0x01, and unwrap per §13.
Any missing/malformed K or AEAD failure is a terminal error.
b. Otherwise require the bytes to parse as bare SealedQubCbor; no K is
required. If neither shape parses, report an integrity error.
4. Parse SealedQubCbor → SealedQub.
5. Validate: SealedQub.version is known (0x01), visibility is known, and the
delivery shape matches it (wrapped = 0x00 private; bare = 0x01 public).
Reject any mismatch or unknown value.
6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
6a. Round-binding check. Recompute expected_round from
SealedQub.unlock_at per §4.3. Reject unless SealedQub.drand_round ==
expected_round OR SealedQub.drand_round == expected_round - 1 (the
legacy pre-release mapping—see §4.3), AND the round baked into the tlock
ciphertext stanza (read via the age/tlock header, no signature required)
== SealedQub.drand_round exactly. The stanza round is the one that
actually gates decryption; without this check a malicious creator could
bind the ciphertext to an already-past round while displaying a future
countdown, so anyone reading the stored bytes could decrypt before
unlock_at. Implementations with no chain identity (test mocks) skip this
check.
7. Once current time ≥ SealedQub.unlock_at:
a. Fetch drand round signature for SealedQub.drand_round from drand network.
b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
8. Parse B → QubEnvelope.
9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
Fail → integrity error.
12a. Verify: QubEnvelope.outcome_at == SealedQub.outcome_at (both absent, or
both present and equal). Fail → integrity error.
12b. Content re-derivation. Recompute qub_id per §4.1 from the decrypted
fields — (QubEnvelope.version, content_type, created_at, unlock_at,
outcome_at, SealedQub.drand_round, QubEnvelope.body_hash,
title_hash(SealedQub.title)) — and verify it equals SealedQub.qub_id.
Fail → integrity error. The pairwise checks in steps 10-12a only prove
the two layers agree with EACH OTHER; a forger who rewrites a bound
field consistently on both surfaces (a pre-reveal title swap, or a
post-round body swap with a recomputed body_hash re-encrypted to the
same round under the same qub_id) passes them all. Only re-deriving
the identity from content closes this.
13. Verify: QubEnvelope.content_type is known and renderable.
Known values: 0x01 (text), 0x03 (pact), 0x04 (verdict).
Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using the appropriate renderer (see §10 for text and §6 for pact/verdict).
17. Construct RevealedQub for display.
9. 作者簽章
9.1 理由
qub 永久儲存於永久儲存。作者簽章必須長期保持不可偽造,這就是 v1.0 採用後量子的 ML-DSA-65 方案(FIPS 204)而非某個其安全性可能在 qub 的永久生命週期內退化的傳統方案的原因。
9.2 演算法註冊表
sig_alg |
計畫 | 金鑰大小 | 簽名尺寸 | 狀態 |
|---|---|---|---|---|
0x00 |
無簽名(未簽署) | — | — | 活躍 |
0x01 |
ML-DSA-65(FIPS 204) | 1,952 位元組 | 3,309 位元組 | 活躍 |
0x02 |
Ed25519 | 32 位元組 | 64 位元組 | 保留常數;在協議 v1 中不支援 |
Protocol-v1 觀眾必須拒絕所有超出範圍的值 {0x00, 0x01},包括
矜持的 0x02 價值。預留防止意外重用;它不是
啟動。啟動它需要遵循第15條所述的受控變更。
9.3 簽章前映像構造
前映像曾存在兩個版本。所有簽章都必須使用 V2,而驗證者也必須僅接受 V2。 舊版 V1 前映像(下方保留供歷史參考)在遷移至 V2 期間曾被接受為僅供驗證的回退;該回退現已淘汰,僅能對 V1 驗證通過的簽章如今會被拒絕。
V2(目前版本——由所有新的作者簽署,以及約定暫存 / 共同簽名流程的兩個簽章共同產生):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V2" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 || // u8 (1 byte): MUST be 0x00 in v1.x
sender_label_hash || // [u8; 32]: SHA3-256(NFC(sender_label)),
// or 32 zero bytes when absent
reply_to_or_zero // [u8; 32]: parent qub_id, or 32 zero
// bytes when absent
)
// Total preimage: 155 bytes → 32-byte hash
signature = Sign(author_secret_key, sig_input)
sender_label_hash 遵循與 title_hash(§4.2.1)相同的缺失標記慣例:32 個零位元組並非有效的 SHA3-256 輸出,因此「不存在」永遠不會與實際存在的標籤相碰撞。所有欄位皆為定寬,因此前映像無需長度前綴即無歧義。
V1(舊版——已淘汰;不再產生,驗證時亦不再接受):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V1" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 // u8 (1 byte): MUST be 0x00 in v1.0
)
// Total preimage: 91 bytes → 32-byte hash
V1 前映像省略了 sender_label 與 reply_to。它在遷移至 V2 期間曾被接受為僅供驗證的回退;該回退此後已淘汰——驗證者必須僅接受 V2 前映像。此處保留其定義供歷史參考,並用以說明下方的域分隔符。僅能對 V1 驗證通過的簽章必須視為驗證失敗。
域分隔符: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" 各為 17 個 ASCII 位元組([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32])。不進行填充。兩者相異的分隔符對這兩種構造進行域分隔,因此在其中一個前映像上產生的簽章絕不會對另一個驗證通過。
org_id_present 位元組: 緊接在 unlock_at 之後的位元組必須為 0x00。參考實作將其作為常數 ORG_ID_PRESENT_INDIVIDUAL = 0x00 暴露於 crates/qub-core/src/signing.rs;為驗證而重建 sig_input 的查看者必須輸出相同的位元組。
簽章作用域——涵蓋與未涵蓋的內容。 V2 的 sig_input 直接承諾到 version、qub_id、body_hash、unlock_at、sender_label 與 reply_to(外加固定的域分隔符與 org_id_present 位元組)。qub_id 本身經由 §4.1 的前映像從 version、content_type、created_at、unlock_at、outcome_at、drand_round 與 body_hash 推導而來,因此對上述任一欄位的任何改動都會產生不同的 qub_id,並以遞移方式使簽章失效。因此被認證的範圍為:
| 領域 | 由簽名驗證 | 如何 |
|---|---|---|
version |
✓ | 直接輸入到 sig_input |
qub_id |
✓ | 直接輸入 |
body_hash |
✓ | 直接輸入 |
unlock_at |
✓ | 直接輸入 |
sender_label |
✓ | 直接輸入通過 sender_label_hash (V2 預映像 — 唯一被接受的形式) |
reply_to |
✓ | 透過直接輸入 reply_to_or_zero (V2 預映像 — 唯一被接受的形式) |
content_type |
✓ | 間接地,透過 qub_id 原像 |
created_at |
✓ | 間接地,透過 qub_id 原像 |
outcome_at |
✓ | 間接地,透過 qub_id 原像 |
drand_round |
✓ | 間接地,透過 qub_id 原像 |
body |
✓ | 經由,中介而 body_hash = SHA3-256(body) |
author_pubkey |
—(隱含) | 根據定義,驗證簽名的金鑰是作者的 |
cosigner_pubkey / cosigner_signature |
— | 獨立簽署過相同的 sig_input (見 §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | 外部 SealedQub 欄位,而不是在信封內 — 由它們自己的結構不變量(輪次 / 鏈一致性)覆蓋,但不由作者簽名覆蓋。(drand_round 現在透過……以遞移方式綁定 qub_id 前像 — 見上文。 |
為何 V2 是唯一被接受的前映像。
- 在已淘汰的 V1 前映像下,擁有儲存位元組寫入權限的一方可以替換
sender_label("Alice" → "Mallory")或重新掛載reply_to——並在公開輪之後重新加密——而不讓作者簽章失效,因為這兩個欄位都不在已簽署的前映像中。V2 對兩者皆涵蓋,因此對任一欄位的任何改動都會使驗證翻轉為「失敗」。由於驗證者如今僅接受 V2,此替換手法對每一個簽章皆已封閉:未繫結這兩個欄位(亦即僅能對 V1 驗證通過)的簽章會被直接拒絕,而非退回接受。 - 信封內部的
author_pubkey仍是真正的身分錨——查看者必須從author_pubkey(經由 §9.5 的認證層)推導顯示身分,而非信任sender_label。
向終端使用者展示 sender_label 或 reply_to 的實作必須把已認證的身分(公鑰指紋、認證)作為主要身分訊號呈現,而非標籤。
9.4 驗證流程
1. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
only accepted form — the legacy V1 fallback is retired (§9.3), so a
signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
簽章驗證是最昂貴的操作(尤其是 ML-DSA-65)。它應在所有更廉價的檢查(雜湊、qub_id、unlock_at)都通過之後再執行。
9.5 身分認證
身分認證——將 author_pubkey 映射到人類可識別的身分聲明(如 qub 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),第二層簽章證明雙方對相同條款表示同意。
信封欄位:
cosigner_pubkey:共同簽名者(Party B)的 ML-DSA-65 公鑰。cosigner_signature:在與作者相同的sig_input上的簽章(§9.3)。
兩個欄位必須同時存在或同時缺失。若僅其中一個存在,查看者必須回報完整性錯誤。
驗證流程:
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."
特性:
- 共同簽名者在與作者相同的
sig_input上簽署——雙方均承諾到同一qub_id、body_hash與unlock_at(且在 V2 下,還包括同一sender_label_hash與reply_to_or_zero)。 - 為使共同簽名者無需存取原始信封位元組即可重建 V2 前映像,暫存服務在暫存時強制要求約定信封的
sender_label等於pact_terms.party_a.label,且reply_to不存在。此兩項對每一個參考使用者端的約定皆成立;違反者的信封在暫存階段即被拒絕。 qub_id推導(§4.1)不包含共同簽名者欄位。向已有信封新增共同簽名者不會改變qub_id。- 一個約定可以僅由作者簽署(單邊承諾)、僅由共同簽名者簽署(少見),或兩者皆有(完整雙邊證明)。
信箱繫結門控(運維層面)。 當暫存的約定攜帶 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 允許的元素
- 標題:
#至####(不允許#####或######) - 強調:粗體(
**)、斜體(*)、刪除線(~~) - 清單:有序(
1.)與無序(-、*) - 區塊引用(
>) - 程式碼:行內片段(```)與圍欄程式碼區塊(`````)
- 水平線(
---) - 換行(行尾兩個空格或空行)
- 段落
10.2 禁用的元素
| 元素 | 處理方式 |
|---|---|
原始 HTML(<div>、<script> 等) |
完全剝離。HTML 不會通過。 |
圖片() |
剝離。輸出中刪除圖片語法。 |
連結([text](url)) |
URL 渲染為可見的純文字。不自動建立連結。未經使用者顯式操作不可點擊。 |
| 危險 URL 協定 | javascript:、data:、vbscript:、file:——剝離。 |
| iframe、嵌入、物件 | 剝離。 |
| HTML 實體 | 僅在安全時解碼為可顯示字元。 |
10.3 實作
實作必須使用嚴格的白名單解析器,而非黑名單。推薦做法:
- 用
pulldown-cmark(或等價工具)解析 Markdown。 - 遍歷 AST 並丟棄任何不在白名單(§10.1)中的節點。
- 對於連結節點:將 URL 作為可見文字輸出,而非可點擊的
<a>元素。 - 將過濾後的 AST 轉換為型別化中間表示(例如僅含安全變體的
MarkdownNode列舉)。原始 HTML 在該 IR 中結構上不可表達。 - 從型別化 IR 渲染到目標檢視層(例如回應式檢視元件、DOM 節點)。任何階段都不進行 HTML 字串拼接或使用
innerHTML。
黑名單方法是脆弱的,因為新的 Markdown 擴充或解析器怪癖可能引入未被過濾的元素。型別化 AST 方法讓 XSS 在結構上不可能存在——沒有任何變體可以攜帶任意 HTML。
10.4 大小與結構限制
- 最大渲染標題深度:
####(H4)。#####及更深者渲染為粗體文字。 - 段落數量不設上限(§6 中的主體大小限制即是約束)。
- 圍欄程式碼區塊:MVP 中不進行語法高亮。渲染為等寬預格式文字。
11. 第三方驗證
任何持有已儲存位元組(以及私人/封裝qub的 K)的第三方都可以 在沒有 QUB 合作的情況下驗證加密產物。獨立地 附時間戳的 存在 聲明還需要經驗證的 per-qub 永久儲存包含或經驗證的 §16 透明度日誌證明。
1. Obtain the stored bytes. For a private delivery, also obtain K from the
delivery-link fragment.
2. Resolve the delivery shape (§8 step 3a): unwrap OuterWrapper with K, or
accept bare SealedQubCbor. Require shape/visibility agreement.
3. Parse SealedQubCbor → SealedQub; validate protocol version, visibility,
content type, chain identity, and structural bounds.
4. Recompute expected_round from unlock_at (§4.3); require the stored round
(allowing the documented legacy minus-one case) and ciphertext-stanza round
to agree exactly as §8 step 6a specifies.
5. Obtain the drand signature for SealedQub.drand_round and verify its BLS
signature against the pinned chain public key.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify envelope/sealed equality for qub_id, unlock_at, and outcome_at.
10. Recompute qub_id from the decrypted fields, sealed drand_round, and title;
require it to equal the carried qub_id (§4.1).
11. If sig_alg != 0x00, verify the V2 author signature (§9.4). If cosigner
fields are present, verify their pairing, key separation, and signature
(§9.7).
12. For an existence-time claim, independently verify either:
a. the permanent-storage transaction's data-to-id binding, owner, block
inclusion, and block timestamp; or
b. a §16 inclusion proof through its pinned anchor and anchor block time.
13. Report each verified claim separately; do not collapse an absent storage
or anchor proof into a successful timing verdict.
驗證證明了什麼:
| 證明輸入 | 它所建立的 |
|---|---|
| 有效套件/封存產物 + drand 簽章 | 還原的本文符合 body_hash;綁定至 qub_id 的中繼資料完好;密文綁定至所宣告的 drand 輪次;且該輪次已經過。這無法證明密文建立於何時。 |
| 有效的 V2 作者/共同簽署人簽名 | 對應秘密金鑰的持有人已在§9.3中對簽署的表面進行了認證。 |
| 經獨立驗證的每量子塊儲存交易 | 精確儲存的密文的存在時間不晚於其區塊時間戳。 |
| 有效的錨定透明度日誌證明 | §16.11 中的葉片種類特定請求,包括來自錨點區塊的上限承諾時間。 |
驗證無法證明的事項:
| 非證明 | 為什麼 |
|---|---|
| 作者身分 | 這 sender_label 是裝飾性的。沒有 sig_alg ≥ 0x01任何人都可以封存這個內容。 |
| 意圖 | 這件人工製品證明的是位元和加密關係,而不是創作者主觀想表達的意思。 |
先前的承諾來自 .qub 孤單 |
創作者可以在綁定回合結束後組裝有效的套件。嵌入的 drand 簽名證明回合已經結束,而不是證明密文在此之前存在。 |
| 精確的封印按鈕時間 | 儲存或錨定區塊時間戳是一個可以獨立驗證的上限,並且可能落後於使用者的本地操作。 sealed_at / received_at 斷言是沒有證據的。 |
所實施的透明度日誌 (§16) 擴展了跨 qubs 的驗證
防篡改 訂購 以及無信任的 上限承諾時間 (這
錨定區塊時間),按葉子種類範圍限定(§16.11)。它不增加作者身分或
意圖;對於預設的盲位元組上傳路徑,它本身並不證明
body_hash 或 drand_round,這些仍然來自於產物檢查。
12. 版本控制與發佈管理
文件版本、內部線路協定與外層包裝各自擁有獨立的版本空間。因此,僅修改文件的澄清不會暗中變更位元組,而未來的線路遷移也不能偽裝成編輯修訂。
12.1 文件版本
本規格使用語意文件版本(MAJOR.MINOR.PATCH) 和
一個不可變的 Git 標籤名為 protocol-v<release>。
- 補丁:不改變符合的位元或所需行為的準確性或編輯更正。
- 未成年人: 向後兼容的規範性新增、新的註冊表條目或新的獨立版本的 sidecar 格式。
- 主要的: 不相容的規範變更,包括新的必需線路解釋。
發行狀態是其中之一 草稿 (尚未規範), 目前 (唯一的
建議的實作目標),或 已取代 (保留作為歷史用途
驗證)。未版本化的 /protocol 路由顯示當前版本;
發佈標籤保留其精確的來源以及隨之發佈的每個語言版本。
更改狀態或版本號需要更新此表格和版本
在同一審閱過的變更中的歷史。
| 文件版本 | 生效日期 | 狀態 | 線路協定 | 包裝版本 | 來源 |
|---|---|---|---|---|---|
| 1.0.0 | 2026-09-23 | 目前 | 0x01 |
0x01 |
protocol-v1.0.0 |
12.2 協議版本
這 version 兩者中的欄位 (u8) SealedQub 和 QubEnvelope 識別主要協議版本。
- 觀眾必須以明確的錯誤拒絕未知的主要版本。
- 在已知的主要版本中,解碼器必須拒絕未知的映射鍵(§3.1)——架構演進是通過引入新的方式發生的
version,而不是通過添加現有解碼器會跳過的鍵。(此規範的早期版本允許容忍未知的可選欄位;該條款已被撤回——它導致encode(decode(x))非單射並在協議有效負載上打開了一個隱藏簽名內容向量。) - 內容類型 (
content_type) 以及簽名方案 (sig_alg) 受版本限制:只有在推出新協議版本或明確的登錄表更新時,才能引入新值。
12.3 協議版本歷史
| 版本 | 價值 | 描述 |
|---|---|---|
| v1 | 0x01 |
私密/封裝和公開/裸送達;文字(0x01), 契約 (0x03),以及判決 (0x04) 身體;ML-DSA-65 V2 作者/共同簽署者簽署;drand quicknet tlock;SHA3-256。 |
12.4 向前相容性
遇到具有未知 CBOR 映射鍵(鍵不在 §3.2 規範順序中)的 QubEnvelope 的 v1 查看器,必須以解碼錯誤(§3.1)拒絕它。向前相容性依賴於 version 欄位,而不是鍵容差:未來的新增項目 — 即使是微小的元資料 — 也會以新的方式發布 version 值,v1 查看器會以明確的「更新的協議」錯誤拒絕,而不是悄悄丟棄簽名所承諾的內容。
遇到 v1 觀看者 sig_alg = 0x01 (ML-DSA-65) 但缺乏 ML-DSA-65 驗證支援的情況下,應該顯示 qub 內容並附上「簽名存在但無法驗證」的提示,而不是完全拒絕 qub。目前的參考實作會拒絕每個 sig_alg 除了…之外的值 0x00 和 0x01 因為 v1 登錄表不包含其他有效的演算法——嚴格拒絕與軟失敗在觀察上是相同的,直到註冊了第三個演算法為止。一旦 §9.2 承認了一個新的條目,上述的軟失敗行為將變得具承載力,並且參考查看器屆時會更新為軟失敗。
12.5 外包裝版本
第13節所描述的外部封裝自帶其自身的 version 位元組, 獨立 的 SealedQub.version 和 QubEnvelope.version這兩個版本空間分別演進:未來的後量子安全對稱替代會提升封裝位元組而不接觸內部協議版本,而未來協議層的新增(例如,新增信封欄位)會提升內部版本而不接觸封裝位元組。
OUTER_WRAPPER_VERSION_* |
價值 | 算法 | 狀態 |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM,使用12位元組隨機數、16位元組驗證標籤,綁定附加認證資料(AAD) qub_id |
啟用私人配送 |
| — | 0x02–0xFF |
保留 | 未來 |
觀眾必須以明確的錯誤訊息拒絕未知的封裝版本。該協議刻意保持封裝版本的空間狹窄,直到出現具體的遷移驅動因素(例如,NIST 指南偏好不同的 AEAD);一 0x02 插槽將在引入該演算法的同一版本中分配。
13. 外層加密封裝
13.1 理由
協定各層(QubEnvelope → tlock → SealedQub)使已封印 qub 變為時間鎖定:主體在 unlock_at 與 drand 輪簽章發布之前不可讀。然而,在解鎖後,輪簽章是公開的,且 SealedQub 的規範 CBOR 形態可被識別,因此索引過永久儲存交易的採集者可批次解密整個 qub 語料庫。
對於私人傳送,外層加密包裝會在規範 SealedQubCbor 與儲存位元組之間加入額外的對稱 AEAD 層,以封閉此通道。在瀏覽器封印路徑中,256 位元金鑰 K 只存在於傳送 URL 的片段及使用者裝置上;瀏覽器不會把 URL 片段傳送至伺服器,因此 qub.social、所有儲存閘道及其前方的 CDN 都無法觀察 K。私人 qub 的儲存表示因而是不透明密文;若沒有創作者選擇分享的 URL,就無法還原其明文。公開傳送則刻意省略此層(§13.8)。
淨效應:
- 針對私人傳送的列舉阻力。
OuterWrapper仍然是可識別的結構化 CBOR——它在字面上並非與隨機位元組無法區分——但其密文欄位隱藏了可識別的內部SealedQub形狀。記錄的收割者策略「對裸 qub 形狀上傳進行 GraphQL 查詢,使用公開 drand 簽名進行批量解密」在沒有 K 的情況下不會終止為明文。 - 預設私人瀏覽器流程的加密銷毀隱私防護姿態。 qub.social 無法解密其預設伺服器端資料中儲存的那些產物。明確的恢復、公開傳送和受信任的伺服器端封存具有不同的公開信任邊界。
- 兩層機密性梯階。 預設 = 連結控制的存取(本節)。接收者加密的私人qub(保留的第二階段功能,尚未指定)作為第二層疊加在其上。
13.2 分層
plaintext body ← QubEnvelope.body (§2.2)
↓ canonical CBOR (§3)
envelope CBOR
↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
↓ canonical CBOR (§3)
SealedQubCbor bytes ← inner wire artifact
├─ public (visibility=0x01) ───────────────▶ stored directly (§13.8)
└─ private (visibility=0x00)
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12, this section)
OuterWrapper CBOR bytes ← stored private payload
協定層的封印與公開(§7、§8)在封裝邊界以下保持不變;封裝在 seal() 呼叫點附加,在 unlock() 呼叫點解除。
13.3 OuterWrapper 資料結構
struct OuterWrapper {
version: u8, // 0x01, see §12.5
qub_id: [u8; 32], // copied from inner SealedQub; AEAD AAD
nonce: [u8; 12], // 96-bit AEAD nonce
ciphertext: Vec<u8>, // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}
欄位不變量。
version必須相等0x01用於 v1.0 包裝器的位元組。qub_id必須等於qub_idSealedQub 解封後回收的領域。兩者皆參考wrap_sealed_qub和unwrap_sealed_qub解析內部 CBOR 並直接強制執行此等式;AAD 綁定分別使外部的後包裹篡改變得qub_id身分驗證失敗。nonce必須是 96 位元(12 位元組),每次包裝操作都由 CSPRNG 全新產生。在相同的金鑰下重複使用 nonce 會允許 AEAD nonce 重用攻擊來回收明文;製造者必須處理(key,nonce) 配對作為一次性操作。ciphertextAES-256-GCM 的輸出是:密文位元組與 16 位元組的認證標籤連接在一起。ciphertext.len() == SealedQubCbor.len() + 16正是如此。
CBOR 編碼。 按 §3 規範 CBOR,使用相同的鍵排序規則(先按編碼位元組長度升序,再按字典序)。四個鍵為:
| 鍵 | 編碼位元組 | 順序 |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
因此 OuterWrapper CBOR 的首位元組是 4 項對映的定長對映頭(0xA4)。
13.4 AAD 與 qub_id 的繫結
封裝將 qub_id 作為 AEAD 附加認證資料繫結。這是抵禦以下三類攻擊的承重結構性防禦:
| 攻擊 | 防禦 |
|---|---|
將密文移到不同的位置 qub_id 包裝器中的欄位 |
AAD 不匹配 → AEAD 認證失敗 |
| 將 qub A 的 URL 片段與 qub B 的已儲存位元組混合 | 錯誤的金鑰(以及獨立綁定的 AAD)→ AEAD 認證失敗 |
竄改 qub_id 上傳後包裝的欄位 |
AAD 不匹配 → AEAD 認證失敗 |
在封裝明文中攜帶 qub_id 並不會顯著削弱列舉免疫——qub_id 本身是 §4.1 前映像的 SHA3-256 雜湊,從摘要無法復原其前映像,且已採集到封裝位元組的列舉者從可見的 qub_id 中所學到的,並不多於其從上傳存在這一事實本身可推斷的。
13.5 封裝與解封演算法
wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
require K.len() == 32 and N.len() == 12 and Q.len() == 32
I := canonical_cbor_decode(S) as SealedQub
require I.qub_id == Q // reject mismatched caller AAD
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C includes the 16-byte authentication tag at the end
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
require K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
require O.version == 0x01 // §12.5
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
S := canonical_cbor_decode(P) as SealedQub
require S.qub_id == O.qub_id // explicit inner/outer cross-check
return P // P is the validated SealedQubCbor
失敗模式塌縮。 錯誤的 K、錯誤的隨機數、AAD 不匹配以及被竄改的密文都會產生相同的 DECRYPT_FAILED 錯誤。這是一種有意為之的 AEAD 特性:區分失敗模式會構成遠端攻擊者可透過傳送畸形封裝並對回應計時來探測的旁路。參考實作必須將所有 AEAD 失敗塌縮為單一錯誤形態。
13.6 金鑰材料與分發
封裝金鑰 K 是由 CSPRNG 為每個 qub 產生的 256 位元均勻隨機值。參考實作的來源為:
- WASM 創作者:
getrandom(在wasm_js後端下使用 WebCrypto)。 - 伺服器端封印 API 呼叫方:其本地 CSPRNG;呼叫方以
wrapper_key_b64url提供並保留K。Worker 於記憶體中將K用於包裝,但不得將其持久化。如此一來,冪等重試便可憑藉呼叫方所保留的能力,復原一則已遮蔽的回應,而無需依賴由伺服器端一次性產生的秘密。
分發:K 必須編碼為 URL 安全 base64(RFC 4648 §5,無填充)並作為片段分量附加到傳遞連結:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
符合規範的瀏覽器不會將片段傳送至任何伺服器。在使用者裝置之外持久化完整傳遞連結(包含片段)的恢復管道——例如伺服器端歷程索引、可選的郵件自動傳送——是對預設加密粉碎姿態的明確折衷,必須由使用者顯式同意。
片段遺失。 若使用者遺失 URL 片段且沒有恢復管道,qub 將不可讀。這是設計中的承重折衷,且必須在封印時向使用者揭露。MVP 透過明確的「儲存此 URL」文案以及對選擇啟用的使用者提供的已驗證信箱恢復管道,加強封印時的揭露。
13.7 本節範圍之外
- 作者簽署 (§9) 未改變:簽名在內部計算中完成
QubEnvelope並在解包 → tlock 解密 → CBOR 解析後恢復。 - 收件人公鑰加密(保留的
recipient_pubkey欄位)是一個未來功能,有別於今天的私有、連結能力控制的封裝模式。 - 目前的伺服器端契約共同簽署流程會發出公開/裸露
SealedQubCbor具有能見度0x01;它無法滿足僅瀏覽器的 K-保密模型,因為最終封印發生在伺服器協助共同簽署之後。未來的私人協議產生器可能會使用相同的封裝器,該封裝器對內部內容類型是位元盲的。
13.8 公開 qub(省略封裝)
外層包裝是 在傳輸層可選. 創作者可以封印一個 qub 如 公共的,在這種情況下,經典的 SealedQubCbor 進入儲存管線 直接地,沒有 OuterWrapper 圖層且無鍵 K:
SealedQubCbor bytes ──(public)──▶ stored as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ stored
一個公共的 qub 是 時間鎖定但不設連結門檻:在其分散輪次發布之前,它保持不可讀(tlock 層保持不變),但解鎖後,任何擁有儲存交易 ID 的人都可以解密它——不需要 URL 片段,因為沒有 K。這是伺服器必須推動的表面上的刻意交易:揭示通知電子郵件、無片段的 oEmbed/自動嵌入連結,以及更豐富的揭示後 SEO,都需要一個在伺服器從未持有的秘密之外仍然有效的連結 (§13.6)。私有 qub 仍然可以使用明確的 <qub-embed src="full_delivery_url"> 當出版商提供其完整的片段承載能力時的形式。
生產者必須考量的後果:
- 沒有列舉豁免權。 公開的 qubs 透過結構設計放棄 §13.1 列舉-免責屬性。參考上傳服務會標記一個
Visibility: public它們上有永久儲存標籤(僅限它們),因此它們是刻意可被發現的;私有 qubs 不帶有這種標籤,並保持其位元不可區分性。 - 明文標題在封印時暴露。 第§3.2條
title欄位內是純文字SealedQubCbor在包裝下,它被隱藏,直到觀眾提供K; 沒有封裝層,它在永久儲存上是全世界可讀的 從上傳的那一刻起在解鎖之前。符合規範的創作者應用程式必須在封印時披露此資訊。 - 偵測以結構為準,並須交叉核對。 符合規範的查看器/嵌入程式會解析儲存位元組來區分兩種形態:可解析為
OuterWrapper者走使用K的解包路徑;可解析為未包裝SealedQubCbor者直接接受。還原後的內部值必須與傳送形態一致(已包裝/私人為0x00,未包裝/公開為0x01)。qub_id不綁定可見性,但規範SealedQub位元組會攜帶可見性,因此公開與私人內部編碼在位元組層級並不相同。
私有(已封裝)仍為預設;公開是每則 qub 由創作者明確做出的選擇。
14. 測試向量
14.1 qub_id 推導
Input:
version = 0x01
content_type = 0x01
created_at = 1735689600 (2025-01-01 00:00:00 UTC)
unlock_at = 1736294400 (2025-01-08 00:00:00 UTC)
outcome_at = absent
drand_round = 4695446 (= floor((1736294400 - 1595431050) / 30) + 1, §4.3 mapping, drand mainnet params §14.2)
body = "Hello, future." (UTF-8, 14 bytes)
title = absent
Intermediate:
body_hash = SHA3-256("Hello, future.")
= 76ab8b3f843c6ed4f2d0fd75b9f457b4
ad49dd4450f9c22723ae430e3af3211d
title_hash = [0u8; 32] (title absent — §4.2.1 sentinel)
Domain separator (10 bytes):
[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]
Preimage (108 bytes—current protocol v1):
domain_separator || // 10 bytes
0x01 || // version
0x01 || // content_type
0x0000000067748580 || // created_at as i64 big-endian (1735689600)
0x00000000677DC000 || // unlock_at as i64 big-endian (1736294400)
0x0000000000000000 || // outcome_at_or_zero (outcome_at absent)
0x000000000047A596 || // drand_round as u64 big-endian (4695446)
body_hash || // 32 bytes
title_hash // 32 bytes (all-zeros sentinel; title absent)
Expected output:
qub_id = SHA3-256(preimage)
= 4a84e3dfaec32954949c30073f8e6506
fd3204c1bb97f9162b81c7587afe412e
實作必須產生相同的 body_hash 和 qub_id 此輸入的值。此測試向量應該是編寫的第一個單元測試。上述標準值是由參考實現計算的,必須逐位匹配。歷史上在發布前的原型佈局(沒有實際qub依賴前兩個)使用了 92 個位元組。 outcome_at (3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0) 並在添加後增加 100 位元組 outcome_at_or_zero (b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed而當前的 108 位元組佈局隨後被添加 drand_round 和那 QUB_ID_V2 領域分隔符。早期的 108 位元組向量使用舊有的 ceil 輪次映射(drand_round = 4695445) 並產生 3a9fcb31b750d985c262fada6d4f777fd6a28be831d941d85c131f5a4bbaf8a4—仍然有效 qub_id 對於該輪輸入,儘管上面的例子遵循第 §4.3 節的當前輪映射。
14.2 解鎖-回合對應
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calculation:
(1735689600 - 1595431050) / 30 = 4675285.0
floor(4675285.0) + 1 = 4675286
drand_round = 4675286
第 4675286 輪發布於 1595431050 + (4675286 - 1) * 30 = 1735689600—正好在 unlock_at,前所未有。(傳統預發布版本 ceil 映射給 4675285,發表於 1735689570—提前30秒;驗證者接受根據§4.3的舊回合。
14.3 規範 CBOR 往返
實作必須驗證對所有有效輸入都滿足 serialize(parse(serialize(qub))) == serialize(qub)。這是一項性質測試,而非單一向量。
14.4 PactTerms CBOR(content_type 0x03)
Input:
pact_version = 1
title = "Scooter deposit"
terms = [
{ key: "Item", value: "Honda Metropolitan scooter" },
{ key: "Price", value: "$100" },
{ key: "Deposit", value: "$10" }
]
party_a = { label: "Alice" }
party_b = { label: "Bob", contact: "bob@example.com" }
notes = absent
Canonical CBOR key order (PactTerms):
"notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)
Canonical CBOR key order (PactTerm):
"key"(4) < "value"(6)
Canonical CBOR key order (PartyIdentifier):
"label"(6) < "contact"(8)
規範 CBOR 位元組與 SHA3-256 body_hash 由參考實作計算。對此輸入,實作必須產出位元組相同的 CBOR。
實作還必須驗證對所有有效 PactTerms 輸入都滿足 serialize(parse(serialize(pact))) == serialize(pact)(性質測試)。
14.5 外層封裝跨語言向量
外層封裝(§13)的規範固件位於 crates/qub-core/tests/vectors/wrapper_v1.json。每個用例將一個 (key, nonce, qub_id, sealed_cbor) 元組固定為不透明十六進位輸入,並斷言一個特定的 expected_wrapper_hex 輸出。兩套參考實作消費同一個 JSON 檔案:
- Rust:
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors)。 - TypeScript:
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test)。
目前的測試夾具固定三個低階包裝案例。它們測試確定性 OuterWrapper 編碼與 AEAD 互通性,與 §13.8 的傳送形態不變條件分開;尤其是歷史名稱 basic-text-public 及其內部 visibility = 0x01,不會使產生的包裝位元組成為符合規範的公開傳送。產生端仍必須直接儲存公開內部位元組,且只包裝私人(0x00)內部位元組。
| 案件 | 涵蓋範圍 |
|---|---|
basic-text-public |
歷史低階裝置名稱。最小的現實尺寸 SealedQub 形狀,沒有可選欄位;僅測試封裝位元組,並非符合 §13.8 的儲存交付。 |
with-recipient-pubkey |
SealedQub 和 recipient_pubkey 設置(保留的未來路徑)。使用不同的內部 CBOR 鍵集;其獨特的固定內容獨立產生不同的 qub_id (recipient_pubkey 本身不在 §4.1 的前像中。 |
longer-body |
~4 KiB 主體 — 在內部信封和外部密文中都練習多位元組 CBOR 長度前綴。 |
對已記錄的輸入,實作必須產出位元組相同的 expected_wrapper_hex。重新產生該固件需要 QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors,且僅用於刻意的格式變更。
15. 加密配置治理(未來)
本節對 v1 而言是參考性的,且在 qub 任一加密原語中首次進入第二個演算法時變為規範性。
15.1 當前姿態
協定 v1 對每種原語恰好繫結一個演算法:
- 簽名: ML-DSA-65 (
sig_alg = 0x01;1952 位元組公鑰,3309 位元組簽名)以及未簽名(sig_alg = 0x00). 程式碼庫保留0x02對於 Ed25519,但協議 v1 不會啟用它;v1 驗證者必須拒絕每一個sig_alg外面{0x00, 0x01}。 - 時間鎖: 僅限 drand quicknet — 鏈雜湊、公鑰、創世時間及週期是隨參考攜帶的固定網路參數
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) 和config/drand-endpoints.json。 - 外包裝: 僅 AES-256-GCM v1 (§13)
驗證器目前對每個活動原語硬編碼金鑰和簽名長度。 sig_alg 而 wrapper-version 位元組是明確的選擇器,但 v1 不進行帶內協商,僅接受上述的有效值。
15.2 目標形態
當第二個演算法進入協定時,驗證者將以命名化的 CryptoProfile(例如 ExqubV1)進行配置,列出每種原語所允許的精確取值集合——sig_algs、drand 鏈、封裝版本、內容類型。該配置在驗證時固定,決不在帶內協商。任何不在活動配置內的取值都將被拒絕。
這保證新增 ML-DSA-87 或啟用 Ed25519 不能追溯性地削弱已有的驗證者配置:v1 驗證者在 v2 配置發布之後仍然是 v1 驗證者。
15.3 觸發條件
當出現以下任一情況時,將 §15 提升為規範性狀態:
- 一秒
sig_alg位元組(Ed25519 啟用、ML-DSA-87,或 §9 登記冊中的任何新條目)。 - 第二個正在生產使用中的 drand 鏈。
- 第二個外層包裝版本。
- 透明度日誌信任根的輪換 —
LogProfile.anchor_owner位址或固定收據金鑰公開金鑰(§16.6)。該LogProfile作為受控基元加入 §15.2 輪廓曲面:旋轉是帶符號的LogProfile在驗證器更新中已發送的提升(計劃輪換跨簽發出 → 收到;基於妥協的輪換無法做到,並依賴這個提升以及先前錨點分叉檢查來限制中間損害)。透明度日誌版本空間(LOG_VERSION,ANCHOR_FORMAT) 以獨立的兄弟形式演變,就像 §12.5 套裝版本與協議版本是獨立的一樣。
在此之前,§15 是一個占位章節,用於固定遷移形態,使未來的 PR 朝既定目標落地,而非從零開始重新爭論協商介面。
16. 透明性日誌與持久性層級(設計——審查完成)
狀態。 本節已在 W5/UP-B1 第 1–8 階段實作,以下明確界定產生端與信任根的範圍。線路格式、雜湊及驗證路徑均已投入使用:核心 Merkle 與規範 CBOR 類型(
qub-core)、TypeScript 對應實作與 ANS-104 打包工具(workers/api/src/crypto/)、單一寫入者LogDO與依座標索引的 R2 節點儲存、/upload的日誌附加嘗試、每日錨定與打包器排空排程、GET /api/v1/qub/:tx_id/proof包含證明端點、GET /api/v1/log/consistencyRFC 9162 一致性證明端點、.qub套件所攜帶的型別化包含證明(§17.5)、原生 ANS-104 錨點驗證器(tools/qub-verify),以及雙重自行發布樹頭的掛鉤(§16.6)。成功的/upload一律先耐久寫入 R2;只有在已設定LOG_DO且行內附加成功時才受日誌涵蓋,也只有此時回應才包含log_seq、receipt與anchor_status。若RECEIPT_SK缺少或無效,收據的sig_b64url為空,不能提供不可否認性。目前/seal與 pact 發布路徑會排程個別 Arweave 交易,但不會附加日誌葉節點;也沒有程式碼會在附加失敗後執行/upload註解所提議的後續調和。W5 外部審查已完成:§16.15 記錄設計決策與啟動限制,但不會擴大上述產生端涵蓋範圍。三項信任/部署事項仍受閘門限制:(a) 專用錨點錢包(ANCHOR_JWK;LogProfile.anchor_owner仍是[0xAB; 32]預留值);(b) 收據簽署金鑰及相符的公鑰固定值(RECEIPT_SK為選用,且LogProfile.receipt_pubkey目前為空);(c) 自行發布樹頭所用的 GitHub 儲存庫與權杖(§16.6)。在錨點與設定檔固定值完成配置前,獨立驗證器會如實回報證明狀態,不會聲稱已完成錨定且固定信任根的驗證。此設計完全採附加方式,不會變更SealedQub/QubEnvelope線路格式。
16.1 理由與持久性層級
目前的發布路徑將請求確認與 Arweave 確認分離:系統會推導並簽署個別交易,把產物與精確提交狀態保存至 R2,再非同步發布。對於成功附加至 LogDO 的一般 /upload 請求,透明度日誌另行提供具獨立錨定的排序層:
| 層級 | 名稱 | 保證 | 何時 |
|---|---|---|---|
| T1 | R2-首次同步確認 | 耐久性保障——封存的位元組和精確的發佈狀態會在回傳成功之前寫入耐久性儲存。 | 已在當前出版途徑中實施。 |
| T2 | 批次透明度日誌包含 | 僅追加、可檢測篡改的承諾 + 一旦納入並固定後的總排序。 | 當前製作人:成功 LogDO 附加自 /upload; 回應帶有收據元組。不是通用的。 |
| T3 | Per-qub Arweave 永久性 | 一個針對 qub 的單獨 Arweave 交易。 | 目前已為每個接受的出版物準備好,並異步張貼;確切的簽署交易將保留在可排出收件匣中,直到交付為止。 |
這些層級描述的是不同的證據和耐久性屬性,而不是當前的商業計劃。現行程式碼仍然為每個被接受的發佈安排單獨的 Arweave 交易;它並不僅將 T3 作為付費升級功能暴露。API 金鑰/帳戶配額上限仍然是獨立的應用程式控制。
耐久性 誠實。 T1 寫入是同步的,因此成功的回應能在不等待 Arweave 門戶的情況下建立應用層的耐久性。它本身不會建立獨立的時間戳。已確認的單筆交易會提供其區塊時間的上限。對於攜帶完整 T2 收據元組的回應,下一個已確認的錨定可以提供下文所述的日誌證明。如果元組缺失,則沒有表面跡象可以暗示此 qub 已經在透明日誌中。錨定和發佈延遲沒有協議層面的數值 SLA。
16.2 LogLeaf 結構(兩種已底定形態)
一筆日誌條目是一個 LogLeaf,以 §3.1 配置下的手寫規範 CBOR 編碼(定長、無標籤、無浮點、最短形態整數、NFC 文字、可選欄位在缺席時省略、鍵先按編碼位元組長度升序再按位元組序排列)。§3.1 的 parse → re-encode → compare 規範守衛在雜湊之前的編碼路徑上套用(而不僅在解碼時),如此兩個實作便不會因整數寬度或鍵序差異而對葉位元組產生分歧。所有整數皆為 u8 / u64 / i64;所有摘要皆為 32 位元組的位元組字串(bstr[32])。已儲存的永久儲存交易 id 是一個原始的 32 位元組 SHA-256 摘要,以 bstr[32] 攜帶,決不是 base64url 文字字串(與 §3.3 一致)。
這片葉子有 由一個選取的兩個形狀 kind 位元組,因為一般上傳路徑是位元組盲的: POST /api/v1/upload 故意將兩種已接受的有效載荷形狀視為不透明並接收 qub_id 和 unlock_at 僅作為不受信任的客戶端聲明。在預設的私人路徑上, body_hash, drand_round, created_at,以及 drand_chain_version 還額外隱藏在 §13 外層包裝內,工人從未持有其金鑰。類型系統還為派生的生產者定義了一個經過認證的形狀 body_hash / drand_round 它本身。當前 /seal 路由有那些值,但沒有調用 LogDO,所以目前生產僅排放已聲明的(0x02) 從成功的一般上傳附加中留下。這種分割保持每個已提交的值都是誠實的,而不假裝被證明的生產者已連線:
| 鑰匙 | 封裝長度 | 類型 | 存在 | 意思 |
|---|---|---|---|---|
seq |
四 | u64 |
必需的 | 全局以 0 為基礎的葉子索引;包含證明提交的位置。 |
kind |
五 | u8 |
必需的 | 0x01 已證實可行(已定義,當前未發出)或 0x02 已斷言(客戶端封印 / 位元盲上傳)。 |
ref |
四 | bstr[32] |
必需的 | 葉節點參考ID。已證實 → 原始 qub_id. 斷言 → the 失明的 身分證 SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1)。 |
chash |
六 | bstr[32] |
必需的 | 內容位址 SHA3-256(stored_bytes) ——那個勞工總能在兩條路上誠實計算的唯一內容聯繫。 |
unlock_at |
十 | i64 |
必需的 | 複製(經證明)或聲稱(聲稱);驗證 > 0 在它進入葉子之前。 |
received_at |
十二 | i64 |
必需的 | R2-ack 的工作者時鐘。 非證據性的 (操作員聲明;§16.6)。用於自我描述,從不作為證明。已驗證 > 0。 |
body_hash |
十 | bstr[32] |
kind=0x01 僅 |
省略於 0x02 — 根據第13條,該工人缺乏它。 |
drand_round |
十二 | u64 |
kind=0x01 僅 |
省略於 0x02。 |
一個 kind=0x02 葉刻意既不承諾 body_hash 也不承諾 drand_round:它認證的是位於內容位址 chash 的一段不透明密文的承諾與排序,並聲稱 qub_id 與 unlock_at——而非其明文或輪。一個已斷言 qub 的明文/輪那兩條腿來自既有的 §11 .qub 套件驗證,而非來自日誌(§16.11)。drand_chain_version 不在葉中(在預設路徑上它位於封裝之內);鏈粒度位於錨定上(§16.7)。編碼器紀律:拒絕全零的 ref 或 chash,並拒絕非正的 unlock_at / received_at,鏡映 cbor.rs 中的 outcome_at > 0 哨兵守衛。
16.2.1 私有 qub 盲化
日誌不得成為 §13 外層封裝意在防止的那種列舉預言機(§13.1)。對一個私有(已封裝)qub,asserted 葉承諾盲化識別符 SHA3-256(qub_id ‖ log_blind_secret),其中 log_blind_secret 是一個伺服器持有的機密,且省略 body_hash。第三方無法將這樣的葉繫結到一個特定 qub_id;而持有傳遞 URL 因而擁有 qub_id 的 qub 持有者,可重新計算該盲化以確認自身的納入。一個公開 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 歸類為與其他伺服器機密同一保管層級的關聯/女巫等級機密,且僅向前輪替(一次輪替會重新盲化未來的葉;它無法追溯地解連結已被錨定的葉)。
16.3 葉與節點雜湊
RFC 6962 §2.1 的域分隔雜湊,以 SHA3-256 取代 SHA-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 樹與批次化
存在一棵恆久增長的、涵蓋所有葉並依 seq 序的 RFC 6962 樹——而非各自獨立的 per-batch 樹。(一種以攜帶葉串接成鏈的 per-batch 構造遭到否決:它不是真正的前綴關係,故其「一致性證明」並不健全。)累積樹給出真正的 RFC 9162 一致性證明,並使單一近期錨定能證明任何較舊 qub 的納入。
這 LogDO 耐用物件是 單一作家 (blockConcurrencyWhile,鏡像 QuotaDO / EntitlementDO) — 將內容附加到共享日誌是在共享狀態上進行讀-修改-寫,因此必須通過 DO,而不能通過 KV。它緩存樹的右邊緣前緣(O(log n) 雜湊)因此關閉一個批次是 O(batch). A 批次 是一組固定在一起的葉子;其實現的觸發器是 tree_size 至少提前 LOG_BATCH_MAX_LEAVES (預設 4096),年齡達到錨點節奏,或明確的管理/排程強制關閉。 root_i 是葉子上的累積默克爾樹雜湊 0 .. tree_size_i。
16.6 經由永久儲存錨定的已簽署樹首
該永久儲存錨定交易即是已簽署樹首,並取代了樹首本身的營運者簽章:每日錨定不需要任何 qub 金鑰,因為永久儲存交易的 owner 即是該簽章。護城河論點成立——承重的是不可變基底,而非某個 qub 持有的機密,才是已錨定根的根本。
日誌設計要求一次溫暖的成功追加 收據金鑰 (§16.10),固定於 LogProfile 並由…交叉簽署 anchor_owner. 目前的實作尚未完成該信任根的配置: RECEIPT_SK 是可選的,缺失/無效的鍵會產生 sig_b64url: "",以及編譯的 LogProfile.receipt_pubkey 是空的。這樣的收據可以描述附加的葉子,但 不 不可否認的簽署收據。只有在驗證者釋放與之匹配的公鑰且錨點擁有者進行交叉簽署後,更強的設計聲明才適用。沒有完整收據元組的發佈回應不提出任何日誌接受聲明;有空簽名的回應提出追加位置聲明,但不提出簽名驗證聲明。
SignedTreeHead 為規範 CBOR(鍵按編碼長度):size:u64、root:bstr[32]、batch:u64、prev:bstr[32](先前的 sth_hash;創世 = 32 個零位元組)、log_id:bstr[32]、first_seq:u64、anchored_at:i64。其雜湊為 sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead))。
已釘定信任根。 log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address)。一個符合規範的驗證者必須要求 anchor_tx.owner == LogProfile.anchor_owner,其中 anchor_owner(與回執金鑰公鑰)作為 LogProfile 烘焙進 qub_core——與 DrandTimelockProvider::quicknet() 中既有的 quicknet 常數並列——並與驗證者二進位檔一同分發。驗證者還必須在本地驗證該永久儲存交易的 data → tx_id 繫結,而非信任閘道的 /raw/ 回應。這封堵了流氓錢包的模稜兩可漏洞:在驗證者釘定是哪一個錢包之前,「錨定於永久儲存」毫無意義。
輪替是一項 §15 治理擴充,而非重用(已解決——§16.15 Q3)。 §15.2 的配置介面目前僅列舉 sig_algs / drand 鏈 / 封裝版本 / 內容類型,且 §15.3 的觸發條件未列出其中任何一項——LogProfile / anchor_owner 尚未進入 §15 的介面。因此輪替治理必須建立:§15.3 被擴充(如下)以加入 LogProfile 觸發條件,而一次輪替是一個在驗證者更新中發布的、已簽署的 LogProfile 升版。一次有計畫的輪替攜帶一個輸出 → 輸入的交叉簽章;一次因洩而驅動的輪替無法攜帶(恰在其時輸出金鑰是不受信任/不可用的),並回退到 §15 治理下的升版,由(下文的)prev-anchor 分叉檢查在過渡期內限定損害。
含糊窗口(一級信任參數)。 一片葉子只有在其覆蓋錨點是 Arweave 時才具備抗含糊性已確認. 窗戶是 received_at → anchor confirmation (節奏 + Arweave 最終性,無協議延遲保證)。在信任根供應之前,目前的實現提供了 qub 的運行完整性以及現有的任何未簽名附加元資料;它不提供計劃中的不可否認性保證。三個問責制文獻定義了完整設計(見證模型為 §16.15 Q2 的決議):
- 封條收據(依補給而定) — 當上傳的日誌附加成功時,SCT 類比回傳 (§16.10)。它只有在……時才具有不可否認性
sig_b64url非空 和 匹配的公鑰/錨點擁有者關係已固定在驗證器中。當前空的生產配置固定無法支援該判定。此控制不適用於被省略的收據元組或未簽名的收據。 - 已發佈的監控方法 + 前鏈行走 — 錨
prev鏈條是從頭走到創世記;一個叉子(在一個處有兩個錨)size不同的root,或一個破碎的prev)是可發表的不當行為證據。模棱兩可檢測是一項明確的操作承諾,而不是默默的假設。 - 雙重自費出版頭像 — 每一個新的頭
{sth_hash, tree_size}已張貼到專屬的 qub 擁有 公開的、僅可附加的 GitHub 存放庫 (承重防篡改自我出版腳),僅以社交貼文作為最大努力的證實。發布失敗必須使用分頁(不得默默失敗)。已實作(階段 8)為publishHead掛在錨上 cron (workers/api/src/utils/heads-publish.ts): aPUT到內容 API 沒有sha是僅附加的 (a422意思是 head 已經被發布,從來不是覆蓋);選擇加入 / 部署受控於門檻PUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO}並且在儲存庫被配置之前都是惰性的。透過 GitHub 的硬性故障頁面health_alert通道並觸發一個耐用的失敗指標(m:tlog_publish_head_fail); Arweave 錨點本身在發布失敗時從不回滾。「不默默失敗」由那個持久指標保證 — 營運團隊必須在儀表板上配置警報 — 即使最佳努力的電子郵件頁面無法送達。「先進錨點」(cron 只有在大小增加時才會發布) 帶來兩個誠實的限制:短暫的 GitHub 故障會留下 間隙 在該大小的已發佈頭部序列中——有界的,非靜默的(它會分頁),並且因為每個頭部都提交了一個超集合樹,一個§16.9一致性證明彌補了這個差距;關鍵是,該一致性證明是從...計算得出的 權威的 Arweave 錨定樹,而非來自 GitHub 表面,所以 GitHub 的缺口從不削弱可驗證性。一個填補已發布版本缺口的追趕補充是一種延期的改進。
誠實的約束(約束條件)。 因為 qub 控制了兩個計劃中的發布平台,這是 自費出版,未經獨立見證。任何產品、行銷或法律表面不得聲稱該日誌是「獨立見證的」。在收據/配置檔/頭閘被配置後,允許的聲明是 模棱兩可是可被檢測到的,而成功簽署的附錄會留下不可否認的收據在那之前,該主張不可使用。一位真正獨立的第三方證人將延後到未來的§15治理更新。
received_at 為營運者斷言,且任何聲稱皆不得倚賴它——它決不在任何產品/法律/API/證明渲染介面上被呈現為證明或爭議佐證。永久儲存錨定區塊時間 T 是唯一的免信任時間戳(「記入日誌時間」的上界)。任何對 received_at 的監測健全性檢查必須與 T 比對,而非與營運者控制的 anchored_at STH 欄位比對;這類檢查僅是針對一個誠實營運者時鐘錯誤的防護,而非針對一個惡意營運者的問責控制(§16.15 Q5)。
16.7 錨定交易格式與節奏
AnchorBundle 是經由 §16.8 打包器寫入的規範 CBOR 永久儲存交易主體:ver:u8、sth:bstr(規範 SignedTreeHead 位元組)、prev_anchor:bstr(先前錨定交易 id 的原始位元組;創世時省略)、chain_hash:tstr(生效中的 drand 鏈——quicknet),以及該批次依 seq 序的葉-CBOR 串流,使錨定自我完備:一個監測者零 qub 依賴地從主體重新推導出 root。(若該葉串流在高流量下變得龐大,未來一次修訂可僅以參照承諾一個葉範圍;已記錄,但未在 v1 採納。)
永久儲存標籤刻意可列舉——與私有 qub 不同,日誌意在被找到:App-Name: qub-tlog、Anchor-Format: 1、Log-Id: <hex>、Batch: <n>、Tree-Size: <n>、Root: <hex>、Prev-Anchor: <tx>、Content-Type: application/cbor。標籤是不受信任的提示;CBOR 主體是唯一權威。
節奏: 預設每日,重新審視了音量(尺寸觸發會在負載下自動縮短有效節奏)。當前的生產者未實現付費封印強制錨點掛鉤。 錨錢包 是專用的且低速的,與上傳錢包分開——它必須是它自己的 自有 JWK (一個獨立的金鑰,而不是上傳錢包上的邏輯角色)因此上傳錢包的損害無法偽造錨點 — 並且每天有固定的錨點交易預算。託管狀態明確說明:一個 具有限制範圍熱鍵、緊密斷路器和低平衡的裝置不是「冷錢包」——一個每天自動簽名的錢包不可能是冷錢包,而且規格也沒有假裝它是冷錢包。
16.8 ANS-104 打包器
一個自製的 ANS-104 DataItem 編碼器與 deep-hash 簽署器,約 300 行,僅用 Web Crypto、零 npm 相依(兩套 Turbo SDK 都通不過 npm ci --ignore-scripts 供應鏈閘)。DataItem 位元組佈局:
signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data
簽署為永久儲存的 deepHash——對 ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] 進行一次遞迴的 SHA-384 摘要(永久儲存的線路要求,crypto.subtle.digest("SHA-384"))——然後以錢包 JWK 透過 crypto.subtle 對該 deep hash 進行 RSA-PSS;id = base64url(SHA-256(signature))。此處的 SHA-384 被隔離為一個永久儲存線路專屬的原語,決不作為 qub 信任原語(§15 記錄了該圍籬;qub 信任雜湊全程為 SHA3-256)。
ANS-104 程式碼路徑服務延遲回退/排水機制並進行寫入 AnchorBundle 資料項目。普通的發佈路徑首先會建立一個精確簽名的 Arweave 交易,並將其 JSON 保存在持久的發送箱中;直接發布是一種延遲優化,而清空路徑會在應用其捆綁器備援之前重試相同的交易。 簽名方案(已解決 — §16.15 Q8):v1 使用 RSA-PSS 簽名(簽名類型 1) 重用現有的 Arweave 錢包 JWK 機制(零新增長期金鑰托管,符合「少一個秘密」的論點);Ed25519 被延後至 §15 PQ 遷移路徑。
該手寫 deep hash 是 W5 中最高風險、自然覆蓋率最低的程式碼,故其把關不容妥協(§16.15 Q8):
- 跨語言固件
tlog_v1.json(Rust + TS,即 §14.5 的wrapper_v1.json模式)涵蓋 deep-hash、DataItem 位元組 + id、葉雜湊、一個 5 葉根 + 稽核路徑、一個 STH 雜湊、一個納入證明,以及一個一致性證明——在簽署與驗證兩個方向上(驗證方向之所以重要,是因為 §16.6 的本地 tx → tx_id 檢查把 deep hash 拉進了每個獨立驗證者,而不僅是寫入者)。 - 一次性、透過一個參考 ANS-104 打包器的互通往返,僅作為靜態測試資料消費——決不作為 npm 執行期相依(僅用 Web Crypto/無安裝指令稿的姿態維持不變)。
- deep-hash + RSA-PSS 路徑必須透過生產所用的同一套
crypto.subtle原語往返,使該自製編碼器位元組相容。 - 一個持續的打包後驗收監測器確認每個錨定/後援 DataItem 確實達成永久儲存接受,並帶警報 + 斷路器——因為該 deep hash 也服務於永久儲存不可用時的後援佇列,故一次無聲倒退會在它意在涵蓋的那次中斷期間,以網路拒絕的項目填滿該佇列。
16.9 納入與一致性證明
兩者皆為 RFC 9162、SHA3-256,以規範 CBOR 供應。
InclusionProof——GET /api/v1/qub/:tx_id/proof:ver:u8、leaf:bstr(確切的葉 CBOR——驗證者自行重新計算 leaf_hash,決不信任所提供的雜湊)、index:u64、size:u64、audit:[bstr[32]]、root:bstr[32]、anchor:{ txid:bstr[32], batch:u64, sth:bstr[32], log_id:bstr[32], block_height?:u64, anchored_at?:i64 }。
ConsistencyProof——GET /api/v1/log/consistency?first=<size_a>&second=<size_b>:ver:u8、first_size:u64、second_size:u64、first_root:bstr[32]、second_root:bstr[32]、nodes:[bstr[32]]、first_anchor、second_anchor。一份單一無歧義的鍵清單,由測試向量釘定。
獨立驗證(無 qub 伺服器,擴充 §11):
1. Parse .qub bundle → SealedQub; recompute qub_id (§4.1).
2. Read leaf.kind.
3a. kind=0x01 (attested):
assert leaf.ref == qub_id
assert leaf.body_hash == SHA3-256(body)
assert leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserted):
assert leaf.ref == SHA3-256(qub_id || blind) // holder supplies blind
OR treat ref as opaque and bind via leaf.chash == SHA3-256(stored_bytes)
4. Recompute leaf_hash = SHA3-256(0x00 || leaf); fold `audit` per RFC 6962
using index/size; require derived root == proof.root.
5. Fetch anchor.txid from any gateway; verify the tx data → tx_id binding
(do not trust a gateway /raw/ response); REQUIRE anchor_tx.owner ==
LogProfile.anchor_owner.
6. Parse AnchorBundle; require committed root == proof.root and size ==
proof.size; read the Arweave block time T.
7. Emit the claim scoped by leaf.kind (§16.11).
證明供應儲存必須以座標為鍵(已解決——§16.15 Q7,阻塞性前提)。 冷葉證明產生在正確性上是中性的,僅當 R2 稽核材料是一個以絕對樹座標 (level, index) 為鍵的持久 Merkle 節點儲存——而非 per-batch 節點增量。在一個以座標為鍵的儲存下,任何 (leaf i, size N) 稽核路徑都是一組 O(log N) 次直接 R2 GET,且跨批次邊界無需重新計算;在一個以批次為鍵的儲存下則否,而這正是本解決方案所封堵的儲存佈局缺口。葉主體同樣以 seq 為內容可定址。一個 W5 測試向量必須證明一個創世時代的冷葉,在 LogDO 儲存被清空的情況下僅以 R2 + 永久儲存對抗一個遠晚得多的根,使 §16.13 中的回收安全性聲稱有所支撐而非僅僅斷言。那些 O(log N) 次循序 R2 GET 只屬於非同步證明端點——決不屬於封印熱路徑(§16.10)或 per-tick cron。
16.10 R2 優先確認排序
已實施的 POST /api/v1/upload 序列是:
- 前半部分的閘門(驗證、驗證、冪等性分片鍵)——保持不變。
- 建立、標記並簽署確切的單一 Arweave 交易。這衍生自
tx_id在本地,雖然交易建立可能會從閘道取回獎勵/錨點元資料,但準備失敗仍會在確認前使請求失敗。 - 同步地 將選定的產物寫入到
qub-cache/<tx_id>並持久化穩定的建立操作/發件箱記錄。這些是耐久性和重試的底線;在結算之前的失敗會回傳 503。 - 何時
LOG_DO已配置, 同步嘗試LogDO.append(leaf). 單一作者指派seq,擴展條目鏈,並更新前沿。追加 RPC 只執行這個操作;批量關閉在警報上離線運行。當前追加傳輸/應用失敗是 失效安全:回應仍然可以在沒有…的情況下成功log_seq,receipt,或anchor_status儘管有實作的評論,但今天沒有自動後續日誌對帳的機制。 - 回傳確認。包括
{ log_seq, anchor_status: "pending", receipt }只有當 append 回傳完整成功的元組時。receipt.sig_b64url當收據簽署人不可用時為空;客戶絕對不可以將該值稱為已簽署或不可抵賴。元組的缺失僅表示持久發布,而不表示透明日誌已接受。 - 使用一個延期任務來發布精確的簽名交易。成功時刪除發件箱;失敗時保留給有界排空定時任務,且不得更改已經確認的內容
tx_id暫時的元資料和其他盡力而為的附屬項目也被延期。
延遲邊界。 請求路徑包括前端授權/配額工作、交易準備/簽署、耐久 R2 寫入,以及(在配置時) LogDO 嘗試。 < 300 ms 在設計審查中顯示為運行目標,而不是協議保證;當前的交易準備步驟可能執行閘道元資料請求。延遲警報和啟動閘是運行控制,而不是驗證者可用的證據。
16.11 信任模型——精確的聲稱,依葉 kind 限定範圍
對 kind=0x01(已認證): 「此內容——主體與 body_hash 匹配、由 qub_id 識別——已於位置 seq 承諾至 qub 的僅附加日誌,且其存在不晚於永久儲存區塊時間 T;在 drand 輪 R = unlock_round(unlock_at) 之前它在密碼學上不可讀。」 這是完整的 {tlock 輪繫結 + Merkle 納入 + 已錨定根} 三元組。
對 kind=0x02(已斷言,即預設): 「一段內容位址為 chash、聲稱 qub_id 與 unlock_at 的不透明密文,已於位置 seq 承諾至僅附加日誌,且其存在不晚於永久儲存區塊時間 T。」 輪與主體那兩條腿由既有的 §11 .qub 套件驗證(qub_core::unlock)提供,而非由日誌提供;日誌相較於一筆裸的 per-qub 交易所增添的,是防竄改排序、一個免信任的上界承諾時間,以及抗模稜兩可性。
依 §11,兩種聲稱皆排除:無 sig_alg ≥ 0x01 的作者身分、意圖,以及次錨定粒度的時機。兩者皆不讓任何聲稱倚賴 received_at。
聲明上限(約束性啟動限制 — 已解決 §16.15 Q1)。 對於一個聲稱(kind=0x02) 葉,上述範圍的聲明是 天花板 任何產品、市場行銷、條款或證明呈現表面上可能聲稱的內容。任何表面不得聲稱或暗示 日誌 證明了位元盲上傳的內容或解鎖回合——日誌證明的是 排序 + 不信任的上限承諾時間對不透明密文。內容和回合證明完全來自現有的 §11 .qub-包驗證,與日誌無關。沒有成功附加/接收的發佈完全沒有任何日誌聲明。
16.12 版本控制與 W3 協調
這項功能不會提升 SealedQub 線路版本,因此也不會提升協定版本(§12.2):日誌是附屬機制,只對既有欄位與位元組作出承諾,不列入 §12.3 的協定版本歷史。W3 的選用 drand_chain_version 不受影響,仍是 SealedQub 唯一的選用欄位。日誌另有獨立的版本空間——LOG_VERSION_1、ANCHOR_FORMAT_1、InclusionProof.ver——呼應 §12.5 的包裝版本獨立性:包裝的版本位元與協定版本分離,日誌版本亦採相同原則。
證明傳遞預設為抓取,並帶一個可選的隨行物。 一個證明在封印時不可能存在(錨定尚未寫入),因此封印時的 .qub 套件保持無證明。W7 的驗證者抓取一次 GET …/proof,或在完全離線模式下,經由對 Log-Id 的一次永久儲存查詢從公開的 AnchorBundle 重建該證明。.qub 套件(W7)保留一個可選 inclusion_proof 成員——封印時缺席,由錨定後的重新匯出為冷封存而填入——遵循與 W3 的 drand_chain_version 相同的「可選、預設省略、附加性」模式。
16.13 保留
LogDO 開放尾段、R2 證明供應基底、錨定斷路器計數器,以及打包器後援佇列的保留窗口,規範於 docs/DATA-RETENTION.md。原則:日誌的熱態 per-entry 儲存(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 文字中; 綁定啟動限制 在結尾處重申。可依據它們進行實施。
- 預設路徑(
kind=0x02)葉節點誠實性——已解決。 依規格保留兩種葉節點:kind=0x02不承諾body_hash,也不承諾drand_round。位元組盲路徑不得加入*_body_hash欄位,因為它會成為整合端最容易誤讀的「已驗證」訊號,而 §11 已能從套件提供這項便利。不得要求經日誌認證的 qub 一律由伺服器封印;那會迫使明文流經 Worker,破壞加密抹除的防護。任何自我描述的捷徑都應置於.qub套件/證明信封中,作為由驗證器重新計算的欄位,絕不能成為葉節點欄位。擁有者確認的聲明上限見 §16.11。 - 含糊其詞/遺漏責任 — 設計已解決,配置尚未完成。 設計要求將密封收據鑰匙固定住
LogProfile並由…交叉簽署anchor_owner,外加監控方法、前鏈走訪,以及雙自發行的頭部。編譯好的配置檔和部署掛鉤仍然是 §16.6 所述的佔位符/可選項,因此在這些門檻關閉之前,更強的可檢測 + 有收據聲明尚未生效。它絕不可被宣傳為獨立見證。真正的第三方見證將延後至 §15 的治理提升。 - 固定的錨點所有者信任根 + 旋轉 — 已解決。 採用
LogProfile插針 (§16.6);驗證者檢查anchor_tx.owner == anchor_owner並在本地驗證 tx 資料 → tx_id 的綁定。輪換治理是 §15 擴展到建築 (§15.3 觸發已新增),不是重用;計劃中的輪換交叉簽署,妥協驅動的輪換退回 §15 碰撞,並透過分叉檢查限制損害。 - 私人 qub 葉片失明 — 已解決。 繼續對私人qub進行盲操作(
ref = SHA3-256(qub_id ‖ log_blind_secret)), 生qub_id對於公共 qubs(已在 §16.2.1 中),chash作為獨立領帶。log_blind_secret是一個相關性/賽比爾級秘密,只能向前旋轉 (§16.2.1)。 received_at— 決議通過。 將其保存在葉片中,承諾但明確不作為證據;從未以任何形式作為證明或爭議的佐證出現。任何監控理智檢查都與 Arweave 區塊時間進行比較T,而不是操作員控制的anchored_at(§16.6)。- 分層可證明時序 — 設計解析度,而非當前佈線。 經審核的設計將錨定塊時間分配給批次層,並將精確小時證明分配給付費 T3,而前者沒有數值 SLA。目前的路由尚未設置這種商業區分:它們為每個被接受的發佈安排單獨交易,且如 §16.1/§16.10 所述,日誌覆蓋仍然是有條件的。產品說明必須描述實際實現,而不是這個未來的層級劃分。
- Workers 上的累積樹——已解決。 採用單一累積 RFC 9162 樹,以及快取前緣的單一寫入者 LogDO(相較約每秒 1,000 次寫入的 DO 上限仍有充裕空間;接近上限時才考慮以分片根的 Merkle 樹分片)。依
(level, index)座標索引的 R2 節點儲存及清空 DO 後的冷葉節點測試向量均已實作(§16.9)。< 300 ms仍是設計/營運目標,不是協定保證(§16.10)。 - ANS-104 簽名方案 + 深度雜湊 — 已解決。 RSA-PSS(簽名類型 1,重用專用錨點錢包 JWK);Ed25519 延遲至 §15 PQ 路徑。手工製作的 SHA-384 深度雜湊受限於雙向交叉實現裝置、一個僅限靜態的參考捆綁互操作檢查,共享的-
crypto.subtle來回旅行,以及後捆綁 Arweave 接受監控 (§16.8)。
綁定啟動限制 (落實執行 + 產品/法律審查):
- 聲明上限(Q1/Q6)。 任何介面都不得宣稱日誌證明了斷言型葉節點的內容或解鎖輪次;對成功錨定的
kind=0x02葉節點,允許的聲明僅為具防篡改排序,並提供無須信任的承諾時間上限。回應若不含收據元組,就不具任何日誌聲明。任何時間戳文案都不得承諾具體延遲數值。 - 見證誠實(第2題)。 市場上的含糊其詞被視為可檢測且有收據,而非獨立見證。
- 收據 + 錨鍵 (Q2/Q8)。 在不可否認/錨定驗證的聲明發出之前,提供並編譯收據公鑰,與已提供的錨點擁有者交叉簽署,並將錨點錢包保留為其自身的 JWK,與上傳錢包不同。
- 深度雜湊閘門(Q8)。 雙向測試夾具與互通性檢查通過前,不得發布任何錨點或 T3 交易;若驗收失敗,監控器必須發出警報。
- 儲存前提條件(Q7)。 以坐標為鍵的節點儲存 + 擦除的 DO 冷葉向量是「回收永不使證明失效」保證的前提條件。
17. 可攜式驗證套件(.qub)
狀態。 本節已在 W7/UP-C2 實作:
qub_core::export會產生及解析套件,而tools/qub-verify是可離線驗證套件的公開、自包含命令列介面。§11 與 §16.9 已將「.qub套件」視為獨立驗證器的輸入單位;本節規定其位元組格式與驗證步驟。此格式純粹附加既有能力——它封裝現有的 §11 輸入,不會變更鏈上線路格式。
17.1 目的
§11 規定任何第三方都能在不需 qub 配合的情況下驗證 qub 的密碼學產物。.qub 套件讓這項驗證可攜且可離線執行:它把封存 CBOR 與用來解鎖的 drand 輪次簽章裝入單一、自包含的產物,使接收者完全不需網路呼叫(無須擷取儲存內容、即時查詢 drand 或呼叫 qub API),即可驗證內容完整性、輪次綁定及作者簽章。套件本身無法證明密文建立於何時;經獨立驗證的儲存交易或錨定日誌證明,才會提供另一項存在時間聲明(§11、§17.5)。
17.2 捆綁格式
A QubBundle 是手寫的符合 §3.1 規範的 CBOR(固定長度,無標籤,無浮點數,最短形式整數,NFC 文字,當缺失時可選欄位省略,鍵按編碼位元組長度升序排序,若長度相同則按位元組順序排序)。這三個 15 字元的鍵順序 d < i < s. 生的 .qub 檔案就是這些位元組;對於 URL 或複製粘貼傳輸,相同的位元組使用 base64url(無填充)。
| 鍵 | 編碼長度 | 類型 | 必要性 | 意義 |
|---|---|---|---|---|
version |
八 | u8 |
必需的 | 套件格式版本(0x01)。 |
sealed_at |
十 | i64 |
可選的 | 建立者指定的封印時間(Unix 秒);自我描述,非證據性。 |
drand_round |
十二 | u64 |
必需的 | 輪次的 qub 被鎖定。嵌入式密封 qub 的投影。 |
arweave_tx_id |
十四 | tstr |
必需的 | 封存的位元組所儲存的交易編號(來源指標)。 |
drand_chain_id |
十五 | tstr |
必填 | drand 鏈(十六進位)。嵌入封印qub的投影。 |
drand_signature |
十六 | bstr |
必需的 | 用於的 drand 信標簽章 drand_round — 解鎖密文的值。 |
inclusion_proof |
十六 | bstr |
可選的 | 在獲得錨定證明之後(§17.5),§16 透明度日誌 Merkle 包含證明。 |
sealed_qub_cbor |
十六 | bstr |
必填 | 內部 SealedQubCbor 位元組(§13 解包後),即 §11 驗證輸入。 |
drand_round 與 drand_chain_id 是 sealed_qub_cbor 的便利投影,讓工具無須解析內層 CBOR 即可讀取。建構時會從內容推導這兩個欄位,解碼時則會對照已解析的封存 qub 再次檢查;頂層欄位若與負載不一致,套件就會被拒絕。編碼器規則與其餘線路格式一致:拒絕空的 drand_signature 或 arweave_tx_id,並限制每個可變長度欄位。
17.3 嵌入式 drand 簽名所證明的內容
該捆綁包帶有 drand 簽名,而不需要驗證者去取得它。時間鎖解密(基於 drand 鏈的 tlock,第 8 節)只能在 真誠 綁定輪次的信標簽名——當該輪次結束時,鏈僅發布一次的值,並且在鏈的公鑰下是一個有效的 BLS 簽名。偽造或錯誤的簽名將無法通過 BLS 驗證或 IBE/AEAD 解密。因此,可以解密的包證明了: 密文綁定於第 R 輪,第 R 輪已經過去. 驗證者固定鏈條(DrandTimelockProvider::quicknet())並應用§11回合綁定檢查,因此一個捆綁包不能聲稱其密文未綁定到某個回合。
這是解鎖條件證明,不是建立時間戳。輪次 R 經過後,任何人都能為 R 建立新密文,並把已公開的簽章裝入套件。因此,不得把套件本身描述為密文或內容在 R、unlock_at 或任何事件之前已存在的證明。
17.4 離線驗證操作指南
qub-verify <file.qub> 完全從捆綁包中運行標準 §11 程序,驅動 qub_core::unlock::unlock 帶有固定的 DrandTimelockProvider:
1. Parse the .qub bytes → QubBundle (canonical-CBOR guard; bound every field;
re-check drand_round / drand_chain_id against the embedded sealed qub).
2. BLS-verify bundle.drand_signature for the pinned chain and round, then
tlock_decrypt(sealed.tlock_ciphertext, bundle.drand_signature) → QubEnvelope.
3. Verify SHA3-256(body) == body_hash (§11 step 8).
4. Verify QubEnvelope.qub_id == SealedQub.qub_id (§11 step 9).
5. Verify QubEnvelope.unlock_at == SealedQub.unlock_at (§11 step 10).
6. Verify ciphertext round == unlock_round(unlock_at) and the chain binding.
7. If sig_alg != 0x00: verify author_signature (and any cosigner; §9.4).
8. Report integrity, round-elapsed/round-binding, authorship, and cosigner
verdicts separately, plus the recovered body. Do not report a commitment
timestamp unless step 9 succeeds.
9. Optional existence-time leg: verify an included §16 proof through its pinned
anchor, or independently verify the referenced storage transaction. Report
its block time as an upper bound on ciphertext existence.
命令列介面的結束碼為 0(已驗證)、1(驗證失敗——仍鎖定、本文雜湊不符、輪次/鏈綁定損壞或簽章驗證失敗),或 2(用法錯誤/套件損壞)。--json 報告會為自動化流程提供相同判定。套件是自包含的,因此第三方只需公開的驗證函式庫 qub-core 與命令列工具 qub-verify;兩者重用協定既有的驗證路徑,不另行建立客製密碼學。
17.5 與透明度日誌的關係
inclusion_proof 是一個可選欄位,用於§16 Merkle 包含證明。僅綑綁驗證(§17.4)對於…是完整的 完整性、輪次裝訂/輪次經過,以及可選的作者身分,但故意沒有獨立標記時間的存在聲明。一個已填充、完全錨點驗證的 inclusion_proof 從 §16.11 添加特定葉子類型的承諾和上限時間,而不更改捆綁格式版本。缺少的證明僅表示「未包含證明」——並非「無效」,也不一定表示「未錨定」.
在參考實作中,插槽現在是 打字的: qub_core::export::QubBundle::inclusion_proof_typed() 回傳一個 Option<InclusionProof> 攜帶完整的 §16.9 結構(葉節點、審計路徑、錨定根,以及 AnchorRef) 通過相同的不可見 CBOR 欄位 — 無套件格式版本升級。獨立的 qub-verify CLI 透過它的消耗 --anchor 腿,並且——直到錨錢包被配置完畢(§16,狀態)——將已填充但占位的所有者證明報告為僅包含,而非完全錨定驗證。