ข้อกำหนดโปรโตคอล qub

qub คือโปรโตคอลสำหรับข้อผูกพันเชิงเวลาทางการเข้ารหัส: ระบบสำหรับผนึกข้อความให้กับวันที่ในอนาคต และเมื่อวันนั้นมาถึง ก็สามารถพิสูจน์ได้อย่างแม่นยำว่าพูดอะไรไว้และพูดเมื่อใด

มีองค์ประกอบพื้นฐานสามอย่างที่ทำให้ระบบนี้ทำงานได้ drand เป็นบีคอนความสุ่มแบบกระจายศูนย์ — วันเปิดเผยถูกบังคับใช้ด้วยฟิสิกส์ ไม่ใช่ด้วยเจตนาดีของฝ่ายใด พื้นที่เก็บถาวรสาธารณะ เป็นที่จัดเก็บสาธารณะที่ป้องกันการแก้ไข — ไม่มีฝ่ายใดสามารถแก้ไขหรือลบ qub ได้เมื่อผนึกแล้ว ML-DSA-65 เป็นลายเซ็นดิจิทัลแบบหลังควอนตัม — qub แต่ละชิ้นผูกกับคู่กุญแจที่ความลับไม่เคยออกจากอุปกรณ์ของผู้เขียน

เมื่อรวมกันแล้ว องค์ประกอบเหล่านี้สร้างคำกล่าวที่ล็อกเวลาได้ ตรวจจับการดัดแปลงได้ และระบุที่มาได้ — ใบเสร็จที่มูลค่าเพิ่มขึ้นเมื่อความสามารถของโลกในการปลอมแปลงอดีตดีขึ้น

ส่วนที่เหลือของเอกสารนี้คือข้อกำหนดเชิงบรรทัดฐานที่จำเป็นสำหรับการใช้งานที่ทำงานร่วมกันได้


ข้อกำหนดโปรโตคอล qub

ฟิลด์ ค่า
เวอร์ชัน 1.0 (เวอร์ชันโปรโตคอล 0x01 เวอร์ชันห่อหุ้มภายนอก 0x01)
วันที่ 2026-05-01
สถานะ ฉบับร่าง
ตรวจทานถึง 2026-05-01

เอกสารนี้เป็นข้อกำหนดโปรโตคอลเชิงบรรทัดฐานสำหรับระบบข้อผูกพันเชิงเวลา qub กำหนดโครงสร้างข้อมูล กฎการเรียงลำดับบิต สูตรการได้มาซึ่งค่า และขั้นตอนการตรวจสอบที่จำเป็นสำหรับการใช้งานที่ทำงานร่วมกันได้

ขอบเขต: ชั้นโปรโตคอลถูกออกแบบให้เป็นกลางทางภาษาโดยเจตนา — เนื้อหา qub เป็นข้อความ / มาร์กดาวน์ / ไบต์สัญญาที่ทึบแสง และการเรนเดอร์ตามภาษาท้องถิ่นเป็นความรับผิดชอบของผู้ชม (เว็บแอป qub.social, iframe <qub-embed>, ไคลเอนต์ MCP เป็นต้น)


1. สัญลักษณ์และข้อตกลง

