ข้อกำหนดโปรโตคอล 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) ดังนั้นเวลาปลดล็อกที่แสดงจึงพิสูจน์ได้ว่าเป็นรอบที่ควบคุมการถอดรหัส
คุณสมบัติ:
- การเปลี่ยนแปลงฟิลด์ใด ๆ ใน QubEnvelope (body, ไทม์สแตมป์, ประเภทเนื้อหา, เวอร์ชัน) จะสร้าง qub_id ที่แตกต่างกัน
- qub_id ถูกคำนวณก่อนการเข้ารหัส ทั้ง QubEnvelope และ SealedQub มี qub_id เดียวกัน ผู้ชมตรวจสอบว่าตรงกันหลังจากการถอดรหัส
- qub_id ไม่ขึ้นกับ
sender_label,author_signatureหรือauthor_pubkeyซึ่งหมายความว่าเนื้อหาเดียวกันที่ผนึกในเวลาเดียวกันจะสร้าง qub_id เดียวกันโดยไม่คำนึงถึงผู้ลงนาม - การเปลี่ยน
titleของ SealedQub (โดยฟิลด์อื่นทั้งหมดคงที่) จะเปลี่ยนqub_idผ่านtitle_hashดังนั้นเกตเวย์จึงไม่สามารถสลับชื่อข้อความธรรมดาที่แสดงในการนับถอยหลังโดยไม่ทำให้เอกลักษณ์ qub เสียได้ - การเปลี่ยน
outcome_atของ SealedQub (โดยฟิลด์อื่นทั้งหมดคงที่) จะเปลี่ยนqub_idผ่านพรีอิมเมจ เกตเวย์ไม่สามารถสลับวันที่ verdict-on ก่อนการเปิดเผยที่แสดงในการนับถอยหลังโดยไม่ทำให้เอกลักษณ์ qub เสียได้ - การเปลี่ยน
drand_round(โดยฟิลด์อื่นทั้งหมดคงที่) จะเปลี่ยนqub_idผ่านพรีอิมเมจ เกตเวย์ไม่สามารถผูก timelock ciphertext ซ้ำเข้ากับรอบที่แตกต่างโดยไม่ทำให้เอกลักษณ์ qub เสียได้ เมื่อรวมกับการตรวจสอบ stanza-round เวลาปลดล็อกใน §8unlock_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) ต้องบังคับใช้:
- HTTPS เท่านั้น สตริงต้องขึ้นต้นด้วยลำดับไบต์
https://สคีมาอื่นใด —http,ftp,javascript,data,fileฯลฯ — จะถูกปฏิเสธ - เพดานความยาว ≤ 2,048 ไบต์ (ขีดจำกัด URL เชิงปฏิบัติของเบราว์เซอร์)
- ตรวจ NFC + จุดรหัสที่เป็นอันตราย กฎเดียวกับ
titleและreflection— จุดรหัส bidi-override / zero-width / tag-block / BOM / C0 / C1 จะถูกปฏิเสธ คำจำกัดความตรงกับ Rustcrate::handle::contains_hostile_text_codepointและ TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(รักษาให้สอดคล้องกัน) - ไม่มีช่องว่าง ไม่มี ASCII control ช่องว่าง / DEL / ไบต์ต่ำกว่า
0x20ที่ใดก็ตามใน URL จะถูกปฏิเสธ — ปิดช่องโหว่การฉีด\n/\tที่กฎ bidi ไม่ครอบคลุม - ส่วน 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("Alice" → "Mallory") โดยไม่ทำให้ลายเซ็นผู้เขียนไม่ถูกต้องauthor_pubkeyภายในซองยังคงเป็นจุดยึดเอกลักษณ์ที่แท้จริง — ผู้ชมต้องดึงเอกลักษณ์การแสดงผลจากauthor_pubkey(ผ่านชั้นการยืนยัน §9.5) แทนที่จะเชื่อsender_label - ฟิลด์
reply_toสามารถแก้ไขหลังการลงนามได้เช่นกัน เนื่องจากqub_idเป็นการระบุที่อยู่ตามเนื้อหา ผู้โจมตีไม่สามารถชี้reply_toไปยังเป้าหมายที่ไม่มีอยู่ได้ แต่พวกเขาสามารถเปลี่ยนพ่อแม่ของการตอบกลับไปยัง qub ที่มีอยู่อื่นได้อย่างเงียบ ๆ
การใช้งานที่แสดง 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) ชั้นลายเซ็นที่สองพิสูจน์ว่าทั้งสองฝ่ายยินยอมตามเงื่อนไขเดียวกัน
ฟิลด์ซอง:
cosigner_pubkey: กุญแจสาธารณะ ML-DSA-65 ของผู้ลงนามคู่สัญญา (ฝ่าย B)cosigner_signature: ลายเซ็นบนsig_inputเดียวกันกับผู้เขียน (§9.3)
ทั้งสองฟิลด์ต้องอยู่ร่วมกันหรือไม่อยู่ร่วมกัน หากมีฟิลด์เดียวอยู่ ผู้ชมต้องรายงานข้อผิดพลาดความสมบูรณ์
ขั้นตอนการตรวจสอบ:
1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3.
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เดียวกัน - การได้มาของ
qub_id(§4.1) ไม่รวมฟิลด์ผู้ลงนามร่วม การเพิ่มผู้ลงนามร่วมในซองที่มีอยู่ไม่เปลี่ยนqub_id - สัญญาสามารถลงนามโดยผู้เขียนเท่านั้น (ข้อผูกพันฝ่ายเดียว) ลงนามโดยผู้ลงนามร่วมเท่านั้น (ไม่ปกติ) หรือทั้งสอง (หลักฐานทวิภาคีเต็มรูปแบบ)
เกตการผูกอีเมล (การดำเนินงาน) เมื่อสัญญาที่จัดเตรียมไว้มีผู้ติดต่ออีเมลฝ่าย 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 องค์ประกอบที่อนุญาต
- หัวเรื่อง:
#ถึง####(ไม่มี#####หรือ######) - การเน้น: ตัวหนา (
**) ตัวเอียง (*) ขีดทับ (~~) - รายการ: เรียงลำดับ (
1.) และไม่เรียงลำดับ (-,*) - บล็อกอ้างอิง (
>) - โค้ด: ช่วงในบรรทัด (```) และบล็อกที่มีรั้ว (`````)
- เส้นแนวนอน (
---) - ตัวแบ่งบรรทัด (ช่องว่างปิดท้ายสองช่องหรือบรรทัดว่าง)
- ย่อหน้า
10.2 องค์ประกอบที่ห้าม
| องค์ประกอบ | การจัดการ |
|---|---|
HTML ดิบ (<div>, <script> เป็นต้น) |
ตัดออกทั้งหมด ไม่มี HTML ผ่านได้ |
รูปภาพ () |
ตัดออก ไวยากรณ์รูปภาพถูกลบออกจากเอาต์พุต |
ลิงก์ ([text](url)) |
URL เรนเดอร์เป็นข้อความธรรมดาที่มองเห็นได้ ไม่ลิงก์อัตโนมัติ คลิกไม่ได้โดยไม่มีการกระทำที่ชัดเจนของผู้ใช้ |
| รูปแบบ URL ที่อันตราย | javascript:, data:, vbscript:, file: — ตัดออก |
| Iframe, embed, object | ตัดออก |
| HTML entity | ถอดรหัสเป็นอักขระแสดงผลเฉพาะเมื่อปลอดภัย |
10.3 การใช้งาน
การใช้งานต้องใช้ ตัวแยกวิเคราะห์ allowlist ที่เข้มงวด ไม่ใช่ blocklist แนวทางที่แนะนำ:
- แยกวิเคราะห์ Markdown โดยใช้
pulldown-cmark(หรือเทียบเท่า) - เดิน AST และตัดโหนดใด ๆ ที่ไม่อยู่ใน allowlist (§10.1)
- สำหรับโหนดลิงก์: ส่ง URL เป็นข้อความที่มองเห็นได้ ไม่ใช่เป็นองค์ประกอบ
<a>ที่คลิกได้ - แปลง AST ที่กรองแล้วเป็น การแทนกลางที่กำหนดประเภท (เช่น enum
MarkdownNodeที่มีเฉพาะตัวแปรที่ปลอดภัย) HTML ดิบไม่สามารถแทนได้ในเชิงโครงสร้างใน IR นี้ - เรนเดอร์จาก IR ที่กำหนดประเภทไปยังชั้นมุมมองเป้าหมาย (เช่น คอมโพเนนต์มุมมองแบบรีแอกทีฟ โหนด DOM) ไม่มีการต่อสตริง HTML หรือ
innerHTMLในจุดใดก็ตาม
แนวทาง blocklist เปราะบางเนื่องจากส่วนขยาย Markdown ใหม่หรือลักษณะเฉพาะของตัวแยกวิเคราะห์อาจแนะนำองค์ประกอบที่ไม่ได้กรอง แนวทาง AST ที่กำหนดประเภททำให้ XSS เป็นไปไม่ได้ในเชิงโครงสร้าง — ไม่มีตัวแปรใดที่สามารถถือ HTML ตามอำเภอใจได้
10.4 ข้อจำกัดด้านขนาดและโครงสร้าง
- ความลึกหัวเรื่องที่เรนเดอร์สูงสุด:
####(H4)#####และลึกกว่าถูกเรนเดอร์เป็นข้อความตัวหนา - ไม่มีข้อจำกัดในจำนวนย่อหน้า (ข้อจำกัดขนาด body ใน §6 เป็นข้อจำกัด)
- บล็อกโค้ดที่มีรั้ว: ไม่มีการเน้นไวยากรณ์ใน MVP เรนเดอร์เป็นข้อความที่จัดรูปแบบไว้ล่วงหน้าแบบ monospace
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_alg ≥ 0x01 ใคร ๆ ก็สามารถผนึกเนื้อหานี้ได้ |
| เจตนา | qub พิสูจน์เนื้อหาและเวลา ไม่ใช่สิ่งที่ผู้สร้างหมายถึงในเชิงอัตวิสัย |
| เวลาก่อนเหตุการณ์ | การรวมบล็อกการจัดเก็บอาจล่าช้ากว่าการอัปโหลดจริงเป็นนาที ไทม์สแตมป์ข้อผูกพันคือเวลาบล็อก ไม่ใช่ขณะที่ผู้ใช้กด "ผนึก" |
12. การกำหนดเวอร์ชัน
12.1 เวอร์ชันโปรโตคอล
ฟิลด์ version (u8) ใน SealedQub และ QubEnvelope ระบุเวอร์ชันโปรโตคอลหลัก
- ผู้ชมต้องปฏิเสธเวอร์ชันหลักที่ไม่รู้จักด้วยข้อผิดพลาดที่ชัดเจน
- เวอร์ชันหลักที่รู้จักอาจยอมรับฟิลด์เลือกได้ที่ไม่รู้จักหากกฎความเข้ากันได้ในอนาคตอนุญาต (ฟิลด์เลือกได้ที่ไม่มีในลำดับกุญแจมาตรฐานถูกละเว้น)
- ประเภทเนื้อหา (
content_type) และรูปแบบลายเซ็น (sig_alg) ถูกควบคุมโดยเวอร์ชัน: ค่าใหม่อาจถูกนำเสนอควบคู่กับเวอร์ชันโปรโตคอลใหม่หรือการอัปเดตทะเบียนที่ชัดเจนเท่านั้น
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 |
| — | 0x02–0xFF |
สงวน | อนาคต |
ผู้ชมต้องปฏิเสธเวอร์ชันตัวห่อหุ้มที่ไม่รู้จักด้วยข้อผิดพลาดที่ชัดเจน โปรโตคอลจงใจรักษาพื้นที่เวอร์ชันตัวห่อหุ้มให้แคบจนกว่าจะมีแรงผลักดันการย้ายที่ชัดเจนปรากฏ (เช่น คำแนะนำของ 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 ที่ผู้สร้างเลือกที่จะแชร์
ผลสุทธิ:
- ภูมิคุ้มกันการระบุค่าเริ่มต้น ไบต์ที่ห่อหุ้มในพื้นที่เก็บถาวรไม่สามารถแยกแยะได้ในเชิงไบต์จากไซเฟอร์เท็กซ์ตามอำเภอใจ กลยุทธ์ผู้เก็บเกี่ยวของ "การสอบถาม GraphQL สำหรับการอัปโหลดที่มีรูปร่าง qub ถอดรหัสจำนวนมากด้วยลายเซ็น drand สาธารณะ" ไม่จบลงด้วยข้อความธรรมดา
- ท่าทีความเป็นส่วนตัวแบบทำลายการเข้ารหัส qub.social ไม่สามารถถอดรหัสคลังข้อมูลของตนเองได้อย่างแท้จริง การส่งหมายเรียกถึงไซเฟอร์เท็กซ์ ไม่ใช่ข้อความธรรมดา
- บันไดความลับสองชั้น ค่าเริ่มต้น = การเข้าถึงที่ควบคุมโดยลิงก์ (ส่วนนี้) qub ส่วนตัวที่เข้ารหัสผู้รับ (ฟีเจอร์ที่สงวนไว้สำหรับระยะที่ 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ต้องเท่ากับ0x01สำหรับไบต์ตัวห่อหุ้ม v1.0qub_idต้องเท่ากับฟิลด์qub_idของ SealedQub ที่กู้คืนหลังการแกะ ขั้นตอนแกะไม่บังคับใช้สิ่งนี้โดยตรง (การผูก AEAD AAD ทำให้การดัดแปลงระดับไบต์เป็นไปไม่ได้) แต่ชั้นปลดล็อกตรวจสอบความสัมพันธ์โดยอ้อม: หากผู้สร้างห่อหุ้มSealedQubCborที่qub_idภายในไม่ตรงกับqub_idของตัวห่อหุ้ม §8 ขั้นตอน 11 จะล้มเหลวnonceต้องเป็น 96 บิต (12 ไบต์) สร้างใหม่โดย CSPRNG สำหรับการดำเนินการห่อทุกครั้ง การใช้ nonce ซ้ำภายใต้กุญแจเดียวกันอนุญาตการโจมตี AEAD nonce-reuse ที่กู้คืนข้อความธรรมดา; ผู้ผลิตต้องถือว่าคู่ (key,nonce) เป็นแบบยิงครั้งเดียวciphertextคือเอาต์พุต AES-256-GCM: ไบต์ไซเฟอร์เท็กซ์ต่อกันกับแท็กรับรองความถูกต้อง 16 ไบต์ciphertext.len() == SealedQubCbor.len() + 16พอดี
การเข้ารหัส 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 การใช้งานอ้างอิงดึงจาก:
- ผู้สร้าง WASM:
getrandom(WebCrypto ภายใต้แบ็กเอนด์wasm_js) - เส้นทางผนึกฝั่งเซิร์ฟเวอร์ Worker:
crypto.getRandomValues
การแจกจ่าย: 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 นอกขอบเขตของส่วนนี้
- การลงนามผู้เขียน (§9) ไม่เปลี่ยนแปลง: ลายเซ็นถูกคำนวณภายใน
QubEnvelopeภายในและกู้คืนหลังจากแกะ → ถอดรหัส tlock → แยกวิเคราะห์ CBOR - qub ส่วนตัวที่เข้ารหัสผู้รับ (ฟีเจอร์ที่สงวนไว้สำหรับระยะที่ 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 สามารถถอดรหัสได้ — ไม่ต้องใช้ URL fragment เพราะไม่มี K นี่คือการแลกเปลี่ยนโดยเจตนาสำหรับพื้นผิวที่เซิร์ฟเวอร์ต้องขับเคลื่อน: อีเมลแจ้งเตือนการเปิดเผย, การฝังจากบุคคลที่สาม และ SEO หลังการเปิดเผยที่สมบูรณ์ยิ่งขึ้น ล้วนต้องการลิงก์ที่ใช้งานได้โดยไม่มีความลับที่เซิร์ฟเวอร์ไม่เคยถือครอง (§13.6)
ผลที่ผู้ผลิตต้องคำนึงถึง:
- ไม่มีภูมิคุ้มกันการระบุ qub สาธารณะสละคุณสมบัติภูมิคุ้มกันการระบุของ §13.1 โดยโครงสร้าง บริการอัปโหลดอ้างอิงประทับแท็กพื้นที่เก็บถาวร
Visibility: publicลงบนพวกมัน (และเฉพาะพวกมัน) เพื่อให้สามารถค้นพบได้โดยเจตนา; qub ส่วนตัวไม่มีแท็กดังกล่าวและคงความไม่สามารถแยกแยะได้ในเชิงไบต์ไว้ - title ข้อความธรรมดาถูกเปิดเผยในเวลาผนึก ฟิลด์
titleของ §3.2 เป็นข้อความธรรมดาภายในSealedQubCborภายใต้ตัวห่อหุ้มมันถูกซ่อนไว้จนกว่าผู้ชมจะให้K; โดยไม่มีตัวห่อหุ้มมันอ่านได้ทั่วโลกบนพื้นที่เก็บถาวร ตั้งแต่ขณะอัปโหลด ก่อนการปลดล็อก แอปผู้สร้างที่สอดคล้องต้องเปิดเผยสิ่งนี้ในเวลาผนึก - การตรวจจับเป็นเชิงโครงสร้าง ผู้ชม/การฝังที่สอดคล้องแยกแยะรูปร่างทั้งสองด้วยการแยกวิเคราะห์: ไบต์ที่แยกวิเคราะห์เป็น
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 ที่เหมือนกันสำหรับอินพุตนี้ เวกเตอร์ทดสอบนี้ควรเป็น 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 เดียวกัน:
- 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)
ปัจจุบัน 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 ผูกอัลกอริทึมหนึ่งตัวต่อหนึ่งองค์ประกอบพื้นฐานพอดี:
- ลายเซ็น: ML-DSA-65 (
sig_alg = 0x01; กุญแจสาธารณะ 1952 ไบต์ ลายเซ็น 3309 ไบต์) และไม่ลงนาม (sig_alg = 0x00) ทะเบียน §9.2 ไม่ได้กำหนดค่าอื่นใด; ตัวตรวจสอบ v1 ต้องปฏิเสธทุกsig_algที่อยู่นอก{0x00, 0x01}รายการ Ed25519 ในอนาคตได้รับการคาดการณ์ไว้ (§15.3) แต่ยังไม่ได้จัดสรรใน v1 - ล็อกเวลา: drand quicknet เท่านั้น — chain hash, กุญแจสาธารณะ, เวลา genesis และคาบเวลาเป็นพารามิเตอร์เครือข่ายคงที่ที่ถูกพกพาโดย
DrandTimelockProvider::quicknet()อ้างอิง (crates/qub-core/src/tlock.rs) และconfig/drand-endpoints.json - ตัวห่อหุ้มภายนอก: AES-256-GCM v1 เท่านั้น (§13)
ตัวตรวจสอบในปัจจุบัน hard-code ความยาวกุญแจและลายเซ็นต่อองค์ประกอบพื้นฐาน ไม่มีพื้นผิวความคล่องตัวเปิดเผยโดยรูปแบบการสื่อสาร
15.2 รูปร่างที่ตั้งใจ
เมื่ออัลกอริทึมที่สองเข้าสู่โปรโตคอล ตัวตรวจสอบจะถูกกำหนดค่าสำหรับ CryptoProfile ที่มีชื่อ (เช่น ExqubV1) ที่แสดงรายชุดค่าที่อนุญาตที่แม่นยำต่อองค์ประกอบพื้นฐาน — sig_algs, drand chain, เวอร์ชันตัวห่อหุ้ม, ประเภทเนื้อหา โปรไฟล์ถูกแก้ไขที่เวลาตรวจสอบ ไม่เคยถูกเจรจาในแบนด์ ค่าใดก็ตามที่อยู่นอกโปรไฟล์ที่ใช้งานจะถูกปฏิเสธ
สิ่งนี้รับประกันว่าการเพิ่ม ML-DSA-87 หรือการเปิดใช้งาน Ed25519 ไม่สามารถลดความเข้มแข็งของการตั้งค่าตัวตรวจสอบที่มีอยู่ย้อนหลังได้: ตัวตรวจสอบ v1 ยังคงเป็นตัวตรวจสอบ v1 แม้หลังจากที่โปรไฟล์ v2 ถูกเผยแพร่
15.3 เงื่อนไขการกระตุ้น
ยกระดับ §15 เป็นสถานะเชิงบรรทัดฐานเมื่อมีการเสนอข้อใดข้อหนึ่งต่อไปนี้:
- ไบต์
sig_algที่สอง (การเปิดใช้งาน Ed25519, ML-DSA-87 หรือรายการใหม่ใด ๆ ในทะเบียน §9) - drand chain ที่สองในการใช้งานจริง
- เวอร์ชันตัวห่อหุ้มภายนอกที่สอง
จนกว่าจะถึงเวลานั้น §15 เป็นตัวยึดที่กำหนดรูปร่างการย้ายเพื่อให้ PR ในอนาคตลงบนเป้าหมายที่รู้จักแทนที่จะอภิปรายซ้ำเกี่ยวกับพื้นผิวการเจรจาตั้งแต่ต้น