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. 表記と規約
| 表記 | 意味 |
|---|---|
u8, u64, i64 |
指定ビット幅の符号なし / 符号付き整数 |
[u8; N] |
N バイトの固定長バイト配列 |
Vec<u8> |
可変長バイト配列 |
Option<T> |
型 T の値、または不在 |
String |
NFC 正規化された UTF-8 テキスト文字列 |
| ` | |
SHA3-256(x) |
バイト列 x の NIST SHA3-256 ハッシュ(FIPS 202) |
ceil(x) |
天井関数: x 以上の最小整数 |
| CBOR | Concise Binary Object Representation(RFC 8949) |
| big-endian | 最上位バイトを先頭に配置 |
プリイメージ構成におけるすべての整数は、特に指定がない限り、big-endian の固定幅バイト配列として符号化されます(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 = 0x01、content_type = 0x01、sig_alg = 0x00、すべての Option フィールドが不在。
その他の 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)を用いてシリアライズされます。永久ストレージにアップロードされます。これがオンチェーン成果物です。
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 プロファイル
すべての SealedQub および QubEnvelope のシリアライゼーションは本プロファイルに準拠しなければなりません(MUST)。同一の論理構造を与えられた二つの実装は、同一のバイト列を生成しなければなりません(MUST)。
3.1 符号化規則
| 規則 | 仕様 |
|---|---|
| 標準 | RFC 8949 §4.2.1(Core Deterministic Encoding Requirements) |
| マップキー順序 | まず 符号化バイト長 でソート(短いものを先)、次に 辞書式(同一長の符号化はバイト単位) |
| 整数符号化 | 最短形式: 0–23 は初期バイトに、24–255 は 2 バイトに、256–65535 は 3 バイトに、以下同様 |
| 長さ符号化 | 確定長のみ。 不定長の配列、マップ、バイト文字列、テキスト文字列を使用してはなりません(追加情報 = 31 は禁止)。 |
| タグ | CBOR タグなし(メジャー型 6 は禁止)。 |
| 浮動小数点 | 浮動小数点なし(メジャー型 7 値 0xF9–0xFB は禁止)。 |
| テキスト文字列 | UTF-8 符号化、NFC 正規化済み(Unicode 正規化形式 C)。 |
| バイト文字列 | 生バイト。CBOR レイヤでは base64 符号化しません。 |
| 重複キー | エラーで拒否。 パーサは重複するマップキーを黙って受け入れてはなりません(MUST NOT)。 |
| 不明なキー | エラーで拒否。 パーサは型の正規キー集合の外にあるマップキーを許容してはなりません(MUST NOT) — 2 つの異なる正規バイト列が同じ値にデコードされることは決してあってはならず(encode(decode(x)) == x)、署名付きペイロードでは余分なキーは両方の署名がコミットする隠れたコンテンツになります。スキーマの進化は version を通じて行い、余分なキーによっては決して行いません。 |
| 単純値 | true(0xF5)、false(0xF4)、null(0xF6)のみ許可されます。 |
| オプションフィールド | 不在のオプションフィールドは CBOR マップから完全に 省略 されます(null として符号化しません)。存在するオプションフィールドはソート済みキー順に含めます。 |
3.2 検証済み正規キー順序
これらのキー順序は規範です。実装はキーを正確にこの順序で出力しなければなりません(MUST)。デバッグアサーションは非リリースビルドで順序を検証することが望ましい(SHOULD)。
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(1 バイト) |
|
| コンテンツタイプ(u8、値 1) | 0x01(1 バイト) |
|
| sig_alg(u8、値 0) | 0x00(1 バイト) |
|
| 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 です。整列のため 1 バイトの 0x00 パディングを付加して 10 バイトにします。実装は正確にこの 10 バイトを使用しなければなりません(MUST): [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]。
outcome_at の符号化: V1.1 では、オプションの outcome_at フィールドをバインディングに折り込むため、プリイメージを 92 バイトから 100 バイトに拡張しました。outcome_at が不在の場合は 8 つのゼロバイトとして符号化されます。プロトコル検証器はあらゆる箇所で outcome_at <= 0 を拒否するため、このセンチネルが正当な値と衝突することはありません。§3.2(ワイヤ形式)と、このフィールドの動機となる verdict メカニクスについてはリポジトリ内の tasks/verdict-uplift-plan.md を参照してください。
drand_round の符号化: V1.2 では、drand_round(対象の drand ラウンド、§4.3)をバインディングに折り込むため、プリイメージを 100 バイトから 108 バイトに拡張し、ドメインセパレータを QUB_ID_V2 に引き上げました。これにより timelock のラウンドが qub のアイデンティティにバインドされます。ゲートウェイは、表示される unlock_at が示すラウンドとは異なる(例えばすでに過去の)ラウンドに暗号文を再バインドすることはできません。解錠手順(§8)はさらに、tlock 暗号文スタンザに焼き込まれたラウンドが unlock_round(unlock_at) と一致することを検証します。したがって、表示される解錠時刻は復号をゲートするラウンドであることが証明可能です。
性質:
- QubEnvelope のいずれかのフィールド(本文、タイムスタンプ、コンテンツタイプ、バージョン)を変更すると、異なる qub_id が生成されます。
- qub_id は暗号化前に計算されます。QubEnvelope と SealedQub は同じ qub_id を持ちます。ビューアーは復号後に両者が一致することを検証します。
- qub_id は
sender_label、author_signature、author_pubkeyに依存しません。これは、同一の内容が同一の時刻に封印された場合、誰が署名したかに関わらず同じ qub_id が生成されることを意味します。 - SealedQub の
titleを変更すると(他のすべてを固定しても)title_hash経由でqub_idが変わります。したがってゲートウェイは、qub のアイデンティティを無効化することなくカウントダウンに表示される平文タイトルを差し替えることはできません。 - SealedQub の
outcome_atを変更すると(他のすべてを固定しても)プリイメージ経由でqub_idが変わります。ゲートウェイは、qub のアイデンティティを無効化することなく、カウントダウンに表示される公開前の verdict-on 日付を差し替えることはできません。 drand_roundを変更すると(他のすべてを固定しても)プリイメージ経由でqub_idが変わります。ゲートウェイは、qub のアイデンティティを無効化することなく timelock 暗号文を別のラウンドに再バインドすることはできません。§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 = 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 は封印時点で未来でなければなりません(MUST)。unlock_at は created_at から 10 年を超えてはなりません(MUST NOT)(長期の drand 依存リスクを制限するため。UI は 2 年を超える公開日時について警告することが望ましい(SHOULD))。
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 マップヘッダで始まることを検証することが望ましい(SHOULD)。完全な構造検証は二重解析を避けるため、構築時ではなく解析時に行います。
6. コンテンツタイプレジストリ
| 値 | 型 | 最大本文サイズ | 備考 |
|---|---|---|---|
0x00 |
予約済み(無効) | — | 使用してはなりません(MUST NOT) |
0x01 |
プレーンテキスト(UTF-8、制限付き Markdown) | 50 KB 有料 / 10 KB 無料 | 表示規則は §10 を参照。無料 / 有料の分割はアップロードサービスによって強制されます。プロトコル層の絶対上限は 50 KB です。 |
0x02 |
予約済み(将来) | — | 将来のコンテンツタイプ用に割り当て済み。v1 では無効です。ビューアーは以下の規則に従って拒否しなければなりません(MUST)。 |
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 を参照。 |
ビューアーは、不明なコンテンツタイプを明確なユーザー可視のエラーで拒否しなければなりません(MUST)。ビューアーは不明な型をテキストとして表示しようとしてはなりません(MUST NOT)。
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)> }
3 つのマップすべての正規 CBOR キー順序は §3.2 にあります。約定 CBOR のシリアライズ済み合計は 100 KB を超えてはなりません(MUST NOT)(§6 と一致)。
スキーマ識別子。 structured/v1 約定の terms の最初の行は { key: "pact_schema", value: "structured/v1" } でなければなりません(MUST)。このマーカーを持たない行は「カスタム」約定であり、構造化検証やスキーマ対応の表示は受けません。
固定された承認スロット。 structured/v1 約定は次のキーの下に正確に 4 つの承認行を持ちます。
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
それぞれの value は (role, kind) ペアによって選ばれる 8 つの固定英文字列のいずれかであり、ここで role ∈ { seller, buyer, provider, client }、kind ∈ { standard, capacity } です。これらの文字列自体が 規範的プロトコルデータ であり、両当事者の ML-DSA-65 署名は body_hash を介して正確なバイト列にコミットします。これらはローカライズされません。署名対象の本文は言語中立です。文言を変更するには新しいスキーマバージョン(structured/v2)が必要です。
8 つの文字列、それらのルックアップ(acknowledgement_for(role, kind))、および各々の根拠はリファレンス実装で固定されています。準拠する実装はバイト単位で同一の承認値を出力しなければならず(MUST)、4 つの役割組み合わせをすべてカバーするゴールデンフィクスチャの SHA3-256 body-hash テストがあらゆる差異を捕捉します。
ビューアーの表示順序。 承認文字列には「described above」のようなフレーズが含まれており、これは説明 / スコープ行が承認の前に表示されることを前提としています。ビューアーは terms 配列を CBOR の順序で表示しなければなりません(MUST)。並び替えは本文の意味を壊します。
カウンターパーティの連絡先。 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 を超えてはなりません(MUST NOT)(上記レジストリ行と一致)。
結果列挙。 ワイヤバイトはインテント中立です。4 つのバケット Right / Partial / Wrong / Unfalsifiable は、判定を持つすべてのインテントの結果空間を網羅します。インテント別ラベル(Right の場合「Called it」/「Kept it」/「Shipped」/「Confirmed」など)はビューアー側の描画上の関心事であり、親 qub のインテントに対して解決されます。ワイヤは言語およびインテントに中立を保ちます。1..=4 の範囲外の値は復号時に拒否しなければなりません(MUST)。
親リンク。 判定 qub は本文中に親への参照を持ちません。親 qub の Arweave トランザクション ID はアップロード時に Parent-Tx-Id ストレージタグとして発行されます(§7 ストレージタグ層)。これにより本文は自己完結した自己採点の署名済み宣言として保たれ、監査チェーン(「何について正しかったのか」)は Arweave タグのルックアップを介して確立されます。
根拠 URL の安全性(規範的)。 evidence_url が存在する場合、検証側(構成側、ワイヤ側、Worker エッジ)は以下を強制しなければなりません(MUST):
- HTTPS のみ。 文字列はバイト列
https://で始まらなければなりません(MUST)。他のスキーム —http、ftp、javascript、data、fileなど — は拒否します。 - 長さ上限。 ≤ 2,048 バイト(ブラウザ URL の実用上限)。
- NFC + 敵対的コードポイントチェック。
titleおよびreflectionと同じ規則 — 双方向制御 / ゼロ幅 / タグブロック / BOM / C0 / C1 のコードポイントは拒否します。定義は Rust のcrate::handle::contains_hostile_text_codepointおよび TS のworkers/api/src/utils/unicode.ts::isHostileCodepointと一致します(同期を保つこと)。 - 空白なし、ASCII 制御文字なし。 URL 内のどこかに空白 / DEL /
0x20未満のバイトがある場合は拒否します — 双方向規則が捕捉しない\n/\t注入ベクターを閉じます。 - 空でないホストセグメント。
https://と最初の/、?、または#の間のすべては空であってはなりません(MUST)。
サーバー側フェッチなし。 Worker は URL をプロキシ、フェッチ、プレビューしてはなりません(MUST NOT)。プロトコルは文字列を保存するだけで、描画はビューアー側で rel="nofollow noopener noreferrer" target="_blank" 付きでリンクテキストとともにホスト名を可視表示して行います。
振り返り。 任意の作成者記述の振り返りテキスト(「何が変わったか、何を学んだか」)。title と同じ NFC + 敵対的コードポイントの検証を行います。空 / 空白のみの入力は構成時に不在へ折りたたまれます。
スキーマバージョン。 v1 は verdict_version = 0x01 のみをサポートします。将来のスキーマ改訂はこのバイトを上げ、§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 は規範的に必須です。リファレンスサービスはさらに、作成者がそれらを表示するよう選択した場合に 3 つの任意タグを付加します。Intent(許可リストで検証された作成意図 — 例: quote、reply、commitment)、Author(64 文字の小文字 16 進数による作成者の §9.3 公開鍵フィンガープリント)、および Parent-Tx-Id(返信チェーン用の親 qub のストレージトランザクション ID、43 文字の base64url)。
Author タグは qub ごとにオプトイン です。リファレンス作成者アプリは、ユーザーが封印時に明示的に公開帰属を有効にした場合にのみこれを付加します。トグルがオフのとき(既定)、Author タグは書き込まれず、qub はチェーン上で帰属表示されません。永久ストレージ上のいかなる情報も、そのアップロードを作成者のハンドル、メール、その他の qub にリンクしません。トグルがオンのとき、Author フィンガープリントは §9.5 のアテステーションチェーンを介して作成者が選んだ @handle に解決されます。返信チェーンの関係性および Intent は識別性を持ちません。外側のラッパー(§13)は内側の 本文 を暗号文相関から保護し、ハーベスターが drand ラウンドの公開後に qub 形状のアップロードを認識し一括復号することを防ぎます。
リファレンスサービスは意図的に App-Name、App-Version、Type タグを付加しません。そのような単一値フィルタは GraphQL クエリに qub コーパス全体を返してしまい、ラッパーの本文限定の秘匿性スコープと矛盾するためです。
準拠する検証者は、§11 の第三者検証のためにいかなるストレージタグにも依存してはなりません(MUST NOT)。本文ハッシュ / 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 では、qub の永久的な寿命の間にセキュリティが劣化する可能性のある古典スキームではなく、耐量子の ML-DSA-65 スキーム(FIPS 204)を使用します。
9.2 アルゴリズムレジストリ
sig_alg |
スキーム | 鍵サイズ | 署名サイズ |
|---|---|---|---|
0x00 |
署名なし(未署名) | — | — |
0x01 |
ML-DSA-65(FIPS 204) | 1,952 バイト | 3,309 バイト |
ビューアーは不明な sig_alg 値を拒否しなければなりません(MUST)。
9.3 署名プリイメージの構成
プリイメージには 2 つのバージョンが存在してきました。すべての署名は V2 を使用しなければならず(MUST)、検証者は V2 のみを受け入れなければなりません(MUST)。 レガシーの 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 プリイメージのみを受け入れなければなりません(MUST)。この定義は、歴史的参照のため、および下記のドメインセパレータを説明するために、ここに保持されています。V1 に対してのみ検証される署名は、検証失敗として扱わなければなりません(MUST)。
ドメインセパレータ: "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])。パディングなし。セパレータが異なることで 2 つの構成がドメイン分離されるため、一方のプリイメージに対する署名がもう一方として検証されることは決してありません。
org_id_present バイト: unlock_at に続くバイトは 0x00 でなければなりません(MUST)。リファレンス実装はこれを crates/qub-core/src/signing.rs の定数 ORG_ID_PRESENT_INDIVIDUAL = 0x00 として公開しています。検証用に sig_input を再構築するビューアーは同じバイトを出力しなければなりません(MUST)。
署名スコープ — 何がカバーされ何がカバーされないか。 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 プリイメージ経由(V1.2) |
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 に対してのみ検証される署名)は、V1 へダウングレードして受け入れられるのではなく、そのまま拒否されます。 - エンベロープ内の
author_pubkeyは真のアイデンティティアンカーであり続けます — ビューアーはsender_labelを信頼するのではなく、author_pubkeyから(§9.5 アテステーション層を介して)表示アイデンティティを導出しなければなりません(MUST)。
sender_label または reply_to をエンドユーザーに表示する実装は、ラベルではなく認証されたアイデンティティ(公開鍵フィンガープリント、アテステーション)を主要なアイデンティティシグナルとして表示しなければなりません(MUST)。
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)がすべてパスした後に実行することが望ましい(SHOULD)。
9.5 アイデンティティアテステーション
アイデンティティアテステーション — author_pubkey を qub ハンドル、メールアドレス、ソーシャルハンドル、パスキー資格情報などの人間が認識可能なアイデンティティクレームにマッピングするもの — は ビューアー側のプログレッシブエンハンスメント であり、署名検証には 必須ではありません。アテステーションを表示アイデンティティに解決するビューアーは、次の優先順位を適用しなければなりません(MUST):
handle > email > social > fingerprint
フィンガープリントフォールバックは SHA3-256(author_pubkey) の小文字 16 進数であり、署名付きの qub であれば常に利用可能です。ビューアーは表示のためにこれを省略してもかまいません(MAY) — リファレンスビューアーは qub: に続けて最初と最後の 4 バイトを表示します(qub:<8 hex>…<8 hex>)。
準拠する検証者は §9.4 のすべてのチェックを、qub API に接続せず、永久ストレージと drand を超えるネットワークを使用せず、サーバー側のルックアップなしで完了できます。アテステーション解決は、署名検証が成功した後にのみ実行される、別個のベストエフォートステップです。
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 は保存サイズをおおよそ 3 倍にします。絶対コストは無視できるほど小さいです。
9.7 共同署名者検証(約定の双方向合意)
双方向合意(content_type = 0x03)では、両当事者が同じ条件に同意したことを証明するために 2 つ目の署名層が用いられます。
エンベロープフィールド:
cosigner_pubkey: 共同署名者(Party B)の ML-DSA-65 公開鍵。cosigner_signature: 作成者と同じsig_inputに対する署名(§9.3)。
両方のフィールドは共に存在するか、共に不在でなければなりません(MUST)。正確に 1 つのみが存在する場合、ビューアーは完全性エラーを報告しなければなりません(MUST)。
検証手順:
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 とその連絡先の正規化メールハッシュの両方に一致する短期間のメール検証マーカーが存在しない限り、共同署名リクエストを拒否しなければなりません(MUST)。マーカーは、マジックリンクトークンが staging_id を含み、検証されたアドレスが SHA-256(normalise_email(party_b.contact)) に一致する場合に /api/v1/auth/verify によって書き込まれます。ここで 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 表示とサニタイゼーション
このセクションはセキュリティ上重要です。ビューアーはテキスト qub(content_type = 0x01)を制限付きの Markdown サブセットを用いて表示します。
10.1 許可された要素
- 見出し:
#から####まで(#####および######はなし) - 強調: 太字(
**)、斜体(*)、取り消し線(~~) - リスト: 順序付き(
1.)および順序なし(-、*) - 引用ブロック(
>) - コード: インラインスパン(```)およびフェンスブロック(`````)
- 水平線(
---) - 改行(末尾の 2 つの空白または空行)
- 段落
10.2 禁止された要素
| 要素 | 処理 |
|---|---|
生 HTML(<div>、<script> など) |
完全に除去。HTML は通過しません。 |
画像() |
除去。画像構文は出力から削除されます。 |
リンク([text](url)) |
URL は可視のプレーンテキストとして表示。自動リンク化なし。明示的なユーザー操作なしにはクリック不可。 |
| 危険な URL スキーム | javascript:、data:、vbscript:、file: — 除去。 |
| iframe、エンベッド、オブジェクト | 除去。 |
| HTML エンティティ | 安全な場合にのみ表示文字へデコード。 |
10.3 実装
実装は、ブロックリストではなく 厳密な許可リストパーサ を使用しなければなりません(MUST)。推奨アプローチ:
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 の協力なしに公開 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 プロトコルバージョン
SealedQub と QubEnvelope の両方にある version フィールド(u8)はメジャープロトコルバージョンを識別します。
- ビューアーは不明なメジャーバージョンを明確なエラーで拒否しなければなりません(MUST)。
- 既知のメジャーバージョン内では、デコーダは不明なマップキーを拒否しなければなりません(MUST)(§3.1)。スキーマの進化は新しい
versionの導入によって行われるのであって、既存のデコーダが読み飛ばすことになるキーの追加によってではありません。(本仕様の以前の改訂では、不明なオプションフィールドの許容を認めていました。その条項は撤回されました —encode(decode(x))を非単射にし、約定ペイロードに対する隠れた署名済みコンテンツの攻撃経路を開いていたためです。) - コンテンツタイプ(
content_type)と署名スキーム(sig_alg)はバージョンゲート化されています。新しい値は、新しいプロトコルバージョンまたは明示的なレジストリ更新と共にのみ導入できます。
12.2 バージョン履歴
| バージョン | 値 | 説明 |
|---|---|---|
| v1 | 0x01 |
公開テキスト qub(content_type 0x01)、約定の双方向合意(0x03、structured/v1 スキーマ、ML-DSA-65 作成者 + 共同署名者)、tlock、SHA3-256 |
12.3 前方互換性
不明な CBOR マップキー(§3.2 の正規順序にないキー)を持つ QubEnvelope に遭遇した v1 ビューアーは、デコードエラーでそれを拒否しなければなりません(MUST)(§3.1)。前方互換性は、キーの許容ではなく version フィールドが担います。将来の追加は — マイナーなメタデータであっても — 新しい version 値の下で出荷され、v1 ビューアーはそれを、署名がコミットするコンテンツを黙って落とすのではなく、明確な「より新しいプロトコル」エラーで拒否します。
sig_alg = 0x01(ML-DSA-65)に遭遇したが ML-DSA-65 検証サポートを欠く v1 ビューアーは、qub 全体を拒否するのではなく、「署名は存在するが検証できません」という通知と共に qub の内容を表示することが望ましい(SHOULD)。リファレンス実装は今日、0x00 と 0x01 以外のすべての sig_alg 値を拒否します。v1 レジストリには他の有効なアルゴリズムが含まれていないためです。厳格な拒否とソフトフェイルは、第三のアルゴリズムが登録されるまで観測上は同一です。上記のソフトフェイル動作は §9.2 が新エントリを承認した時点で重要となり、リファレンスビューアーはその時点でソフトフェイルに更新されます。
12.4 外側ラッパーバージョン
§13 に記載される OuterWrapper は、SealedQub.version および QubEnvelope.version から 独立した 独自の version バイトを持ちます。2 つのバージョン空間は別個に進化します。将来の耐量子対称置換は内側のプロトコルバージョンに触れずにラッパーバイトを引き上げ、将来のプロトコル層の追加(例: 新しいエンベロープフィールド)はラッパーバイトに触れずに内側のバージョンを引き上げます。
OUTER_WRAPPER_VERSION_* |
値 | アルゴリズム | ステータス |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
12 バイトのナンス、16 バイトの認証タグ、qub_id に結合された AAD を持つ AES-256-GCM |
v1 既定 |
| — | 0x02–0xFF |
予約済み | 将来 |
ビューアーは不明なラッパーバージョンを明確なエラーで拒否しなければなりません(MUST)。プロトコルは、具体的な移行ドライバ(例: 異なる AEAD を支持する NIST のガイダンス)が現れるまでラッパーバージョン空間を意図的に狭く保ちます。0x02 スロットはそのアルゴリズムを導入する同じリビジョンで割り当てられます。
13. 外側暗号ラッパー
13.1 根拠
プロトコル層(QubEnvelope → tlock → SealedQub)は、封印された qub を 時間ロック します。本文は unlock_at および drand ラウンド署名が公開されるまで読み取り不可能です。しかし解錠後、ラウンド署名は公開され、SealedQub の正規 CBOR 形状は認識可能であるため、永久ストレージのトランザクションをインデックス化したハーベスターは qub コーパス全体を一括復号できます。
外側暗号ラッパーは、正規 SealedQubCbor と永久ストレージにアップロードされるバイトの間に追加の対称 AEAD 層を挿入することでそのチャネルを閉じます。256 ビットの鍵 K は配信 URL の URL フラグメントとユーザー端末にのみ存在します。ブラウザは URL フラグメントをサーバーに送信しないため、qub.social、すべてのストレージゲートウェイ、およびそれらの前にあるすべての CDN は、観測上 K に対して盲目です。したがって永久ストレージ上のすべての qub は、作成者が共有することを選んだ URL なしには平文を回復不能な不透明な暗号文です。
正味の効果:
- 既定での列挙耐性。 永久ストレージ上のラップされたバイトは任意の暗号文とバイト識別不可能です。「qub 形状のアップロードを GraphQL クエリし、公開された drand 署名で一括復号する」というハーベスター戦略は平文に到達しません。
- 暗号シュレッディング・プライバシー姿勢。 qub.social は自らのコーパスを文字通り復号できません。召喚状は暗号文に届くだけで平文には届きません。
- 2 層の機密性階層。 既定 = リンク制御アクセス(本セクション)。受信者暗号化された非公開 qub(予約済みのフェーズ 2 機能であり、未仕様)が第 2 層として上に重なります。
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
}
フィールドの不変条件。
versionは v1.0 ラッパーバイトについて0x01でなければなりません(MUST)。qub_idは、アンラップ後に回復される SealedQub のqub_idフィールドと等しくなければなりません(MUST)。アンラップステップはこれを直接強制しません(AEAD AAD バインディングがバイトレベルの改ざんを不可能にします)が、解錠層が関係を推移的にチェックします。作成者が内側のqub_idがラッパーのqub_idと一致しないSealedQubCborをラップする場合、§8 ステップ 11 が失敗します。nonceは 96 ビット(12 バイト)でなければならず(MUST)、すべてのラップ操作のために CSPRNG によって新たに生成されます。同じ鍵の下でナンスを再利用すると、平文を回復する AEAD ナンス再利用攻撃が許されます。生成者は(key、nonce)ペアをワンショットとして扱わなければなりません(MUST)。ciphertextは AES-256-GCM 出力です。暗号文バイトと 16 バイトの認証タグを連結したものです。ciphertext.len() == SealedQubCbor.len() + 16が正確に成り立ちます。
CBOR 符号化。 §3 に準拠した正規 CBOR、同じキー順序規則(符号化バイト長の昇順、次に辞書式)を使用します。4 つのキーは次のとおりです。
| キー | 符号化バイト | 順序 |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
したがって OuterWrapper CBOR の最初のバイトは 4 エントリのマップの確定長マップヘッダ(0xA4)です。
13.4 qub_id への AAD バインディング
ラッパーは qub_id を AEAD 追加認証データとしてバインドします。これは 3 種類の攻撃に対する負担を担う構造的防御です。
| 攻撃 | 防御 |
|---|---|
暗号文をラッパー内の異なる 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 失敗を単一のエラー形状に集約しなければなりません(MUST)。
13.6 鍵素材と配布
ラッピング鍵 K は qub ごとに CSPRNG によって生成される 256 ビットの一様ランダム値です。リファレンス実装は次のソースから取得します。
- WASM 作成者:
getrandom(wasm_jsバックエンド配下の WebCrypto)。 - サーバー側の seal API 呼び出し元: ローカルの CSPRNG。呼び出し元は
Kをwrapper_key_b64urlとして供給し、自ら保持します。Worker はラッパーのためにKをメモリ内で使用しますが、これを永続化してはなりません(MUST NOT)。これにより、ワンショットのサーバー生成シークレットに依存する代わりに、呼び出し元が保持する権限を用いて、冪等な再試行が秘匿化された応答を回復できます。
配布: K は URL セーフ base64(RFC 4648 §5、パディングなし)として符号化されなければならず(MUST)、配信 URL のフラグメント成分として付加されます。
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
フラグメントは準拠ブラウザによっていかなるサーバーにも送信されません。完全な配信 URL(フラグメントを含む)をユーザー端末を超えて永続化する回復チャネル(サーバー側の履歴インデックス、オプトイン式のメール自動送信)は、既定の暗号シュレッディング姿勢に対する明示的なトレードオフであり、明示的なユーザー同意でゲート化されなければなりません(MUST)。
フラグメント喪失。 ユーザーが URL フラグメントを失い、回復チャネルを持たない場合、qub は読み取り不可能です。これは設計の負担を担うトレードオフであり、封印時にユーザーに開示されなければなりません(MUST)。MVP は封印時開示を明示的な「この URL を保存」コピーと、オプトインするユーザー向けの検証済みメール回復チャネルで強化します。
13.7 本セクションの範囲外
- 作成者署名(§9)は変更されません。署名は内側
QubEnvelopeの内部で計算され、アンラップ → tlock 復号 → CBOR 解析の後に回復されます。 - 受信者暗号化された非公開 qub(予約済みのフェーズ 2 機能であり、未仕様)は、第 2 層の機密性階層としてこのラッパーの上に重なります。両層は同時に有効化できます。
- 約定(§6、content_type
0x03)はテキスト qub と完全に同じようにラップされます。ラッパーは内側のコンテンツタイプに対してバイト的に盲目です。
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 を持つ誰もがそれを復号できます。K が存在しないため、URL フラグメントは不要です。これは、サーバーが駆動しなければならない面のために意図的に選ばれたトレードオフです。公開通知メール、サードパーティの埋め込み、より充実した公開後 SEO はいずれも、サーバーが決して保持しないシークレットなしに機能するリンクを必要とします(§13.6)。
生成者が考慮しなければならない帰結:
- 列挙耐性なし。 公開 qub は構造上、§13.1 の列挙耐性プロパティを放棄します。リファレンスアップロードサービスはそれら(およびそれらのみ)に
Visibility: publicの永久ストレージタグを刻印するため、意図的に発見可能です。非公開 qub はそのようなタグを持たず、バイト識別不可能性を保ちます。 - 封印時に平文タイトルが露出。 §3.2 の
titleフィールドはSealedQubCbor内では平文です。ラッパー下では、ビューアーがKを供給するまで隠されています。ラッパーがなければ、解錠前の アップロードの瞬間から 永久ストレージ上で誰でも読み取れます。準拠する作成者アプリは封印時にこれを開示しなければなりません(MUST)。 - 検出は構造的。 準拠するビューアー / 埋め込みは、解析によって 2 つの形状を区別します。
OuterWrapperとして解析されるバイトはKによるアンラップ経路を取り、素のSealedQubCborとして解析されるバイトは直接受理されます。ワイヤフラグは不要であり、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 = 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_hash および qub_id 値を生成しなければなりません(MUST)。このテストベクトルは最初に書く単体テストとすることが望ましい(SHOULD)。上記の正規値はリファレンス実装によって計算されており、ビット単位で一致しなければなりません(MUST)。過去のプリイメージレイアウト(ローンチ前であり、これらに依存している稼働中の 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) を検証しなければなりません(MUST)。これは単一のベクトルではなくプロパティテストです。
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 を生成しなければなりません(MUST)。
実装はまた、すべての有効な PactTerms 入力について serialize(parse(serialize(pact))) == serialize(pact) を検証しなければなりません(MUST)(プロパティテスト)。
14.5 外側ラッパーのクロス言語ベクトル
外側ラッパー(§13)は crates/qub-core/tests/vectors/wrapper_v1.json に別の正規フィクスチャを持ちます。各ケースは (key, nonce, qub_id, sealed_cbor) のタプルを不透明な 16 進入力として固定し、特定の 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)。
このフィクスチャは現在 3 つのケースを固定しています。
| ケース | カバレッジ |
|---|---|
basic-text-public |
最小の現実的な SealedQub 形状。オプションフィールドなし。v1.0 典型の qub の正規ラッパー形状を確立。 |
with-recipient-pubkey |
recipient_pubkey が設定された SealedQub(フェーズ 2 パス)。異なる内側 CBOR キーセット、異なる qub_id。 |
longer-body |
約 4 KiB の本文 — 内側エンベロープと外側暗号文の両方の中で複数バイトの CBOR 長さプレフィックスを行使します。 |
実装は記録された入力に対してバイト単位で同一の expected_wrapper_hex を生成しなければなりません(MUST)。フィクスチャの再生成には QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors が必要であり、意図的な形式変更のために予約されています。
15. 暗号プロファイル統治(将来)
このセクションは v1 については情報提供であり、qub の暗号プリミティブのいずれかに 2 つ目のアルゴリズムが入った最初の時点で規範となります。
15.1 現在の姿勢
プロトコル v1 はプリミティブごとに正確に 1 つのアルゴリズムをバインドします。
- 署名: ML-DSA-65(
sig_alg = 0x01、1952 バイトの公開鍵、3309 バイトの署名)および署名なし(sig_alg = 0x00)。§9.2 のレジストリは他の値を定義しておらず、v1 検証者は{0x00, 0x01}以外のすべてのsig_algを拒否しなければなりません(MUST)。将来の Ed25519 エントリが想定されています(§15.3)が、v1 では割り当てられていません。 - タイムロック: drand quicknet のみ — チェーンハッシュ、公開鍵、ジェネシス時刻、および周期は固定のネットワークパラメータであり、リファレンスの
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs)およびconfig/drand-endpoints.jsonによって保持されます。 - 外側ラッパー: AES-256-GCM v1 のみ(§13)。
検証者は現在、プリミティブごとに鍵と署名の長さをハードコーディングしています。ワイヤ形式によって公開されるアジリティ面はありません。
15.2 意図された形状
2 つ目のアルゴリズムがプロトコルに入るとき、検証者は名前付きの CryptoProfile(例: ExqubV1)向けに構成され、プリミティブごとに許可された値の正確なセット(sig_algs、drand チェーン、ラッパーバージョン、コンテンツタイプ)が列挙されます。プロファイルは検証時に固定され、帯域内で交渉されることはありません。アクティブプロファイル外の値はすべて拒否されます。
これにより、ML-DSA-87 の追加や Ed25519 の有効化が既存の検証者構成を遡及的に弱体化できないことが保証されます。v1 検証者は、v2 プロファイルが公開された後も v1 検証者のままです。
15.3 トリガー条件
次のいずれかが提案されたときに §15 を規範ステータスに昇格させます。
- 2 つ目の
sig_algバイト(Ed25519 の有効化、ML-DSA-87、または §9 レジストリへの新エントリ)。 - 本番使用での 2 つ目の drand チェーン。
- 2 つ目の外側ラッパーバージョン。
それまで §15 は、将来の PR が交渉面を一から再議論するのではなく既知のターゲットに対して着地するように移行形状を固定するプレースホルダーです。
16. 透明性ログと耐久性ティア(設計 — レビュー完了)
ステータス。 本セクションは 設計仕様 です。以下のワイヤ形式、ハッシュ化、信頼モデルは 実装にとって 規範的ですが、透明性ログのコードはまだ出荷されていません。W5 の外部レビューは完了しています。§16.15 は確定した決定と、そこから生じた 拘束力のあるローンチ制約 を記録します。実装はそれらの制約の下で進めることができます。§16 は §15 と同じ意味で前向きであり続けます。すなわち、コードレビューで信頼モデルを再導出するのではなく、確定した設計に対して実装が着地するようにターゲットを固定します。これは厳密に追加的です。既存のすべての qub は個別の Arweave トランザクションを保持し続け、
SealedQub/QubEnvelopeのワイヤ形式に変更はありません。
16.1 根拠と耐久性ティア
今日、qub の耐久性とその時間的コミットメントは、いずれも単一の qub ごとの Arweave トランザクション(§11)に依存しています。これは封印のレイテンシを Arweave のファイナリティに結びつけ、qub ごとのアップロードを製品コストの上限(ARWEAVE_DAILY_CEILING)とし、qub をまたぐ改ざん検出可能な 順序付け を一切提供しません。透明性ログは、その単一ティアの下と周囲に 2 つの層を追加します。
| ティア | 名称 | 保証 | タイミング |
|---|---|---|---|
| T1 | R2 ファースト同期 ack | 耐久性の下限 — 封印が返る前に封印済みバイトが耐久ストレージに書き込まれます(< 300 ms p95)。 |
すべての qub、同期的(§16.10)。 |
| T2 | バッチ化された透明性ログ包含 | 普遍的な追記専用・改ざん検出可能なコミットメント + 全順序付け。Arweave にアンカーされます。 | すべての qub、遅延 + バッチ化(§16.5–16.7)。 |
| T3 | qub ごとの Arweave 永続性 | qub のための個別の Arweave トランザクション。 | 有料アップセル、および Arweave 利用不能時のフォールバック(§16.8)。 |
T2 は qub ごとの Arweave を唯一の耐久性経路ではなく 選択肢(T3)にします。ARWEAVE_DAILY_CEILING は製品上限として廃止され、専用のアンカーウォレットのみのサーキットブレーカーへと格下げされます(§16.7)。ユーザーの封印がそれを超過したことを理由に拒否されることは決してありません。
耐久性の正直さ(解決済み — §16.15 Q6)。 耐久性は 後退しません。T1 の R2 書き込みは同期かつ一度きりであるため、T3 を購入しなかった無料ティアの qub は封印が返った瞬間に完全に耐久化されています。粗くなるのは証明可能な 上限コミットメント時刻 です。無料 qub の場合、それは qub ごとのトランザクションのブロック時刻ではなく アンカーブロック時刻 になります。低ボリューム時 — 現実的なローンチ初期およびオフピークの状態 — では、完全な日次サイクルが稀なエッジではなく 典型的な 下限です。したがって製品上のフレーミングは、コミットされた数値レイテンシを持たない 上限です — 「今すぐ封印され耐久化されています。独立した公開タイムスタンプは次のログアンカーで追加されます(通常は日次)」 — そして 正確な時刻のコミットメント証明は有料 T3 の性質 であり、ティア比較の面と規約(§16.11、§16.15 Q6)で開示されます。いかなる時間境界も内部 SLO に過ぎず、マーケティング上の SLA では決してありません。
16.2 LogLeaf 構造(コミットされた 2 つの形状)
ログエントリは LogLeaf であり、§3.1 プロファイル(確定長、タグなし、float なし、最短形式の整数、NFC テキスト、不在時にオプションフィールドを省略、符号化バイト長の昇順に続いてバイト単位でキーを順序付け)の下で手書きの正規 CBOR として符号化されます。§3.1 の parse → re-encode → compare 正規ガードは、(デコード時だけでなく)ハッシュ前のエンコード経路で 適用されるため、2 つの実装が整数幅やキー順序の差異を通じてリーフバイトについて食い違うことはできません。すべての整数は u8 / u64 / i64 であり、すべてのダイジェストは 32 バイトのバイト文字列(bstr[32])です。保存される Arweave トランザクション id は bstr[32] として運ばれる生の 32 バイト SHA-256 ダイジェストであり、決して base64url テキスト文字列ではありません(§3.3 に一致)。
リーフは kind バイトによって選択される 2 つの形状 を持ちます。なぜなら既定のアップロード経路では Worker はバイトに対して盲目だからです。POST /api/v1/upload は 信頼できないクライアントのアサーション として qub_id と unlock_at のみを受け取ります — body_hash、drand_round、created_at、drand_chain_version はすべて §13 の外側ラッパーの内部に封印されており、その鍵を Worker は決して保持しません。サーバー封印経路(POST /api/v1/seal)のみが平文から body_hash / drand_round を導出します。したがって body_hash + drand_round を運ぶ単一のリーフ形状は、実際の qub の大多数についてオペレーターが検証していない値をコミットすることになります。この分割は、コミットされるすべての値を正直に保ちます。
| キー | 符号化長 | 型 | 存在 | 意味 |
|---|---|---|---|---|
seq |
4 | u64 |
必須 | グローバルな 0 始まりのリーフインデックス。包含証明がコミットする位置。 |
kind |
5 | u8 |
必須 | 0x01 認証済み(サーバー封印)または 0x02 主張(クライアント封印 / バイト盲目アップロード)。 |
ref |
4 | bstr[32] |
必須 | リーフ参照 id。認証済み → 生の qub_id。主張 → ブラインド化された id SHA3-256(qub_id ‖ log_blind_secret)(§16.2.1)。 |
chash |
6 | bstr[32] |
必須 | コンテンツアドレス SHA3-256(stored_bytes) — Worker が両経路で常に正直に計算できる唯一のコンテンツ結合。 |
unlock_at |
10 | i64 |
必須 | コピー(認証済み)または主張(主張)。リーフに入る前に > 0 が検証されます。 |
received_at |
12 | i64 |
必須 | R2-ack 時の Worker のウォールクロック。非証拠的(オペレーター主張、§16.6)。自己記述のために存在し、決して証明にはなりません。> 0 が検証されます。 |
body_hash |
10 | bstr[32] |
kind=0x01 のみ |
0x02 では省略 — §13 の下では Worker がこれを持ちません。 |
drand_round |
12 | 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 の Arweave タグを運んでいる)は生の qub_id をコミットします。これは、スタンドアロン検証可能性が荷重を担うプライバシー不変条件に意図的に譲歩する唯一の箇所です。非公開 qub のスタンドアロン結合は chash です(§16.9)。
log_blind_secret の管理(解決済み — §16.15 Q4)。 ブラインドは平文の機密性ではなく リーフの非リンク可能性 を保護します(§13 ラッパーがそれを独立に担います)。log_blind_secret が侵害された場合、敵対者がすでに保持しているか再構築できる任意の qub_id(バンドル / URL を持つすべての qub、加えて低エントロピーまたは公開の qub_id)について、敵対者は 1 回のハッシュ でリーフ ref を再計算しそれをリンクします — これは未知の空間に対する総当たりではなく、既知の母集団の直接的なリンクです。log_blind_secret を、他のサーバー秘密と同じ管理ティアにある相関 / Sybil 級の秘密として分類し、前方にのみローテーションします(ローテーションは将来のリーフを再ブラインド化しますが、すでにアンカーされたものを遡及的に非リンク化することはできません)。
16.3 リーフおよびノードのハッシュ化
SHA-256 を SHA3-256 に置き換えた RFC 6962 §2.1 のドメイン分離ハッシュ化:
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")
公開される追記専用の権威は、オペレーターがたまたまリーフを提供する生の順序ではなく、累積マークルルート + そのアンカー(§16.5–16.6)です。チェーンは提供された任意の順序について再計算されるため、アンカーされたルートだけが正規の位置を固定します。
16.5 累積マークル木とバッチ化
すべてのリーフにわたる 1 つの増大し続ける RFC 6962 木 が seq 順に存在します — 孤立したバッチごとの木ではありません。(キャリーリーフ連鎖されたバッチごとの構成は却下されました。それは真の接頭辞関係ではないため、その「一貫性証明」は不健全です。)累積木は本物の RFC 9162 一貫性証明を提供し、単一の最近のアンカーが任意の古い qub の包含を証明できるようにします。
LogDO Durable Object は 単一ライター(blockConcurrencyWhile、QuotaDO / EntitlementDO を反映)です — 共有ログへの追記は共有状態に対する読み取り・変更・書き込みであるため、KV ではなく DO を経由しなければなりません(MUST)。これは木の右端フロンティア(O(log n) 個のハッシュ)をキャッシュするため、バッチのクローズは O(batch) です。バッチ は共にアンカーされるリーフの集合です。そのトリガーは構成可能でありプロトコルで固定されてはいません。すなわち、少なくとも LOG_BATCH_MAX_LEAVES(既定 4096)の tree_size の前進、またはアンカーサイクルに達する経過時間、または有料 T3 封印が着地したときの強制フラッシュです。root_i はリーフ 0 .. tree_size_i にわたる累積マークルツリーハッシュです。
16.6 Arweave アンカーによる署名付きツリーヘッド
Arweave アンカートランザクションは署名付きツリーヘッド(Signed Tree Head)そのもの であり、ツリーヘッド自体に対する オペレーター署名を 置き換えます。日次アンカーは qub 鍵を必要としません。Arweave トランザクションの owner が署名だからです。モートの命題は成立します — アンカーされたルートにとって荷重を担うのは不変の基盤であり、qub が保持する秘密ではありません。
設計には正確に 1 つ のウォームな qub 署名鍵が存在し、それは ピン留めされて います。すなわち封印ごとの レシート鍵(§16.10)です。その公開鍵は LogProfile(検証者と共に配布される)にコミットされ、かつ anchor_owner によってクロス署名されるため、検証者はレシートをアンカーと 同じピン留めされたルート に対して検証します。これは §16.15 Q2 の解決です — ピン留めされていない、オペレーターがローテーション可能なレシート鍵は否認可能(オペレーターがその鍵を自分のものではないと否認できる)であり、レシートが抑止するために存在するオペレーターレベルの敵対者に対するレシートの説明責任の価値を無効にしてしまいます。したがって qub は ピン留めされていないログ署名鍵を一切保持しません。レシート鍵はピン留めされ anchor_owner でクロス署名されます。
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 を要求しなければなりません(MUST)。ここで anchor_owner(およびレシート鍵の公開鍵)は、DrandTimelockProvider::quicknet() にすでにある quicknet 定数と並んで qub_core に LogProfile として焼き込まれ、検証者バイナリと共に配布されます。検証者はまた、ゲートウェイの /raw/ 応答を信頼するのではなく、Arweave トランザクションの データ → tx_id バインディングをローカルで検証 しなければなりません(MUST)。これは不正ウォレットのエクイボケーションの穴を塞ぎます。「Arweave にアンカーされている」は、検証者が どの ウォレットかをピン留めするまで無意味です。
ローテーションは §15 統治の 拡張 であり、再利用ではありません(解決済み — §16.15 Q3)。 §15.2 のプロファイル面は現在 sig_alg / drand チェーン / ラッパーバージョン / コンテンツタイプのみを列挙しており、§15.3 のトリガーにはこれらのいずれも挙げられていません — LogProfile / anchor_owner はまだ §15 の面に 含まれていません。したがってローテーション統治は 構築 されなければなりません。§15.3 は(以下のように)拡張されて LogProfile トリガーを追加し、ローテーションは検証者更新で出荷される署名付き LogProfile の引き上げです。計画された ローテーションは送出 → 着信のクロス署名を運びます。侵害駆動の ローテーションはそれを運べません(送出鍵はまさにそのとき信頼できない / 利用不能です)ため §15 統治の引き上げにフォールバックし、その間は(以下の)直前アンカーのフォークチェックが被害を限定します。
エクイボケーションウィンドウ(第一級の信頼パラメータ)。 リーフは、それをカバーするアンカーが Arweave で 確認 されて初めてエクイボケーション耐性を持ちます。ウィンドウは received_at → アンカー確認(≤ サイクル + Arweave ファイナリティ)です。その中での唯一の保証は、ピン留めされた封印レシート(§16.10)と qub の運用上の完全性です。3 つの説明責任アーティファクトが、これを手を振るだけのものではなく正直なものにします(ウィットネスモデルは §16.15 Q2 の解決です):
- ピン留めされた署名付き封印レシート — アップロード応答で返される SCT アナログ(§16.10)であり、ピン留めされ
anchor_ownerでクロス署名されたレシート鍵によって署名されます。アンカー前に取りこぼされたリーフは、被害者に公開可能な否認不能のレシートを残し、サイレントな省略の穴を塞ぎます。 - 公開されたモニタ手法 + 直前チェーンウォーク — アンカー
prevチェーンは head→genesis で辿られます。フォーク(1 つのsizeに異なるrootを持つ 2 つのアンカー、または壊れたprev)は不正行為の公開可能な証明です。エクイボケーション検出はサイレントな仮定ではなく、明示された運用上のコミットメントです。 - 二重に自己公開されるヘッド — 各新規ヘッド
{sth_hash, tree_size}は、専用の qub 所有の 公開・追記専用 GitHub リポジトリ(荷重を担う改ざん検出可能な自己公開のレッグ)に投稿され、ソーシャル投稿はベストエフォートの裏付けとしてのみ行われます。投稿の失敗はページ通知しなければならず(MUST)、サイレントに失敗してはなりません。
正直さの境界(拘束力のある制約)。 qub が両方の投稿面を制御するため、これは 自己公開 であり、独立に立ち会われたものではありません。いかなる製品・マーケティング・法的面も、ログが「独立に立ち会われている」と主張してはなりません。許される主張は エクイボケーションが検出可能であり否認不能のレシートを残す ことです。真の独立した第三者ウィットネスは将来の §15 統治の引き上げに延期されます。
received_at はオペレーター主張であり いかなる主張もそれに依拠してはなりません — それはいかなる製品 / 法的 / API / 証明描画面でも証明としても紛争の裏付けとしても表面化されることはありません。Arweave アンカーブロック時刻 T が唯一の信頼不要なタイムスタンプ(「いつまでにログされたか」の上限)です。received_at に対するモニタの健全性チェックは、オペレーター制御の anchored_at STH フィールドではなく T に対して比較しなければなりません(MUST)。そのようなチェックは正直なオペレーターの時計バグに対するガードに過ぎず、悪意あるオペレーターに対する説明責任のコントロールでは ありません(§16.15 Q5)。
16.7 アンカートランザクション形式とサイクル
AnchorBundle は §16.8 のバンドラーを介して書き込まれる正規 CBOR の Arweave トランザクション本体です: ver:u8、sth:bstr(正規 SignedTreeHead バイト)、prev_anchor:bstr(直前のアンカー tx id の生バイト。ジェネシスでは省略)、chain_hash:tstr(有効な drand チェーン — quicknet)、および バッチのリーフ CBOR ストリームを seq 順に 含み、アンカーが自己完結するようにします。モニタは qub 依存なしに本体から root を再導出します。(リーフストリームが高ボリュームで大きくなる場合、将来のリビジョンは参照によってリーフ範囲のみをコミットするかもしれません。記録するのみで v1 では採用しません。)
Arweave タグは意図的に列挙可能です — 非公開 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 本体が唯一の権威です。
サイクル: 既定では日次であり、ボリュームに応じて見直されます(サイズトリガーは負荷下で実効サイクルを自動的に短縮します)。有料 T3 封印はアンカーを強制するため、支払う顧客が丸 1 日待つことは決してありません。アンカーウォレット は専用かつ低速度であり、アップロードウォレットとは別です — それは 独自の JWK(アップロードウォレット上の論理的な役割ではなく、別個の鍵)でなければならず(MUST)、アップロードウォレットの侵害がアンカーを偽造できないようにします — そして 1 日あたりのアンカートランザクションの厳格な予算(格下げされた ARWEAVE_DAILY_CEILING)を持ちます。管理姿勢は率直に述べられます: 狭いスコープのホット鍵で、厳格なサーキットブレーカーと低残高 であり、「コールド」ではありません — 毎日自動署名するウォレットはコールドであり得ず、本仕様はそうであるかのように装いません。
16.8 ANS-104 バンドラー
自社製の ANS-104 DataItem エンコーダおよびディープハッシュ署名器であり、およそ 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
署名は Arweave の deepHash — ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] にわたる再帰的な SHA-384 ダイジェスト(Arweave のワイヤ要件、crypto.subtle.digest("SHA-384"))— に続いて、crypto.subtle を介してウォレット JWK でディープハッシュに対する RSA-PSS を行います。id = base64url(SHA-256(signature))。ここでの SHA-384 は Arweave ワイヤ専用のプリミティブとして隔離され、決して qub の信頼プリミティブではありません(§15 がフェンスを記録します。qub の信頼ハッシュは全体を通じて SHA3-256 です)。
1 つのコード経路が 3 つの消費者に対応します: 有料 T3 の qub ごとの永続性、Arweave 利用不能時のフォールバック(DataItem をキューに入れ、いずれにせよ R2 ファースト ack を返す — これは現在の ARWEAVE_UNAVAILABLE 503 の行き止まりを塞ぎます)、そして AnchorBundle の書き込みです。署名スキーム(解決済み — §16.15 Q8): v1 は RSA-PSS(署名タイプ 1)で署名します。既存の Arweave ウォレット JWK 機構を再利用します(新たな長命鍵の管理がゼロであり、「秘密を 1 つ減らす」命題に寄与します)。Ed25519 は §15 の PQ 移行経路に延期されます。
手書きのディープハッシュは W5 で最もリスクが高く、最も自然なカバレッジが低いコードであるため、そのゲーティングは 交渉の余地がありません(§16.15 Q8):
- クロス言語フィクスチャ
tlog_v1.json(Rust + TS、§14.5 のwrapper_v1.jsonパターン)は、ディープハッシュ、DataItem バイト + id、リーフハッシュ、5 リーフのルート + 監査パス、STH ハッシュ、包含証明、一貫性証明を、署名と検証の両方向で カバーします(検証方向が重要なのは、§16.6 のローカル tx → tx_id チェックがディープハッシュをライターだけでなく すべてのスタンドアロン検証者 に引き込むためです)。 - リファレンス ANS-104 バンドラーを通じた一度きりの相互運用ラウンドトリップ。これは 静的なテストデータとしてのみ — 決して npm ランタイム依存としてではなく 消費されます(Web Crypto のみ / インストールスクリプトなしの姿勢は維持されます)。
- ディープハッシュ + RSA-PSS 経路は、本番が使用するのと 同じ
crypto.subtleプリミティブ を通じてラウンドトリップしなければならず、自社製エンコーダがバイト互換であるようにします。 - 継続的な バンドル後受理モニタ が、各アンカー / フォールバック DataItem が実際に Arweave の受理を達成することを、アラーム + サーキットブレーカーと共に確認します — ディープハッシュは Arweave 利用不能時のフォールバックキューにも対応するため、サイレントなリグレッションは、まさにそれがカバーするために存在する障害の最中に、そのキューをネットワークに拒否されたアイテムで満たしてしまうからです。
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) でキー化された永続マークルノードストア である 場合に限り であって、batch ごとのノードデルタではありません。座標キー化されたストアでは、任意の (リーフ i, サイズ N) 監査パスは O(log N) 個の直接 R2 GET の集合であり、バッチ境界をまたぐ再計算はありません。バッチキー化されたストアではそうではなく、それがこの解決が塞ぐストレージレイアウトのギャップです。リーフ本体も同様に seq でコンテンツアドレス可能です。W5 のテストベクトルは、LogDO ストレージを消去した状態で R2 + Arweave のみを使ってジェネシス時代のコールドリーフをはるかに後のルートに対して 証明しなければならず(MUST)、§16.13 の回収安全性の主張が主張されるだけでなく裏付けられるようにします。O(log N) の逐次 R2 GET は 非同期の証明エンドポイントにのみ 属し、封印ホット経路(§16.10)やティックごとの cron には決して属しません。
16.10 R2 ファースト ack 順序付け
POST /api/v1/upload のシーケンスは次のようになります:
- フロントハーフのゲート(認証、検証、冪等性シャードキー)— 変更なし。
- 同期的に
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— 耐久性の下限。また W1 のプリキャッシュレース(以前は Arweave サブミット後のctx.waitUntil)も塞ぎます。 - 同期的に
await LogDO.append(leaf)— 1 回のコロ内 DO RPC。単一ライターがseqを割り当て、エントリチェーンを伸ばし、フロンティアを更新します。(共有状態への RMW → DO、KV ではない。)appendRPC は それだけ を行います —O(batch)のマークルバッチクローズ作業は、この RPC から外して LogDO アラームで実行されます。さもなければ追記の p95 がLOG_BATCH_MAX_LEAVES番目の封印ごとにスパイクします。 - 今すぐ ack を返す — 封印レシート(ピン留めされたレシート鍵で署名、§16.6)と
{ tx_id, log_seq, anchor_status: "pending" }を伴います。複数秒の Arweave ファンアウトはクリティカルパスから除去されます。 - 1 回の
ctx.waitUntilが遅延作業をエンキューします: qub ごとの Arweave サブミット(現在はベストエフォート / 有料。失敗時はユーザーを 503 にするのではなくバンドラーフォールバックキューに回します)に加えて既存の暫定メタ書き込み。バッチクローズとアンカリングは LogDO アラームと日次アンカー cron から独立して実行されます。ループ内にctx.waitUntilはありません。既存の冪等性シャードキーは保持されます。
レイテンシ予算(解決済み — §16.15 Q7)。 < 300 ms p95 のターゲットは仮定ではなく 計測されるローンチゲート です。正直なクリティカルパスは、フロントハーフの KV 読み取り + 1 回の R2 PUT + 2 つの直列化された Durable Object — 既存の QuotaDO 封印クォータ引き落とし と 新しい LogDO 追記 — です。したがって予算は 1 回ではなく 2 回のコロ内 DO ラウンドトリップを考慮しなければなりません。QuotaDO のものを反映した LogDO レイテンシアラームを出荷し、p95 のリグレッションをリリースブロッカーとして扱います。
16.11 信頼モデル — リーフの種別でスコープされた正確な主張
kind=0x01(認証済み) の場合: 「このコンテンツ — body_hash に一致する本文、qub_id で識別される — は位置 seq で qub の追記専用ログにコミットされ、Arweave ブロック時刻 T より遅くない時点で存在しており、drand ラウンド R = unlock_round(unlock_at) まで暗号学的に読み取り不可能であった。」 これは完全な {tlock ラウンドバインディング + マークル包含 + アンカーされたルート} のトリプルです。
kind=0x02(主張、既定) の場合: 「コンテンツアドレス chash を持ち qub_id と unlock_at を主張する不透明な暗号文が、位置 seq で追記専用ログにコミットされ、Arweave ブロック時刻 T より遅くない時点で存在していた。」 ラウンドと本文のレッグは、ログではなく既存の §11 .qub バンドル検証(qub_core::unlock)によって供給されます。ログが素の qub ごとのトランザクションを超えて加えるものは、改ざん検出可能な順序付け、信頼不要な上限コミットメント時刻、そしてエクイボケーション耐性です。
どちらの主張も §11 に従って次を除外します: sig_alg ≥ 0x01 のない作成者性、意図、およびサブアンカー粒度のタイミング。どちらも、いかなる主張も received_at に依拠させません。
主張の上限(拘束力のあるローンチ制約 — 解決済み §16.15 Q1)。 無料 / 既定(kind=0x02)の qub について、上記のスコープされた kind=0x02 主張が、いかなる製品・マーケティング・規約・証明描画面が主張してよいことの 上限 です。いかなる面も、ログ が既定 qub のコンテンツまたは公開ラウンドを証明すると述べたり示唆したりしてはなりません — ログが証明するのは 不透明な暗号文の順序付け + 信頼不要な上限コミットメント時刻 です。コンテンツとラウンドの証明は、ログから独立した既存の §11 .qub バンドル検証からのみ得られます。これはコピーに対する厳格なローンチブロッカーであり、文体上の好みではありません。これがバイト盲目の既定経路を正直に保つ解決です。
16.12 バージョン管理と W3 の調整
SealedQub ワイヤの引き上げはなく、したがって プロトコルバージョンの引き上げもありません(§12.1)。ログは既存のフィールドとバイトにコミットするサイドカーであるため、§12.2 のプロトコルバージョン履歴には入りません。W3 のオプションの drand_chain_version は手つかずであり、唯一のオプション SealedQub フィールドであり続けます。代わりにログは独自の独立したバージョン空間を導入します — LOG_VERSION_1、ANCHOR_FORMAT_1、InclusionProof.ver — これは §12.4 のラッパーバージョン独立性を反映しています(ラッパーはプロトコルバージョンから独立したバージョンバイトを運び、ログバージョンは同じ分離に従います)。
証明の配信は既定でフェッチされ、オプションの相乗りがあります。 証明は封印時には存在し得ない(アンカーがまだ書き込まれていない)ため、封印時の .qub バンドルは証明なしのままです。W7 の検証者は GET …/proof を一度フェッチするか、完全オフラインモードでは Log-Id に対する Arweave クエリを介して公開された AnchorBundle から証明を再構築します。.qub バンドル(W7)は オプションの inclusion_proof メンバー を予約します — 封印時には不在で、コールドアーカイブ用のアンカー後の再エクスポートで投入されます — これは W3 の drand_chain_version と同じ「オプション、既定で省略、追加的」パターンに従います。
16.13 保持
LogDO のオープンテール、R2 の証明提供基盤、アンカーサーキットブレーカーのカウンタ、およびバンドラーフォールバックキューの保持ウィンドウは docs/DATA-RETENTION.md に規定されます。原則: ログのホットなエントリごとのストレージ(LogDO)はアンカー後に回収可能です。その監査素材 — 座標キー化された (level, index) マークルノードストア + seq アドレスのリーフ本体(§16.9)+ Arweave アンカー — は永続的です。DO からコールドリーフを回収しても発行済みの証明が無効になることは決してありません。証明は DO ではなくその永続 R2 ノードストアと Arweave アンカーに対して解決されるからです(そして §16.9 の DO 消去テストベクトルがそれを証明します)。
16.14 テストベクトル
W5 はクロス言語フィクスチャ tlog_v1.json(§16.8)に加えて、作業済みのベクトルを出荷します: kind=0x01 と kind=0x02 のリーフ → leaf_hash、5 リーフの累積ルート、1 つの包含証明、1 つの一貫性証明、1 つの AnchorBundle、1 つの DataItem id。これらは §14.5 の外側ラッパーのベクトルと並んで存在し、Rust(qub-core)と TypeScript(Worker)の両実装によって行使されます。
16.15 レビュー決定(W5 — 解決済み)
W5 の外部レビュー(敵対的な設計パス + オーナーサインオフ)は完了しています。以下の各決定は確定しており、上記の §16 テキストに反映されています。拘束力のあるローンチ制約 は末尾で再掲されます。実装はそれらの下で進めることができます。
- 既定経路(
kind=0x02)のリーフの正直さ — 解決済み。 指定どおりに 2 リーフ種別の分割を出荷します:kind=0x02はbody_hashもdrand_roundもコミットしません。バイト盲目経路に*_body_hashフィールドはありません(それはインテグレーターにとって最も読みやすい偽の「検証済み」シグナルとなり、しかも §11 がすでにバンドルから提供する利便性です)。ログ認証 qub にサーバー封印を 要求しません(それは平文を Worker に通すことを強制し、暗号シュレッディングのモートを破壊します)。自己記述的なショートサーキットは、リーフフィールドではなく.qubバンドル / 証明エンベロープ内の検証者再計算フィールドに属します。オーナー確認済みの主張の上限: §16.11。 - エクイボケーション / 省略の説明責任 — 解決済み。 封印レシート鍵は
LogProfileにピン留めされanchor_ownerでクロス署名されます(以前の「署名鍵なし」の矛盾を塞ぎます。§16.6)。ローンチのウィットネスモデル: ピン留めされたレシート + モニタ手法 + 直前チェーンウォーク + 二重に自己公開されるヘッド(qub 所有の公開 GitHub リポジトリ、ソーシャルはベストエフォート)。検出可能 + レシート付き としてマーケティングされ、独立に立ち会われている とは 決して されません。真の第三者ウィットネスは §15 統治の引き上げに延期されます。 - ピン留めされた anchor-owner 信頼ルート + ローテーション — 解決済み。
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 には生のqub_id(すでに §16.2.1)、スタンドアロン結合としてchashを維持します。log_blind_secretは相関 / Sybil 級の秘密であり、前方にのみローテーションします(§16.2.1)。 received_at— 解決済み。 リーフに残しますが、コミットされても明示的に非証拠的です。いかなる面でも証明や紛争の裏付けとして表面化されることはありません。いかなるモニタの健全性チェックも、オペレーター制御のanchored_atではなく Arweave ブロック時刻Tに対して比較します(§16.6)。- 無料ティアの証明可能タイミング — 解決済み(オーナーサインオフ)。 耐久性は後退しません。証明可能な上限コミットメント時刻のみがアンカーブロック時刻へと粗くなります。無料ティアのコピーは数値 SLA を使いません(「…次のログアンカーで追加されます、通常は日次」)。正確な時刻の証明は有料 T3 の性質であり、ティア比較の面 + 規約で開示されます(§16.1)。
- Workers 上の累積木 — 解決済み。 単一の累積 RFC 9162 木 + フロンティアキャッシュされた単一ライター LogDO(約 1k writes/sec の DO 上限に対して快適な余裕。それに近づくまでマークル・オブ・シャードルートのシャーディングは延期)。ブロッキング前提条件: 座標キー化された
(level, index)R2 ノードストア + DO 消去コールドリーフテストベクトル(§16.9)。< 300 msは 2 つの直列化された DO にわたる計測されるローンチゲートです(§16.10)。 - ANS-104 署名スキーム + ディープハッシュ — 解決済み。 RSA-PSS(sig タイプ 1、専用のアンカーウォレット JWK を再利用)。Ed25519 は §15 の PQ 経路に延期。手書きの SHA-384 ディープハッシュは、両方向のクロス実装フィクスチャ、静的のみのリファレンスバンドラー相互運用チェック、共有
crypto.subtleラウンドトリップ、およびバンドル後の Arweave 受理モニタでゲートされます(§16.8)。
拘束力のあるローンチ制約(実装 + 製品 / 法的レビューに引き継ぐ):
- 主張の上限(Q1/Q6)。 いかなる面も ログが 既定 qub のコンテンツまたは公開ラウンドを 証明する と述べてはなりません。許される主張は 改ざん検出可能に、信頼不要な上限コミットメント時刻と共に、順序付けられている ことです。無料ティアのタイムスタンプコピーは数値レイテンシを運びません。正確な時刻の証明は有料 T3 のみです。
- ウィットネスの正直さ(Q2)。 エクイボケーションを 検出可能 + レシート付き としてマーケティングし、独立に立ち会われている とは決してしません。
- レシート鍵 + アンカー鍵(Q2/Q8)。 レシート鍵はピン留め + クロス署名されます。アンカーウォレットはアップロードウォレットとは別個の独自の JWK です。
- ディープハッシュゲート(Q8)。 両方向のフィクスチャ + 相互運用チェックがパスするまで、いかなるアンカーまたは T3 tx も出荷されません。受理モニタは失敗時にページ通知します。
- ストレージ前提条件(Q7)。 座標キー化されたノードストア + DO 消去コールドリーフベクトルは、「回収が証明を決して無効化しない」保証の前提条件です。