สัญลักษณ์ ความหมาย
u8, u64, i64 จำนวนเต็มไม่มีเครื่องหมาย/มีเครื่องหมายตามความกว้างบิตที่ระบุ
[u8; N] อาร์เรย์ไบต์ความยาวคงที่ N ไบต์
Vec<u8> อาร์เรย์ไบต์ความยาวแปรผัน
Option<T> ค่าประเภท T หรือไม่มีอยู่
String สตริงข้อความ UTF-8 ที่ผ่านการนอร์มัลไลซ์แบบ NFC
`
SHA3-256(x) แฮช NIST SHA3-256 ของสตริงไบต์ x (FIPS 202)
ceil(x) ฟังก์ชันปัดขึ้น: จำนวนเต็มที่น้อยที่สุดที่ ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian ไบต์ที่มีนัยสำคัญที่สุดมาก่อน

จำนวนเต็มทั้งหมดในการสร้างพรีอิมเมจถูกเข้ารหัสเป็นอาร์เรย์ไบต์ความกว้างคงที่ big-endian (i64 → 8 ไบต์, u8 → 1 ไบต์) ยกเว้นที่ระบุไว้เป็นอย่างอื่น

ไทม์สแตมป์ทั้งหมดเป็น วินาที Unix ใน UTC


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); reply_to ตั้งค่าเป็น qub_id ของ qub แม่สำหรับ qub แบบสายตอบกลับ (ดู §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 ทั้งหมดต้องเป็นไปตามโปรไฟล์นี้ การใช้งานสองแบบที่ได้รับโครงสร้างเชิงตรรกะเดียวกันต้องสร้างไบต์ที่เหมือนกัน

3.1 กฎการเข้ารหัส

กฎ ข้อกำหนด
มาตรฐาน RFC 8949 §4.2.1 (ข้อกำหนดการเข้ารหัสแบบกำหนดได้แกนกลาง)
การเรียงลำดับกุญแจ map เรียงตาม ความยาวไบต์ที่เข้ารหัสแล้ว ก่อน (สั้นก่อนยาว) แล้วจึง เรียงตามพจนานุกรม (เปรียบเทียบไบต์ต่อไบต์สำหรับการเข้ารหัสที่มีความยาวเท่ากัน)
การเข้ารหัสจำนวนเต็ม รูปแบบสั้นที่สุด: 0–23 ในไบต์เริ่มต้น; 24–255 ใน 2 ไบต์; 256–65535 ใน 3 ไบต์ และอื่น ๆ
การเข้ารหัสความยาว ความยาวแบบกำหนดได้เท่านั้น ไม่อนุญาตอาร์เรย์ map ไบต์สตริง หรือสตริงข้อความที่มีความยาวไม่กำหนด (additional info = 31 ถูกห้าม)
แท็ก ไม่มีแท็ก CBOR (major type 6 ถูกห้าม)
ทศนิยมลอย ไม่มี float (major types 7 values 0xF9–0xFB ถูกห้าม)
สตริงข้อความ เข้ารหัส UTF-8 นอร์มัลไลซ์ NFC (Unicode Normalization Form C)
สตริงไบต์ ไบต์ดิบ ไม่มีการเข้ารหัส base64 ที่ชั้น CBOR
กุญแจซ้ำ ปฏิเสธพร้อมข้อผิดพลาด ตัวแยกวิเคราะห์ต้องไม่ยอมรับกุญแจ map ซ้ำอย่างเงียบ ๆ
ค่าเรียบง่าย อนุญาตเฉพาะ true (0xF5), false (0xF4) และ null (0xF6) เท่านั้น
ฟิลด์ที่เลือกได้ ฟิลด์ที่เลือกได้ที่ไม่มีอยู่จะถูก ละเว้น จาก map CBOR ทั้งหมด (ไม่ได้เข้ารหัสเป็น null) ฟิลด์ที่เลือกได้ที่มีอยู่จะรวมอยู่ในลำดับกุญแจที่เรียงแล้ว

3.2 ลำดับกุญแจมาตรฐานที่ตรวจสอบแล้ว

ลำดับกุญแจเหล่านี้เป็นเชิงบรรทัดฐาน การใช้งานต้องส่งกุญแจในลำดับนี้พอดี การ assertion ระหว่างดีบักควรตรวจสอบลำดับในบิลด์ที่ไม่ใช่รีลีส

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 text string ความยาวที่เข้ารหัส = 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 (map 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) Major type 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 มีการเพิ่มไบต์ padding 0x00 หนึ่งไบต์เพื่อให้ครบ 10 ไบต์เพื่อการจัดเรียง การใช้งานต้องใช้ 10 ไบต์เหล่านี้พอดี: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]

การเข้ารหัส outcome_at: V1.1 ขยายพรีอิมเมจจาก 92 เป็น 100 ไบต์เพื่อรวมฟิลด์ outcome_at ที่เลือกได้เข้าในการผูกพัน เมื่อ outcome_at ไม่มีอยู่จะเข้ารหัสเป็น 8 ไบต์ศูนย์ ตัวตรวจสอบโปรโตคอลปฏิเสธ outcome_at <= 0 ในทุกที่ จึงทำให้ค่า sentinel นี้ไม่สามารถชนกับค่าที่ถูกต้องได้ ดู §3.2 (รูปแบบการสื่อสาร) และ tasks/verdict-uplift-plan.md ในต้นไม้ซอร์สสำหรับกลไก verdict ที่เป็นเหตุผลของฟิลด์นี้

การเข้ารหัส drand_round: V1.2 ขยายพรีอิมเมจจาก 100 เป็น 108 ไบต์เพื่อรวม drand_round (รอบ drand เป้าหมาย, §4.3) เข้าในการผูกพัน และเลื่อนตัวแยกโดเมนเป็น QUB_ID_V2 การกระทำนี้ผูกรอบไทม์ล็อกเข้ากับเอกลักษณ์ qub: เกตเวย์ไม่สามารถผูก ciphertext ซ้ำเข้ากับรอบที่แตกต่าง (เช่น รอบที่ผ่านไปแล้ว) จาก unlock_at ที่แสดงได้ ขั้นตอนการปลดล็อก (§8) ยังตรวจสอบเพิ่มเติมว่ารอบที่ฝังอยู่ใน stanza ของ tlock ciphertext ตรงกับ unlock_round(unlock_at) ดังนั้นเวลาปลดล็อกที่แสดงจึงพิสูจน์ได้ว่าเป็นรอบที่ควบคุมการถอดรหัส

คุณสมบัติ:

4.2 body_hash

body_hash = SHA3-256(body)

โดยที่ body คือข้อมูลโหลดเนื้อหา Vec<u8> ดิบ สำหรับ qub ข้อความ นี่คือเนื้อหา qub ที่เข้ารหัส UTF-8

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 ทำงานในเวลาแฮชเพื่อให้ไดเจสต์มีเสถียรภาพข้ามลำดับโค้ดพอยต์ที่เทียบเท่ากันทางการมองเห็น ค่า sentinel ที่เป็นศูนย์ทั้งหมดถูกสงวนไว้สำหรับกรณีไม่มี สตริงว่างถูกปฏิเสธที่ขอบเขต CBOR มาตรฐานเนื่องจากเป็นการเข้ารหัส "ไม่มี" ที่ไม่เป็นมาตรฐาน (การเข้ารหัสมาตรฐานละเว้นฟิลด์ทั้งหมด)

4.3 การแมปรอบปลดล็อก

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
พารามิเตอร์ แหล่งที่มา ตัวอย่าง
unlock_at วินาที Unix UTC ที่ผู้ใช้เลือก 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time drand chain info (genesis_time) 1595431050
chain_period_seconds drand chain info (period) 30

การดำเนินการ ceil() เลือก รอบ drand แรกที่มีเวลาเปิดเผย ≥ unlock_at สิ่งนี้ทำให้มั่นใจว่า qub ไม่สามารถถอดรหัสได้ก่อนเวลาปลดล็อกที่เลือก

กรณีขอบ: ถ้า (unlock_at - chain_genesis_time) หารด้วย chain_period_seconds ลงตัวพอดี ผลลัพธ์จะเป็นรอบนั้นพอดี — qub ปลดล็อกที่เวลาเปิดเผยของรอบนั้นแน่นอน

การตรวจสอบ: unlock_at ต้องอยู่ในอนาคตในเวลาผนึก unlock_at ต้องไม่เกิน 10 ปีนับจาก created_at (เพื่อจำกัดความเสี่ยงในการพึ่งพา drand ในระยะยาว; UI ควรเตือนสำหรับวันที่ปลดล็อกที่เกิน 2 ปี)


5. Newtype รูปแบบการสื่อสาร

Newtype รูปแบบการสื่อสารให้ความปลอดภัยในเวลาคอมไพล์ป้องกันความสับสนของไบต์ CBOR กับ JSON ข้อความธรรมดาดิบ หรือการเข้ารหัสไบต์อื่น ๆ

ประเภท ประกอบด้วย สร้างโดย บริโภคโดย
SealedQubCbor CBOR มาตรฐานของ SealedQub serialize_sealed_qub() การอัปโหลดสู่พื้นที่เก็บถาวร, การดึงข้อมูลโดยผู้ชม
QubEnvelopeCbor CBOR มาตรฐานของ QubEnvelope 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() ควรตรวจสอบว่าอินพุตเริ่มต้นด้วยเฮดเดอร์ map CBOR ที่ถูกต้อง การตรวจสอบโครงสร้างเต็มรูปแบบเกิดขึ้นในเวลาแยกวิเคราะห์ ไม่ใช่ในเวลาสร้าง เพื่อหลีกเลี่ยงการแยกวิเคราะห์ซ้ำ


6. ทะเบียนประเภทเนื้อหา

ค่า ประเภท ขนาด Body สูงสุด หมายเหตุ
0x00 สงวน (ไม่ถูกต้อง) ต้องไม่ใช้
0x01 ข้อความธรรมดา (UTF-8, Markdown จำกัด) 50 KB ชำระเงิน / 10 KB ฟรี ดู §10 สำหรับกฎการเรนเดอร์ การแบ่งฟรี / ชำระเงินถูกบังคับใช้โดยบริการอัปโหลด เพดานแข็งระดับโปรโตคอลคือ 50 KB
0x02 สงวน (อนาคต) จัดสรรไว้สำหรับประเภทเนื้อหาในอนาคต; ไม่ถูกต้องใน v1 ผู้ชมต้องปฏิเสธตามกฎด้านล่าง
0x03 สัญญา (ข้อตกลงทวิภาคี, เนื้อหา CBOR) 100 KB Body คือ canonical CBOR PactTerms (§6.1) การลงนามผู้ลงนามร่วมตาม §9.7
0x04 คำตัดสิน (การให้คะแนนตัวเองของผู้สร้าง, เนื้อหา CBOR) 8 KB Body คือ canonical CBOR VerdictBody (§6.2) ส่งออกได้เฉพาะ intent ฝั่งระบบ verdict เท่านั้น ความสัมพันธ์กับ qub แม่อยู่บน Arweave tag Parent-Tx-Id ไม่ได้อยู่บน body ดู verdict-uplift-plan §3.4

ผู้ชมต้องปฏิเสธประเภทเนื้อหาที่ไม่รู้จักด้วยข้อผิดพลาดที่ผู้ใช้มองเห็นได้ชัดเจน ผู้ชมต้องไม่พยายามเรนเดอร์ประเภทที่ไม่รู้จักเป็นข้อความ

6.1 Body ของสัญญา (content_type = 0x03)

Body ของสัญญาคือการเข้ารหัส CBOR มาตรฐานของค่า PactTerms:

PactTerms {
    pact_version:  u8,                    // 0x01 for structured/v1
    title:         String,                // ≤ 200 bytes, NFC
    terms:         Vec<PactTerm>,         // ≤ 20 rows
    party_a:       PartyIdentifier,       // initiator
    party_b:       PartyIdentifier,       // counter-signer
    notes:         Option<String>,        // ≤ 5,000 bytes, NFC; absent key if none
}

PactTerm       { key: String (≤ 100), value: String (≤ 2,000) }   // NFC on both sides
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }

ลำดับกุญแจ CBOR มาตรฐานสำหรับ map ทั้งสามรายการอยู่ใน §3.2 CBOR สัญญาที่เรียงลำดับบิตทั้งหมดต้องไม่เกิน 100 KB (ตรงกับ §6)

ตัวแยกประเภทสคีมา แถวแรกใน terms สำหรับสัญญา structured/v1 ต้องเป็น { key: "pact_schema", value: "structured/v1" } แถวที่ไม่มีเครื่องหมายนี้เป็นสัญญา "กำหนดเอง" และไม่ได้รับการตรวจสอบเชิงโครงสร้างหรือการเรนเดอร์ที่ตระหนักถึงสคีมา

ช่องการรับทราบที่ถูกตรึง สัญญา 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 สตริงเหล่านี้ไม่ได้แปลเป็นภาษาท้องถิ่น; body ที่ลงนามแล้วเป็นกลางทางภาษา การเปลี่ยนถ้อยคำใด ๆ ต้องมีเวอร์ชันสคีมาใหม่ (structured/v2)

แปดสตริง การค้นหา (acknowledgement_for(role, kind)) และเหตุผลสำหรับแต่ละสตริงถูกตรึงโดยการใช้งานอ้างอิง การใช้งานที่สอดคล้องต้องส่งค่าการรับทราบที่เป็นไบต์เหมือนกัน การทดสอบ golden-fixture SHA3-256 body-hash ที่ครอบคลุมการรวมบทบาททั้งสี่จับการเลื่อนใด ๆ

ลำดับการแสดงผลของผู้ชม สตริงการรับทราบมีวลีเช่น "described above" ซึ่งสันนิษฐานว่าแถวคำอธิบาย / ขอบเขตเรนเดอร์ก่อนการรับทราบ ผู้ชมต้องเรนเดอร์อาร์เรย์ terms ตามลำดับ CBOR; การจัดเรียงใหม่ทำลายความหมายของข้อความ

ผู้ติดต่อของคู่สัญญา เมื่อ contact ของฝ่าย B เป็นที่อยู่อีเมลที่ถูกต้อง บริการอัปโหลด qub จะส่งอีเมลคำเชิญรีวิว / ลงนามร่วมโดยอัตโนมัติในเวลาจัดเตรียม และผูกการลงนามร่วมเอาในที่สุดเข้ากับการยืนยันที่อยู่เดียวกันนั้น (§9.7) สัญญาที่ผู้ติดต่อฝ่าย B ไม่มีอยู่ยังสามารถลงนามร่วมได้ แต่ผ่านช่องทางนอกแบนด์เท่านั้น — บริการปฏิเสธคำขอลงนามร่วมที่ไม่สามารถสร้างเครื่องหมายยืนยันอีเมล 15 นาทีที่ตรงกันได้

6.2 Body ของคำตัดสิน (content_type = 0x04)

Body ของคำตัดสินคือการเข้ารหัส canonical CBOR ของค่า VerdictBody:

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
}

ลำดับกุญแจ canonical 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 (ตรงกับแถวรีจิสทรีด้านบน)

Enum ของผลลัพธ์ ไบต์บนสายเป็นกลางทางเจตนา; สี่กลุ่ม Right / Partial / Wrong / Unfalsifiable ครอบคลุมพื้นที่ผลลัพธ์ของทุก intent ที่มีคำตัดสิน ป้ายเฉพาะ intent ("Called it" / "Kept it" / "Shipped" / "Confirmed" สำหรับ Right เป็นต้น) เป็นเรื่องการเรนเดอร์ฝั่งผู้ชมที่แก้ไขเทียบกับ intent ของ qub แม่ — สายยังคงเป็นกลางทางภาษาและ intent ค่าที่อยู่นอก 1..=4 ต้องถูกปฏิเสธในขั้นตอนถอดรหัส

การเชื่อมโยงกับ qub แม่ qub ของคำตัดสินไม่ได้ใส่การอ้างอิงไปยัง qub แม่ใน body รหัสธุรกรรม Arweave ของ qub แม่จะส่งออกเป็นแท็กการจัดเก็บ Parent-Tx-Id ในเวลาอัปโหลด (§7 ชั้นแท็กการจัดเก็บ) วิธีนี้ทำให้ body เป็นคำกล่าวที่ลงนามครบในตัวเองของการประเมินตนเอง ห่วงโซ่ตรวจสอบ ("ทำนายอะไรถูก") ถูกสร้างขึ้นผ่านการค้นหาแท็ก Arweave

ความปลอดภัยของ URL หลักฐาน (เชิงบรรทัดฐาน) เมื่อมี evidence_url อยู่ ตัวตรวจสอบ (ฝั่ง compose, ฝั่งสาย, ขอบ Worker) ต้องบังคับใช้:

  1. HTTPS เท่านั้น สตริงต้องขึ้นต้นด้วยลำดับไบต์ https:// สคีมาอื่นใด — http, ftp, javascript, data, file ฯลฯ — จะถูกปฏิเสธ
  2. เพดานความยาว ≤ 2,048 ไบต์ (ขีดจำกัด URL เชิงปฏิบัติของเบราว์เซอร์)
  3. ตรวจ NFC + จุดรหัสที่เป็นอันตราย กฎเดียวกับ title และ reflection — จุดรหัส bidi-override / zero-width / tag-block / BOM / C0 / C1 จะถูกปฏิเสธ คำจำกัดความตรงกับ Rust crate::handle::contains_hostile_text_codepoint และ TS workers/api/src/utils/unicode.ts::isHostileCodepoint (รักษาให้สอดคล้องกัน)
  4. ไม่มีช่องว่าง ไม่มี ASCII control ช่องว่าง / DEL / ไบต์ต่ำกว่า 0x20 ที่ใดก็ตามใน URL จะถูกปฏิเสธ — ปิดช่องโหว่การฉีด \n/\t ที่กฎ bidi ไม่ครอบคลุม
  5. ส่วน host ต้องไม่ว่างเปล่า ทุกอย่างระหว่าง https:// กับ /, ?, หรือ # ตัวแรกต้องไม่ว่างเปล่า

ไม่มีการดึงข้อมูลฝั่งเซิร์ฟเวอร์ Worker ต้องไม่ proxy, fetch, หรือพรีวิว URL โปรโตคอลเก็บเพียงสตริง การเรนเดอร์เกิดขึ้นฝั่งผู้ชมด้วย rel="nofollow noopener noreferrer" target="_blank" และแสดงโฮสต์ที่มองเห็นได้ข้างข้อความลิงก์

การสะท้อนคิด ข้อความสะท้อนคิดที่ผู้สร้างเขียนไว้ตามทางเลือก ("อะไรเปลี่ยนไป คุณเรียนรู้อะไร") การตรวจสอบ NFC + จุดรหัสที่เป็นอันตรายเดียวกับ title อินพุตที่ว่างเปล่า / มีแต่ช่องว่างจะยุบเป็นไม่มีในเวลาก่อสร้าง

เวอร์ชันสคีมา 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 จำเป็นเชิงบรรทัดฐาน บริการอ้างอิงแนบแท็กเลือกได้สามรายการเพิ่มเติมเมื่อผู้สร้างเลือกที่จะแสดง: Intent (เจตนาในการแต่งที่ผ่านการตรวจสอบ allowlist เช่น quote, reply, commitment), Author (ลายนิ้วมือกุญแจสาธารณะ §9.3 ของผู้สร้างเป็นเลขฐานสิบหก 64 ตัวอักษรพิมพ์เล็ก) และ Parent-Tx-Id (ID ธุรกรรมการจัดเก็บของ qub แม่สำหรับสายตอบกลับ, base64url 43 ตัวอักษร)

แท็ก Author เป็น เลือกได้ต่อ qub: แอปผู้สร้างอ้างอิงแนบเฉพาะเมื่อผู้ใช้เปิดใช้งานการระบุที่มาสาธารณะอย่างชัดเจนในเวลาผนึก เมื่อสวิตช์ปิด — ค่าเริ่มต้น — ไม่มีการเขียนแท็ก Author และ qub ไม่มีการระบุที่มาบนเชน: ไม่มีอะไรในพื้นที่เก็บถาวรเชื่อมโยงการอัปโหลดเข้ากับ handle อีเมล หรือ qub อื่น ๆ ของผู้สร้าง เมื่อสวิตช์เปิด ลายนิ้วมือ Author จะแปลงเป็น @handle ที่ผู้สร้างเลือกผ่านสายการยืนยันใน §9.5 ความสัมพันธ์สายตอบกลับและ Intent ไม่ระบุตัวตน ตัวห่อหุ้มภายนอก (§13) ปกป้อง body ภายในจากการเชื่อมโยงไซเฟอร์เท็กซ์ — ป้องกันผู้เก็บเกี่ยวจากการรู้จักและถอดรหัสจำนวนมากของการอัปโหลดที่มีรูปร่าง qub หลังจากที่รอบ drand ของพวกเขาเผยแพร่

บริการอ้างอิงจงใจไม่แนบแท็ก App-Name, App-Version หรือ Type: ตัวกรองค่าเดียวใด ๆ จะส่งคืนคลังข้อมูล qub ทั้งหมดให้กับการสอบถาม GraphQL ซึ่งไม่สอดคล้องกับขอบเขตการรักษาความลับเฉพาะ body ของตัวห่อหุ้ม

ตัวตรวจสอบที่สอดคล้องต้องไม่พึ่งพาแท็กการจัดเก็บใด ๆ สำหรับการตรวจสอบของบุคคลที่สามตาม §11; body hash / 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 เหตุผล

qubs ถูกจัดเก็บในพื้นที่เก็บถาวร ลายเซ็นผู้เขียนต้องไม่สามารถปลอมแปลงได้อย่างไม่มีกำหนด ซึ่งเป็นเหตุผลที่ v1.0 ใช้รูปแบบหลังควอนตัม ML-DSA-65 (FIPS 204) แทนรูปแบบคลาสสิกที่ความปลอดภัยอาจเสื่อมลงภายในอายุการใช้งานถาวรของ qub

9.2 ทะเบียนอัลกอริทึม

sig_alg รูปแบบ ขนาดกุญแจ ขนาดลายเซ็น
0x00 ไม่มีลายเซ็น (ไม่ลงนาม)
0x01 ML-DSA-65 (FIPS 204) 1,952 ไบต์ 3,309 ไบต์

ผู้ชมต้องปฏิเสธค่า sig_alg ที่ไม่รู้จัก

9.3 การสร้างพรีอิมเมจที่ลงนาม

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V1"  ||    // domain separator (17 bytes)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 bytes)
    body_hash            ||    // [u8; 32] (32 bytes)
    unlock_at            ||    // i64 big-endian (8 bytes)
    0x00                       // u8 (1 byte): MUST be 0x00 in v1.0
)

// Total preimage: 91 bytes → 32-byte hash

signature = Sign(author_secret_key, sig_input)

ตัวแยกโดเมน: "QUB_AUTHOR_SIG_V1" คือ 17 ไบต์ ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31] ไม่มี padding

ไบต์ท้าย: ไบต์พรีอิมเมจที่ 91 ต้องเป็น 0x00 การใช้งานอ้างอิงเปิดเผยสิ่งนี้เป็นค่าคงที่ ORG_ID_PRESENT_INDIVIDUAL = 0x00 ใน crates/qub-core/src/signing.rs; ผู้ชมที่สร้าง sig_input ใหม่สำหรับการตรวจสอบต้องส่งไบต์เดียวกัน

ขอบเขตลายเซ็น — สิ่งที่ครอบคลุมและไม่ครอบคลุม sig_input ผูกพันกับสี่ฟิลด์ในซอง: version, qub_id, body_hash, unlock_at (บวกตัวแยกโดเมนคงที่และไบต์ org_id_present) สามในสี่นี้เป็นค่าคงที่เชิงโครงสร้าง: qub_id เองได้มาจาก version, content_type, created_at, unlock_at, outcome_at, drand_round และ body_hash ผ่านพรีอิมเมจ §4.1 ดังนั้นการเปลี่ยนแปลงฟิลด์เหล่านั้นใด ๆ จะสร้าง qub_id ที่แตกต่างกันและทำให้ลายเซ็นไม่ถูกต้องโดยอ้อม พื้นผิวที่รับรองความถูกต้องโดยตรงจึงเป็น:

ฟิลด์ ตรวจสอบโดยลายเซ็น อย่างไร
version อินพุตโดยตรงไปยัง sig_input
qub_id อินพุตโดยตรง
body_hash อินพุตโดยตรง
unlock_at อินพุตโดยตรง
content_type โดยอ้อมผ่านพรีอิมเมจ qub_id
created_at โดยอ้อมผ่านพรีอิมเมจ qub_id
outcome_at โดยอ้อมผ่านพรีอิมเมจ qub_id
drand_round โดยอ้อมผ่านพรีอิมเมจ qub_id (V1.2)
body โดยอ้อมผ่าน body_hash = SHA3-256(body)
author_pubkey — (โดยปริยาย) กุญแจที่ตรวจสอบลายเซ็นคือผู้เขียนตามนิยาม
sender_label ข้อความแสดงผลเท่านั้น; เปลี่ยนแปลงได้โดยไม่ทำให้ลายเซ็นเสีย
reply_to ตัวชี้สายข้อความ; เปลี่ยนแปลงได้โดยไม่ทำให้ลายเซ็นเสีย
cosigner_pubkey / cosigner_signature ลงนามอย่างอิสระบน sig_input เดียวกัน (ดู §9.7)
drand_chain_id, tlock_ciphertext, visibility ฟิลด์ภายนอก SealedQub ไม่อยู่ในซอง — ครอบคลุมโดยค่าคงที่เชิงโครงสร้างของตนเอง (ความสอดคล้องของรอบ / เชน) แต่ไม่ใช่โดยลายเซ็นผู้เขียน (drand_round ตอนนี้ถูกผูกโดยอ้อมผ่านพรีอิมเมจ qub_id — ดูด้านบน)

ผลกระทบด้านความปลอดภัยของฟิลด์ที่ไม่ผ่านการรับรอง

การใช้งานที่แสดง 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 (same formula as §9.3).
6. Verify(author_pubkey, sig_input, author_signature).
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."

การตรวจสอบลายเซ็นเป็นการดำเนินการที่มีค่าใช้จ่ายมากที่สุด (โดยเฉพาะ ML-DSA-65) ควรดำเนินการหลังจากที่การตรวจสอบที่ถูกกว่าทั้งหมด (แฮช, qub_id, unlock_at) ผ่านแล้ว

9.5 การยืนยันเอกลักษณ์

การยืนยันเอกลักษณ์ — การแมป author_pubkey กับการอ้างเอกลักษณ์ที่มนุษย์สามารถจดจำได้ เช่น qub handle ที่อยู่อีเมล social handle หรือข้อมูลรับรอง passkey — เป็น การปรับปรุงที่ก้าวหน้าฝั่งผู้ชม และ ไม่จำเป็น สำหรับการตรวจสอบลายเซ็น ผู้ชมที่แปลงการยืนยันเป็นเอกลักษณ์การแสดงผลต้องใช้ลำดับความสำคัญ:

handle > email > social > fingerprint

ค่าสำรองลายนิ้วมือคือเลขฐานสิบหกพิมพ์เล็กของ SHA3-256(author_pubkey); มีอยู่เสมอสำหรับ qub ที่ลงนามใด ๆ ผู้ชมอาจย่อค่านี้สำหรับการแสดงผล — ผู้ชมอ้างอิงเรนเดอร์ qub: ตามด้วยสี่ไบต์แรกและสี่ไบต์สุดท้าย (qub:<8 hex>…<8 hex>)

ตัวตรวจสอบที่สอดคล้องสามารถดำเนินการตรวจสอบทุกข้อใน §9.4 ได้อย่างสมบูรณ์โดยไม่ต้องติดต่อ API qub โดยไม่ต้องใช้เครือข่ายเกินกว่าพื้นที่เก็บถาวรและ drand และโดยไม่ต้องค้นหาฝั่งเซิร์ฟเวอร์ใด ๆ การแปลงการยืนยันเป็นขั้นตอนพยายามที่ดีที่สุดแยกต่างหากที่ดำเนินการเฉพาะหลังจากการตรวจสอบลายเซ็นสำเร็จแล้ว

9.6 ผลกระทบด้านขนาด

Ed25519 ML-DSA-65
ลายเซ็น 64 ไบต์ 3,309 ไบต์
กุญแจสาธารณะ 32 ไบต์ 1,952 ไบต์
รวมต่อ qub 96 ไบต์ 5,261 ไบต์
ค่าใช้จ่ายการจัดเก็บเพิ่ม (ที่ ~$5/MB) ~$0.0005 ~$0.026

สำหรับ qub ข้อความขนาด 500–2,000 ไบต์ ML-DSA-65 ทำให้ขนาดที่จัดเก็บเพิ่มขึ้นประมาณสามเท่า ค่าใช้จ่ายสัมบูรณ์ไม่มีนัยสำคัญ

9.7 การตรวจสอบผู้ลงนามร่วม (ข้อตกลงทวิภาคีของสัญญา)

สำหรับข้อตกลงทวิภาคี (content_type = 0x03) ชั้นลายเซ็นที่สองพิสูจน์ว่าทั้งสองฝ่ายยินยอมตามเงื่อนไขเดียวกัน

ฟิลด์ซอง:

ทั้งสองฟิลด์ต้องอยู่ร่วมกันหรือไม่อยู่ร่วมกัน หากมีฟิลด์เดียวอยู่ ผู้ชมต้องรายงานข้อผิดพลาดความสมบูรณ์

ขั้นตอนการตรวจสอบ:

1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
   Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3.
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."

คุณสมบัติ:

เกตการผูกอีเมล (การดำเนินงาน) เมื่อสัญญาที่จัดเตรียมไว้มีผู้ติดต่ออีเมลฝ่าย B (§6.1) บริการอัปโหลด qub ต้องปฏิเสธคำขอลงนามร่วมเว้นแต่จะมีเครื่องหมายยืนยันอีเมลอายุสั้นที่ตรงกับทั้ง staging id และแฮชอีเมลที่นอร์มัลไลซ์ของผู้ติดต่อนั้น เครื่องหมายนี้ถูกเขียนโดย /api/v1/auth/verify เมื่อโทเค็น magic-link มี staging_id และที่อยู่ที่ตรวจสอบแล้วตรงกับ SHA-256(normalise_email(party_b.contact)) — โดยที่ normalise_email(addr) รักษาตัวพิมพ์ของ local-part และทำให้ส่วนโดเมนเป็นตัวพิมพ์เล็กเท่านั้น (ตาม RFC 5321 §2.3.11) และ SHA-256 ที่นี่คือแฮช NIST FIPS 180-4 (แตกต่างจาก SHA3-256 ที่ใช้ในการหาค่า §4) — และหมดอายุ 900 วินาที (15 นาที) หลังจากออก นี่เป็นเกตป้องกันการปลอมตัวเชิงการดำเนินงาน ไม่ใช่ส่วนหนึ่งของหลักฐาน qub บนเชน — ตัวตรวจสอบของบุคคลที่สามที่เล่นซ้ำ §11 ต้องการเพียงพื้นที่เก็บถาวรและ drand โดยไม่ต้องค้นหาฝั่งเซิร์ฟเวอร์ใด ๆ เครื่องหมายมีอยู่เฉพาะฝั่งเซิร์ฟเวอร์เท่านั้นและไม่เคยเป็นส่วนหนึ่งของ body ที่ลงนาม

ผลกระทบด้านขนาด (ML-DSA-65 ผู้เขียน + ผู้ลงนามร่วม):

องค์ประกอบ ขนาด
ลายเซ็นผู้เขียน 3,309 ไบต์
กุญแจสาธารณะผู้เขียน 1,952 ไบต์
ลายเซ็นผู้ลงนามร่วม 3,309 ไบต์
กุญแจสาธารณะผู้ลงนามร่วม 1,952 ไบต์
ภาระการเข้ารหัสรวม 10,522 ไบต์
ค่าใช้จ่ายการจัดเก็บเพิ่ม ~$0.05

10. การเรนเดอร์ Markdown และการทำความสะอาด

ส่วนนี้สำคัญต่อความปลอดภัย ผู้ชมเรนเดอร์ qub ข้อความ (content_type = 0x01) โดยใช้ชุดย่อย Markdown ที่จำกัด

10.1 องค์ประกอบที่อนุญาต

10.2 องค์ประกอบที่ห้าม

องค์ประกอบ การจัดการ
HTML ดิบ (<div>, <script> เป็นต้น) ตัดออกทั้งหมด ไม่มี HTML ผ่านได้
รูปภาพ (![alt](url)) ตัดออก ไวยากรณ์รูปภาพถูกลบออกจากเอาต์พุต
ลิงก์ ([text](url)) URL เรนเดอร์เป็นข้อความธรรมดาที่มองเห็นได้ ไม่ลิงก์อัตโนมัติ คลิกไม่ได้โดยไม่มีการกระทำที่ชัดเจนของผู้ใช้
รูปแบบ URL ที่อันตราย javascript:, data:, vbscript:, file: — ตัดออก
Iframe, embed, object ตัดออก
HTML entity ถอดรหัสเป็นอักขระแสดงผลเฉพาะเมื่อปลอดภัย

10.3 การใช้งาน

การใช้งานต้องใช้ ตัวแยกวิเคราะห์ allowlist ที่เข้มงวด ไม่ใช่ blocklist แนวทางที่แนะนำ:

  1. แยกวิเคราะห์ Markdown โดยใช้ pulldown-cmark (หรือเทียบเท่า)
  2. เดิน AST และตัดโหนดใด ๆ ที่ไม่อยู่ใน allowlist (§10.1)
  3. สำหรับโหนดลิงก์: ส่ง URL เป็นข้อความที่มองเห็นได้ ไม่ใช่เป็นองค์ประกอบ <a> ที่คลิกได้
  4. แปลง AST ที่กรองแล้วเป็น การแทนกลางที่กำหนดประเภท (เช่น enum MarkdownNode ที่มีเฉพาะตัวแปรที่ปลอดภัย) HTML ดิบไม่สามารถแทนได้ในเชิงโครงสร้างใน IR นี้
  5. เรนเดอร์จาก IR ที่กำหนดประเภทไปยังชั้นมุมมองเป้าหมาย (เช่น คอมโพเนนต์มุมมองแบบรีแอกทีฟ โหนด DOM) ไม่มีการต่อสตริง HTML หรือ innerHTML ในจุดใดก็ตาม

แนวทาง blocklist เปราะบางเนื่องจากส่วนขยาย Markdown ใหม่หรือลักษณะเฉพาะของตัวแยกวิเคราะห์อาจแนะนำองค์ประกอบที่ไม่ได้กรอง แนวทาง AST ที่กำหนดประเภททำให้ XSS เป็นไปไม่ได้ในเชิงโครงสร้าง — ไม่มีตัวแปรใดที่สามารถถือ HTML ตามอำเภอใจได้

10.4 ข้อจำกัดด้านขนาดและโครงสร้าง


11. การตรวจสอบของบุคคลที่สาม

บุคคลที่สามใด ๆ สามารถตรวจสอบ qub สาธารณะได้โดยไม่ต้องร่วมมือกับ qub ขั้นตอนการตรวจสอบ:

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

สิ่งที่การตรวจสอบพิสูจน์:

หลักฐาน สิ่งที่ยืนยัน
ข้อผูกพัน ไซเฟอร์เท็กซ์มีอยู่ภายในไทม์สแตมป์ของบล็อกการจัดเก็บ
ความสมบูรณ์ body ข้อความธรรมดาตรงกับแฮชที่ผูกพันและไม่ถูกแก้ไข
เวลา เนื้อหาไม่สามารถอ่านได้จนถึงรอบ drand ซึ่งตรงกับเวลาปลดล็อกที่เลือก (ขึ้นอยู่กับสมมติฐานด้านความปลอดภัยของ tlock และ drand)

สิ่งที่การตรวจสอบไม่พิสูจน์:

ไม่ใช่หลักฐาน เหตุผล
ผู้เขียน sender_label เป็นการตกแต่ง หากไม่มี sig_alg0x01 ใคร ๆ ก็สามารถผนึกเนื้อหานี้ได้
เจตนา qub พิสูจน์เนื้อหาและเวลา ไม่ใช่สิ่งที่ผู้สร้างหมายถึงในเชิงอัตวิสัย
เวลาก่อนเหตุการณ์ การรวมบล็อกการจัดเก็บอาจล่าช้ากว่าการอัปโหลดจริงเป็นนาที ไทม์สแตมป์ข้อผูกพันคือเวลาบล็อก ไม่ใช่ขณะที่ผู้ใช้กด "ผนึก"

12. การกำหนดเวอร์ชัน

12.1 เวอร์ชันโปรโตคอล

ฟิลด์ version (u8) ใน SealedQub และ QubEnvelope ระบุเวอร์ชันโปรโตคอลหลัก

12.2 ประวัติเวอร์ชัน

เวอร์ชัน ค่า คำอธิบาย
v1 0x01 qub ข้อความสาธารณะ (content_type 0x01) ข้อตกลงทวิภาคีของสัญญา (0x03, สคีมา structured/v1, ML-DSA-65 ผู้เขียน + ผู้ลงนามร่วม), tlock, SHA3-256

12.3 ความเข้ากันได้ในอนาคต

ผู้ชม v1 ที่พบ QubEnvelope ที่มีกุญแจ CBOR map ที่ไม่รู้จัก (กุญแจที่ไม่อยู่ในลำดับมาตรฐาน §3.2) ควรละเว้นกุญแจเหล่านั้นและดำเนินการตรวจสอบโดยใช้ฟิลด์ที่รู้จัก สิ่งนี้อนุญาตการเพิ่มเล็กน้อยในอนาคต (เช่น เมตาดาต้าใหม่) โดยไม่ต้องอัปเกรดเวอร์ชันหลัก

ผู้ชม v1 ที่พบ sig_alg = 0x01 (ML-DSA-65) แต่ขาดการสนับสนุนการตรวจสอบ ML-DSA-65 ควรแสดงเนื้อหา qub พร้อมประกาศ "มีลายเซ็นแต่ไม่สามารถตรวจสอบได้" ไม่ใช่ปฏิเสธ qub ทั้งหมด การใช้งานอ้างอิงในปัจจุบันปฏิเสธค่า sig_alg ทุกค่ายกเว้น 0x00 และ 0x01 เนื่องจากทะเบียน v1 ไม่มีอัลกอริทึมที่ถูกต้องอื่น — การปฏิเสธอย่างเข้มงวดและการล้มเหลวอย่างนุ่มนวลเหมือนกันในเชิงสังเกตจนกว่าจะมีการลงทะเบียนอัลกอริทึมที่สาม พฤติกรรมการล้มเหลวอย่างนุ่มนวลด้านบนกลายเป็นสำคัญเมื่อ §9.2 ยอมรับรายการใหม่ และผู้ชมอ้างอิงจะถูกอัปเดตให้ล้มเหลวอย่างนุ่มนวลในจุดนั้น

12.4 เวอร์ชันห่อหุ้มภายนอก

OuterWrapper ที่อธิบายไว้ใน §13 มีไบต์ version ของตัวเอง เป็นอิสระ จาก SealedQub.version และ QubEnvelope.version พื้นที่เวอร์ชันสองอย่างวิวัฒนาการแยกกัน: การแทนที่สมมาตรที่ปลอดภัยหลังควอนตัมในอนาคตจะเพิ่มไบต์ตัวห่อหุ้มโดยไม่แตะเวอร์ชันโปรโตคอลภายใน และการเพิ่มชั้นโปรโตคอลในอนาคต (เช่น ฟิลด์ซองใหม่) จะเพิ่มเวอร์ชันภายในโดยไม่แตะไบต์ตัวห่อหุ้ม

OUTER_WRAPPER_VERSION_* ค่า อัลกอริทึม สถานะ
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM พร้อม nonce 12 ไบต์ แท็กรับรองความถูกต้อง 16 ไบต์ AAD ผูกกับ qub_id ค่าเริ่มต้น v1
0x020xFF สงวน อนาคต

ผู้ชมต้องปฏิเสธเวอร์ชันตัวห่อหุ้มที่ไม่รู้จักด้วยข้อผิดพลาดที่ชัดเจน โปรโตคอลจงใจรักษาพื้นที่เวอร์ชันตัวห่อหุ้มให้แคบจนกว่าจะมีแรงผลักดันการย้ายที่ชัดเจนปรากฏ (เช่น คำแนะนำของ NIST ที่สนับสนุน AEAD ที่แตกต่างกัน); ช่อง 0x02 จะถูกจัดสรรในการแก้ไขเดียวกันที่นำเสนออัลกอริทึม


13. ตัวห่อหุ้มการเข้ารหัสภายนอก

13.1 เหตุผล

ชั้นโปรโตคอล (QubEnvelope → tlock → SealedQub) ทำให้ qub ที่ผนึกแล้ว ล็อกเวลา: body ไม่สามารถอ่านได้จนถึง unlock_at และลายเซ็นรอบ drand ได้รับการเผยแพร่ อย่างไรก็ตามหลังจากปลดล็อก ลายเซ็นรอบเป็นสาธารณะและรูปร่าง CBOR มาตรฐานของ SealedQub สามารถจดจำได้ ดังนั้นผู้เก็บเกี่ยวที่จัดทำดัชนีธุรกรรมพื้นที่เก็บถาวรจึงสามารถถอดรหัสคลังข้อมูล qub ทั้งหมดเป็นจำนวนมากได้

ตัวห่อหุ้มการเข้ารหัสภายนอกปิดช่องนั้นโดยแทรกชั้น AEAD แบบสมมาตรเพิ่มเติมระหว่าง SealedQubCbor มาตรฐานและไบต์ที่เขียนลงพื้นที่เก็บถาวร กุญแจ 256 บิต K อยู่ เฉพาะ ใน URL fragment ของลิงก์ส่งและบนอุปกรณ์ผู้ใช้; เบราว์เซอร์ไม่ส่ง URL fragment ไปยังเซิร์ฟเวอร์ ดังนั้น qub.social, ทุกเกตเวย์การจัดเก็บ และทุก CDN ที่อยู่หน้าทั้งสองจึงมองไม่เห็น K ในเชิงสังเกต ดังนั้นทุก qub ในพื้นที่เก็บถาวรจึงเป็นไซเฟอร์เท็กซ์ทึบแสงซึ่งข้อความธรรมดาไม่สามารถกู้คืนได้โดยไม่มี URL ที่ผู้สร้างเลือกที่จะแชร์

ผลสุทธิ:

13.2 การจัดชั้น

plaintext body                       ← QubEnvelope.body (§2.2)
  ↓ canonical CBOR (§3)
envelope CBOR
  ↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
  ↓ canonical CBOR (§3)
SealedQubCbor bytes                  ← inner wire artifact
  ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12a, this section)
OuterWrapper CBOR bytes              ← uploaded to permanent storage (§7 step 15)

การผนึกและปลดล็อกที่ชั้นโปรโตคอล (§7, §8) ไม่เปลี่ยนแปลงด้านล่างขอบเขตของตัวห่อหุ้ม; ตัวห่อหุ้มแนบที่จุดเรียกของ seal() และถอดออกที่จุดเรียกของ unlock()

13.3 โครงสร้างข้อมูล OuterWrapper

struct OuterWrapper {
    version:    u8,           // 0x01, see §12.4
    qub_id:     [u8; 32],     // copied from inner SealedQub; AEAD AAD
    nonce:      [u8; 12],     // 96-bit AEAD nonce
    ciphertext: Vec<u8>,      // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}

ค่าคงที่ของฟิลด์

การเข้ารหัส CBOR CBOR มาตรฐานตาม §3 ด้วยกฎการเรียงลำดับกุญแจเดียวกัน (เรียงตามความยาวไบต์ที่เข้ารหัสจากน้อยไปมาก แล้วเรียงตามพจนานุกรม) กุญแจทั้งสี่คือ:

กุญแจ ไบต์ที่เข้ารหัส ลำดับ
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

ดังนั้นไบต์แรกของ CBOR OuterWrapper คือเฮดเดอร์ map ความยาวกำหนดได้สำหรับ map 4 รายการ (0xA4)

13.4 การผูก AAD เข้ากับ qub_id

ตัวห่อหุ้มผูก qub_id เป็นข้อมูลรับรองความถูกต้องเพิ่มเติมของ AEAD นี่คือการป้องกันเชิงโครงสร้างที่สำคัญต่อการโจมตีสามประเภท:

การโจมตี การป้องกัน
ย้ายไซเฟอร์เท็กซ์ภายใต้ฟิลด์ qub_id ที่แตกต่างในตัวห่อหุ้ม AAD ไม่ตรงกัน → การรับรองความถูกต้อง AEAD ล้มเหลว
ผสม URL fragment ของ qub A กับไบต์พื้นที่เก็บถาวรของ qub B AAD ไม่ตรงกัน → การรับรองความถูกต้อง AEAD ล้มเหลว
ดัดแปลงฟิลด์ qub_id ของตัวห่อหุ้มหลังการอัปโหลด AAD ไม่ตรงกัน → การรับรองความถูกต้อง AEAD ล้มเหลว

การพก qub_id ในข้อความธรรมดาของตัวห่อหุ้มไม่ลดภูมิคุ้มกันการระบุอย่างมีนัยสำคัญ — qub_id เองเป็นแฮช SHA3-256 ของพรีอิมเมจ §4.1 โดยไม่มีพรีอิมเมจที่สามารถกู้คืนจากไดเจสต์ได้ และผู้ระบุที่เก็บเกี่ยวไบต์ตัวห่อหุ้มไว้แล้วจะไม่เรียนรู้อะไรจาก 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 ผิด nonce ผิด AAD ไม่ตรงกัน และไซเฟอร์เท็กซ์ที่ถูกดัดแปลง ทั้งหมดสร้างข้อผิดพลาด DECRYPT_FAILED เดียวกัน นี่เป็นคุณสมบัติ AEAD โดยเจตนา: การแยกแยะโหมดล้มเหลวจะสร้างช่องด้านข้างที่ผู้โจมตีระยะไกลสามารถสำรวจได้โดยส่งตัวห่อหุ้มที่ผิดรูปแบบและจับเวลาการตอบสนอง การใช้งานอ้างอิงต้องยุบความล้มเหลว AEAD ทั้งหมดเป็นรูปแบบข้อผิดพลาดเดียว

13.6 วัสดุกุญแจและการแจกจ่าย

กุญแจห่อหุ้ม K เป็นค่าสุ่มสม่ำเสมอ 256 บิตที่สร้างต่อ qub โดย CSPRNG การใช้งานอ้างอิงดึงจาก:

การแจกจ่าย: K ต้องถูกเข้ารหัสเป็น base64 ที่ปลอดภัยสำหรับ URL (RFC 4648 §5 ไม่มี padding) และต่อท้ายลิงก์ส่งเป็นองค์ประกอบ fragment:

delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>

Fragment ไม่ถูกส่งไปยังเซิร์ฟเวอร์ใด ๆ โดยเบราว์เซอร์ที่สอดคล้อง ช่องทางการกู้คืน (ดัชนีประวัติฝั่งเซิร์ฟเวอร์ การส่งอีเมลอัตโนมัติแบบเลือกได้) ที่คงลิงก์ส่งทั้งหมด — รวมถึง fragment — เกินกว่าอุปกรณ์ของผู้ใช้เป็นการแลกเปลี่ยนที่ชัดเจนกับท่าทีทำลายการเข้ารหัสเริ่มต้นและต้องมีการยินยอมที่ชัดเจนของผู้ใช้

การสูญหายของ Fragment หากผู้ใช้สูญเสีย URL fragment และไม่มีช่องทางการกู้คืน qub ไม่สามารถอ่านได้ นี่คือการแลกเปลี่ยนที่สำคัญของการออกแบบและต้องเปิดเผยให้ผู้ใช้ทราบในเวลาผนึก MVP เสริมการเปิดเผยในเวลาผนึกด้วยข้อความ "บันทึก URL นี้" ที่ชัดเจนและช่องทางการกู้คืนอีเมลที่ตรวจสอบแล้วสำหรับผู้ใช้ที่เลือกรับ

13.7 นอกขอบเขตของส่วนนี้

13.8 qub สาธารณะ (การละเว้นตัวห่อหุ้ม)

ตัวห่อหุ้มภายนอกเป็น ตัวเลือกที่ชั้นการส่ง ผู้สร้างอาจผนึก qub เป็นแบบ สาธารณะ ซึ่งในกรณีนี้ SealedQubCbor มาตรฐานจะถูกเขียนลงพื้นที่เก็บถาวร โดยตรง โดยไม่มีชั้น OuterWrapper และไม่มีกุญแจ K:

SealedQubCbor bytes  ──(public)──▶  uploaded to permanent storage as-is
SealedQubCbor bytes  ──(private)─▶  AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded

qub สาธารณะเป็นแบบ ล็อกเวลาแต่ไม่จำกัดด้วยลิงก์: ยังคงอ่านไม่ได้จนกว่ารอบ drand จะเผยแพร่ (ชั้น tlock ไม่เปลี่ยนแปลง) แต่หลังปลดล็อก ใครก็ตามที่มี arweave_tx_id สามารถถอดรหัสได้ — ไม่ต้องใช้ URL fragment เพราะไม่มี K นี่คือการแลกเปลี่ยนโดยเจตนาสำหรับพื้นผิวที่เซิร์ฟเวอร์ต้องขับเคลื่อน: อีเมลแจ้งเตือนการเปิดเผย, การฝังจากบุคคลที่สาม และ SEO หลังการเปิดเผยที่สมบูรณ์ยิ่งขึ้น ล้วนต้องการลิงก์ที่ใช้งานได้โดยไม่มีความลับที่เซิร์ฟเวอร์ไม่เคยถือครอง (§13.6)

ผลที่ผู้ผลิตต้องคำนึงถึง:

ส่วนตัว (ห่อหุ้ม) ยังคงเป็นค่าเริ่มต้น; สาธารณะเป็นทางเลือกของผู้สร้างต่อ qub อย่างชัดเจน


14. เวกเตอร์ทดสอบ

14.1 การหาค่า qub_id

Input:
  version      = 0x01
  content_type = 0x01
  created_at   = 1735689600 (2025-01-01 00:00:00 UTC)
  unlock_at    = 1736294400 (2025-01-08 00:00:00 UTC)
  outcome_at   = absent
  drand_round  = 4695445  (= (1736294400 - 1595431050) / 30, drand mainnet params §14.2)
  body         = "Hello, future."  (UTF-8, 14 bytes)
  title        = absent

Intermediate:
  body_hash  = SHA3-256("Hello, future.")
             = 76ab8b3f843c6ed4f2d0fd75b9f457b4
               ad49dd4450f9c22723ae430e3af3211d
  title_hash = [0u8; 32]   (title absent — §4.2.1 sentinel)

Domain separator (10 bytes):
  [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]

Preimage (108 bytes — V1.2):
  domain_separator   ||  // 10 bytes
  0x01               ||  // version
  0x01               ||  // content_type
  0x0000000067748580 ||  // created_at as i64 big-endian (1735689600)
  0x00000000677DC000 ||  // unlock_at as i64 big-endian (1736294400)
  0x0000000000000000 ||  // outcome_at_or_zero (outcome_at absent)
  0x000000000047A595 ||  // drand_round as u64 big-endian (4695445)
  body_hash          ||  // 32 bytes
  title_hash             // 32 bytes (all-zeros sentinel; title absent)

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

การใช้งานต้องสร้างค่า body_hash และ qub_id ที่เหมือนกันสำหรับอินพุตนี้ เวกเตอร์ทดสอบนี้ควรเป็น unit test แรกที่เขียน ค่ามาตรฐานด้านบนถูกคำนวณโดยการใช้งานอ้างอิงและต้องตรงกันแบบบิตต่อบิต เลย์เอาต์พรีอิมเมจในอดีต (ก่อนเปิดตัว — ไม่มี qub ที่ใช้งานจริงพึ่งพาค่าเหล่านี้): qub_id V1.0 แบบ 92 ไบต์ คือ 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; qub_id V1.1 แบบ 100 ไบต์ (หลังรวม outcome_at_or_zero) คือ b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed V1.2 รวม drand_round เข้าไปและเลื่อนตัวแยกโดเมนเป็น QUB_ID_V2

14.2 การแมปรอบปลดล็อก

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

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

drand_round = 4675285

14.3 การไปกลับของ CBOR มาตรฐาน

การใช้งานต้องตรวจสอบว่า serialize(parse(serialize(qub))) == serialize(qub) สำหรับอินพุตที่ถูกต้องทั้งหมด นี่คือการทดสอบคุณสมบัติ ไม่ใช่เวกเตอร์เดียว

14.4 CBOR ของ PactTerms (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 ที่เป็นไบต์เหมือนกันสำหรับอินพุตนี้

การใช้งานต้องตรวจสอบด้วยว่า serialize(parse(serialize(pact))) == serialize(pact) สำหรับอินพุต PactTerms ที่ถูกต้องทั้งหมด (การทดสอบคุณสมบัติ)

14.5 เวกเตอร์ข้ามภาษาของตัวห่อหุ้มภายนอก

ตัวห่อหุ้มภายนอก (§13) มี fixture มาตรฐานแยกที่ crates/qub-core/tests/vectors/wrapper_v1.json แต่ละกรณีกำหนด tuple (key, nonce, qub_id, sealed_cbor) เป็นอินพุตเลขฐานสิบหกทึบแสงและยืนยันเอาต์พุต expected_wrapper_hex ที่เฉพาะเจาะจง การใช้งานอ้างอิงทั้งสองบริโภคไฟล์ JSON เดียวกัน:

ปัจจุบัน fixture ตรึงสามกรณี:

กรณี การครอบคลุม
basic-text-public รูปร่าง SealedQub ที่สมจริงน้อยที่สุด; ไม่มีฟิลด์ที่เลือกได้ สร้างรูปร่างตัวห่อหุ้มมาตรฐานสำหรับ qub v1.0 ทั่วไป
with-recipient-pubkey SealedQub ที่ตั้งค่า recipient_pubkey (เส้นทาง Phase 2) ชุดกุญแจ CBOR ภายในที่แตกต่าง qub_id ที่แตกต่าง
longer-body body ~4 KiB — ออกกำลังกายคำนำหน้าความยาว CBOR หลายไบต์ทั้งในซองภายในและไซเฟอร์เท็กซ์ภายนอก

การใช้งานต้องสร้าง expected_wrapper_hex ที่เป็นไบต์เหมือนกันสำหรับอินพุตที่บันทึกไว้ การสร้าง fixture ใหม่ต้องใช้ QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors และสงวนไว้สำหรับการเปลี่ยนแปลงรูปแบบโดยเจตนา


15. การกำกับดูแลโปรไฟล์การเข้ารหัส (อนาคต)

ส่วนนี้เป็นข้อมูลสำหรับ v1 และกลายเป็นเชิงบรรทัดฐานในครั้งแรกที่อัลกอริทึมที่สองเข้าสู่หนึ่งในองค์ประกอบพื้นฐานทางการเข้ารหัสของ qub

15.1 ท่าทีปัจจุบัน

โปรโตคอล v1 ผูกอัลกอริทึมหนึ่งตัวต่อหนึ่งองค์ประกอบพื้นฐานพอดี:

ตัวตรวจสอบในปัจจุบัน hard-code ความยาวกุญแจและลายเซ็นต่อองค์ประกอบพื้นฐาน ไม่มีพื้นผิวความคล่องตัวเปิดเผยโดยรูปแบบการสื่อสาร

15.2 รูปร่างที่ตั้งใจ

เมื่ออัลกอริทึมที่สองเข้าสู่โปรโตคอล ตัวตรวจสอบจะถูกกำหนดค่าสำหรับ CryptoProfile ที่มีชื่อ (เช่น ExqubV1) ที่แสดงรายชุดค่าที่อนุญาตที่แม่นยำต่อองค์ประกอบพื้นฐาน — sig_algs, drand chain, เวอร์ชันตัวห่อหุ้ม, ประเภทเนื้อหา โปรไฟล์ถูกแก้ไขที่เวลาตรวจสอบ ไม่เคยถูกเจรจาในแบนด์ ค่าใดก็ตามที่อยู่นอกโปรไฟล์ที่ใช้งานจะถูกปฏิเสธ

สิ่งนี้รับประกันว่าการเพิ่ม ML-DSA-87 หรือการเปิดใช้งาน Ed25519 ไม่สามารถลดความเข้มแข็งของการตั้งค่าตัวตรวจสอบที่มีอยู่ย้อนหลังได้: ตัวตรวจสอบ v1 ยังคงเป็นตัวตรวจสอบ v1 แม้หลังจากที่โปรไฟล์ v2 ถูกเผยแพร่

15.3 เงื่อนไขการกระตุ้น

ยกระดับ §15 เป็นสถานะเชิงบรรทัดฐานเมื่อมีการเสนอข้อใดข้อหนึ่งต่อไปนี้:

จนกว่าจะถึงเวลานั้น §15 เป็นตัวยึดที่กำหนดรูปร่างการย้ายเพื่อให้ PR ในอนาคตลงบนเป้าหมายที่รู้จักแทนที่จะอภิปรายซ้ำเกี่ยวกับพื้นผิวการเจรจาตั้งแต่ต้น