مواصفات بروتوكول qub

qub هو بروتوكول للالتزامات الزمنية التشفيرية: نظام لختم الكلمات حتى تاريخ مستقبلي وإثبات ما قيل بالضبط ومتى عند حلول ذلك التاريخ.

تجعل ثلاث عناصر أولية هذا النظام يعمل. drand هو منارة عشوائية لامركزية — تاريخ الكشف قابل للإنفاذ بقوانين الفيزياء، لا بحسن نية أي طرف. التخزين العام الدائم هو متجر عام غير قابل للعبث — لا يمكن لأي طرف تعديل qub أو حذفه بعد ختمه. ML-DSA-65 هو توقيع رقمي ما بعد الكم — يرتبط كل qub بزوج مفاتيح لا يغادر سرّه جهاز المؤلف أبدًا.

تُنتج هذه العناصر مجتمعةً بيانًا مُقفلًا زمنيًا، ومُقاومًا للعبث، وقابلًا للنسب — إيصال تنمو قيمته بنمو قدرة العالم على تزييف الماضي.

ما تبقى من هذه الوثيقة هو المواصفة المعيارية المطلوبة لتنفيذات قابلة للتشغيل البيني.


مواصفة بروتوكول qub

الحقل القيمة
الإصدار 1.0 (إصدار البروتوكول 0x01، إصدار الغلاف الخارجي 0x01)
التاريخ 2026-05-01
الحالة مسودة
مراجَع حتى 2026-05-01

هذه الوثيقة هي مواصفة البروتوكول المعيارية لنظام الالتزام الموقوت في qub. تُعرّف هياكل البيانات، وقواعد التسلسل، وصيغ الاشتقاق، وإجراءات التحقق المطلوبة لتنفيذات قابلة للتشغيل البيني.

النطاق: طبقة البروتوكول محايدة لغويًا عن قصد — يكون جسم qub نصًا أو ماركداون أو بايتات ميثاق غير مفسَّرة، والعَرْض المراعي للُّغة هو مسؤولية المُشاهِد (تطبيق qub.social على الويب، إطار <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 تمثيل ثنائي مختصر للكائنات (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 الأب لـ qubs سلاسل الردود (انظر §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 (متطلبات الترميز الحتمي الأساسي)
ترتيب مفاتيح الخريطة تُرتَّب حسب طول البايتات المُرمَّزة أولًا (الأقصر قبل الأطول)، ثم معجميًا (بايت ببايت للترميزات بنفس الطول)
ترميز الأعداد الصحيحة أقصر صيغة: 0–23 في البايت الابتدائي؛ 24–255 في بايتين؛ 256–65535 في 3 بايتات؛ إلخ.
ترميز الطول أطوال محدَّدة فقط. لا يُسمح بأي مصفوفات أو خرائط أو سلاسل بايتات أو سلاسل نص بطول غير محدَّد (المعلومات الإضافية = 31 محظورة).
الوسوم لا توجد وسوم CBOR (النوع الرئيسي 6 محظور).
الفاصلة العائمة لا أعداد عائمة (النوع الرئيسي 7 بقيم 0xF9–0xFB محظور).
سلاسل النص مُرمَّزة بـ UTF-8، مُطبَّعة وفق NFC (صيغة تطبيع يونيكود C).
سلاسل البايتات بايتات خام. لا ترميز base64 في طبقة CBOR.
المفاتيح المكرَّرة يُرفض مع خطأ. يَجِب على المحللات ألّا تقبل بصمت مفاتيح خريطة مكرَّرة.
القيم البسيطة يُسمح فقط بـ true (0xF5) وfalse (0xF4) وnull (0xF6).
الحقول الاختيارية الحقول الاختيارية الغائبة تُحذف من خريطة CBOR كليًا (لا تُرمَّز كـ null). الحقول الاختيارية الموجودة تُدرَج بترتيب المفاتيح المُصنَّف.

3.2 ترتيبات المفاتيح القانونية المُتحقَّق منها

ترتيبات المفاتيح هذه معيارية. يَجِب على التنفيذات إصدار المفاتيح بهذا الترتيب تمامًا. يَنْبَغي لتأكيدات التصحيح أن تتحقق من الترتيب في بنيات ما قبل الإصدار.

QubEnvelope (الإصدار 0x01، غير موقّع، جميع الحقول الاختيارية غائبة):

"body"                (5 encoded bytes)
"qub_id"              (7 encoded bytes)
"sig_alg"             (8 encoded bytes)
"version"             (8 encoded bytes)
"reply_to"            (9 encoded bytes)   ← only if present (reply chains)
"body_hash"           (10 encoded bytes)
"unlock_at"           (10 encoded bytes)
"created_at"          (11 encoded bytes)
"outcome_at"          (11 encoded bytes)  ← only if present (V1.1 verdict mechanic)
"content_type"        (13 encoded bytes)
"sender_label"        (13 encoded bytes)  ← only if present
"author_pubkey"       (14 encoded bytes)  ← only if present
"cosigner_pubkey"     (16 encoded bytes)  ← only if present (pact cosign)
"author_signature"    (17 encoded bytes)  ← only if present
"cosigner_signature"  (19 encoded bytes)  ← only if present (pact cosign)

اشتقاق ترتيب مفاتيح QubEnvelope: كل مفتاح هو سلسلة نصية CBOR. الطول المُرمَّز = 1 بايت رأس + طول السلسلة (للسلاسل الأقصر من 24 بايتًا). فرز حسب الطول المُرمَّز الكلي أولًا، ثم معجميًا للمفاتيح ذات الطول نفسه.

SealedQub (الإصدار 0x01، عام، بلا مُستقبِل):

"title"             (6 encoded bytes)   ← only if present
"qub_id"            (7 encoded bytes)
"version"           (8 encoded bytes)
"unlock_at"         (10 encoded bytes)
"outcome_at"        (11 encoded bytes)  ← only if present (V1.1 verdict mechanic)
"visibility"        (11 encoded bytes)
"drand_round"       (12 encoded bytes)
"drand_chain_id"    (15 encoded bytes)
"recipient_pubkey"  (17 encoded bytes)  ← only if present
"tlock_ciphertext"  (17 encoded bytes)

PactTerms (جسم الميثاق، content_type 0x03):

"notes"         (6 encoded bytes)  ← only if present
"terms"         (6 encoded bytes)
"title"         (6 encoded bytes)
"party_a"       (8 encoded bytes)
"party_b"       (8 encoded bytes)
"pact_version"  (13 encoded bytes)

PactTerm (صفّ في مصفوفة terms):

"key"    (4 encoded bytes)
"value"  (6 encoded bytes)

PartyIdentifier (خريطة party_a / party_b):

"label"    (6 encoded bytes)
"contact"  (8 encoded bytes)  ← only if present

3.3 مرجع ترميز البايتات

النوع ترميز CBOR مثال
تجزئة SHA3-256 (32 بايتًا) 0x58 0x20 + 32 بايتًا body_hash، qub_id
الطوابع الزمنية (i64) النوع الرئيسي 0 (موجب) أو 1 (سالب)، أقصر ترميز ثوان Unix
الإصدار (u8، القيمة 1) 0x01 (بايت واحد)
نوع المحتوى (u8، القيمة 1) 0x01 (بايت واحد)
sig_alg (u8، القيمة 0) 0x00 (بايت واحد)
توقيع ML-DSA-65 (3,309 بايتات) 0x59 0x0C 0xED + 3,309 بايتات author_signature، cosigner_signature
مفتاح ML-DSA-65 العام (1,952 بايتات) 0x59 0x07 0xA0 + 1,952 بايتات author_pubkey، cosigner_pubkey

4. الاشتقاقات المعيارية

4.1 qub_id

يُحدِّد qub_id qub بشكل فريد ويربط QubEnvelope بـ SealedQub. يُشتق حتميًا من محتوى الغلاف.

qub_id = SHA3-256(
    "QUB_ID_V2"    ||    // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
    version        ||    // u8 (1 byte)
    content_type   ||    // u8 (1 byte)
    created_at     ||    // i64 big-endian (8 bytes)
    unlock_at      ||    // i64 big-endian (8 bytes)
    outcome_at_or_zero   ||  // i64 big-endian (8 bytes; 0 when outcome_at is absent)
    drand_round          ||  // u64 big-endian (8 bytes)
    body_hash      ||    // [u8; 32] (32 bytes)
    title_hash           // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output

ترميز فاصل النطاق: السلسلة "QUB_ID_V2" هي 9 بايتات ASCII. يُضاف بايت حشو 0x00 واحد للوصول إلى 10 بايتات للمحاذاة. يَجِب على التنفيذات استخدام هذه البايتات العشر بالضبط: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

ترميز outcome_at: وسّع V1.1 الصورة الأولية من 92 إلى 100 بايت لطيّ حقل outcome_at الاختياري ضمن الربط. يُرمَّز غياب outcome_at بثماني بايتات صفرية؛ وترفض مُتحقِّقات البروتوكول outcome_at <= 0 في كل موضع، فلا تستطيع هذه القيمة الحارسة أن تتصادم مع قيمة مشروعة. انظر §3.2 (الصيغة السلكية) والملف الداخلي tasks/verdict-uplift-plan.md للآلية التحكيمية التي تُحفّز هذا الحقل.

ترميز drand_round: وسّع V1.2 الصورة الأولية من 100 إلى 108 بايت لطيّ drand_round (جولة drand المستهدفة، §4.3) ضمن الربط، ورفع فاصل النطاق إلى QUB_ID_V2. وهذا يربط جولة القفل الزمني ضمن هوية qub: لا تستطيع أي بوابة إعادة ربط النص المُشفَّر بجولة مختلفة (مثلًا جولة ماضية بالفعل) عن الجولة التي يقتضيها unlock_at المعروض. كما يتحقق إجراء فكّ القفل (§8) إضافةً إلى ذلك من أن الجولة المخبوزة في مقطع نص tlock المُشفَّر تطابق unlock_round(unlock_at)، فيكون وقت فكّ القفل المعروض هو الجولة التي تتحكم في فكّ التشفير بشكل قابل للإثبات.

الخصائص:

4.2 body_hash

body_hash = SHA3-256(body)

حيث body هو حمولة المحتوى الخامة Vec<u8>. لـ qubs النصية، هذا هو جسم 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 وقت التجزئة كي يكون الموجز ثابتًا عبر تتابعات نقاط الترميز المتكافئة بصريًا. القيمة الصفرية بالكامل محجوزة لحالة الغياب؛ والسلسلة الفارغة مرفوضة عند حدود 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 (genesis_time) 1595431050
chain_period_seconds معلومات سلسلة drand (period) 30

تختار عملية ceil() أول جولة drand يكون وقت كشفها ≥ unlock_at. هذا يضمن ألّا يصبح qub قابلًا لفكّ التشفير قبل وقت فكّ القفل المُختار.

حالة الحد: إذا كان (unlock_at - chain_genesis_time) قابلًا للقسمة تمامًا على chain_period_seconds، فإن النتيجة هي تلك الجولة بالضبط — يُفكّ قفل qub بدقة عند وقت كشف تلك الجولة.

التحقق: يَجِب أن يكون unlock_at في المستقبل وقت الختم. يَجِب ألّا يبعد unlock_at أكثر من 10 سنوات عن created_at (للحدّ من مخاطر الاعتماد بعيد الأفق على drand؛ يَنْبَغي لواجهة المستخدم أن تُحذّر من تواريخ فكّ قفل تتجاوز سنتين).


5. أنواع جديدة للصيغة السلكية

تُوفّر الأنواع الجديدة للصيغة السلكية سلامة وقت الترجمة ضد الخلط بين بايتات 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() أن تتحقق من أن المدخل يبدأ برأس خريطة CBOR صالح. يحدث التحقق البنيوي الكامل وقت التحليل، لا وقت البناء، تجنبًا للتحليل المزدوج.


6. سجل أنواع المحتوى

القيمة النوع أقصى حجم للجسم ملاحظات
0x00 محجوز (غير صالح) يَجِب ألّا يُستخدم
0x01 نص عادي (UTF-8، ماركداون مقيّد) 50 KB مدفوع / 10 KB مجاني انظر §10 لقواعد العرض. تُفرض القسمة بين المجاني والمدفوع بواسطة خدمة الرفع؛ السقف الصلب لطبقة البروتوكول هو 50 KB.
0x02 محجوز (مستقبلي) مُخصَّص لنوع محتوى مستقبلي؛ غير صالح في v1. يَجِب على المُشاهِدين رفضه وفقًا للقاعدة أدناه.
0x03 ميثاق (اتفاق ثنائي، جسم CBOR) 100 KB الجسم هو CBOR قانوني PactTerms (§6.1). توقيع شريك التوقيع وفقًا لـ §9.7.
0x04 حكم (تقييم ذاتي من المنشئ، جسم CBOR) 8 KB الجسم هو CBOR قانوني VerdictBody (§6.2). يصدر فقط عبر نيّة verdict على جانب النظام. علاقة الأب موجودة على وسم Arweave Parent-Tx-Id، لا في الجسم. انظر verdict-uplift-plan §3.4.

يَجِب على المُشاهِدين رفض أنواع المحتوى غير المعروفة برسالة خطأ واضحة مرئية للمستخدم. يَجِب على المُشاهِدين ألّا يحاولوا عرض الأنواع غير المعروفة كنصّ.

6.1 جسم الميثاق (content_type = 0x03)

جسم الميثاق هو ترميز 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 القانونية للخرائط الثلاث جميعها مذكورة في §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. لا تُترجَم محليًا؛ الجسم الموقّع محايد لغويًا. أي تغيير في الصياغة يستلزم إصدارًا جديدًا من المخطط (structured/v2).

تُثبَّت السلاسل الثماني وعملية البحث عنها (acknowledgement_for(role, kind)) ومبررات كل منها بواسطة التنفيذ المرجعي. يَجِب على التنفيذات المطابقة إصدار قيم إقرار متطابقة بالبايتات؛ واختبارات body-hash لـ SHA3-256 الذهبية المُغطّية للتركيبات الأربعة للأدوار تكشف أي انجراف.

ترتيب عرض المُشاهِد. تحتوي سلاسل الإقرار على عبارات مثل "described above"، والتي تفترض أن صفوف الوصف / النطاق تُعرَض قبل الإقرارات. يَجِب على المُشاهِدين عرض مصفوفة terms بترتيب CBOR؛ إعادة الترتيب تكسر دلالات النص.

جهة اتصال الطرف المقابل. عندما يكون contact للطرف B عنوان بريد إلكتروني صالحًا، تُرسل خدمة رفع qub تلقائيًا بريدًا إلكترونيًا لدعوة المراجعة / التوقيع المشترك وقت التحضير وتربط التوقيع المشترك النهائي بالتحقق من ذلك العنوان نفسه (§9.7). المواثيق التي يكون فيها جهة اتصال الطرف B غائبة لا يزال يمكن التوقيع المشترك عليها، لكن فقط عبر قناة خارجية — ترفض الخدمة طلبات التوقيع المشترك التي لا يمكنها إنتاج علامة تحقق-بريد-إلكتروني مطابقة لمدة 15 دقيقة.

6.2 جسم الحكم (content_type = 0x04)

جسم الحكم هو ترميز 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
}

ترتيب مفاتيح CBOR القانوني:

"outcome"          (8 encoded bytes)
"reflection"       (11 encoded bytes)  ← only if present
"evidence_url"     (13 encoded bytes)  ← only if present
"verdict_version"  (16 encoded bytes)

يَجِب ألّا يتجاوز إجمالي CBOR الحكم المتسلسل 8 KB (يطابق صفّ السجل أعلاه).

تعداد النتيجة. بايت السلك محايد لجهة النيّة؛ السلال الأربع Right / Partial / Wrong / Unfalsifiable تغطّي فضاء النتائج لكل نيّة حاملة للحكم. أما التسميات الخاصّة بكل نيّة ("توقّعتها" / "وفّيت به" / "أُطلق" / "أكّدتها الأحداث" لـ Right، إلخ) فهي شأن عرض من جانب المُشاهِد يُحَلّ مقابل نيّة qub الأب — يبقى السلك محايدًا لغويًّا ولجهة النيّة. القيم خارج النطاق 1..=4 يَجِب رفضها عند فكّ الترميز.

ربط الأب. لا يحمل qub الحكم مرجع الأب في جسمه. معرّف معاملة Arweave لـ qub الأب يُصدَر كوسم تخزين Parent-Tx-Id وقت الرفع (طبقة وسوم التخزين §7). هذا يُبقي الجسم بيانًا موقّعًا قائمًا بذاته للتقييم الذاتي؛ سلسلة التدقيق ("مُحقّ بشأن ماذا؟") تُثبَت عبر بحث وسم Arweave.

سلامة رابط الدليل (معياري). عندما يكون evidence_url حاضرًا، يَجِب على المُتحقّقين (جانب التأليف، جانب السلك، حافة الـ Worker) فرض ما يلي:

  1. HTTPS فقط. يَجِب أن تبدأ السلسلة بمتسلسلة البايتات https://. أي مخطّط آخر — http أو ftp أو javascript أو data أو file وغيرها — يُرفض.
  2. حدّ الطول. ≤ 2,048 بايت (الحدّ العملي لعنوان المتصفح).
  3. NFC + فحص النقاط المعادية. نفس قاعدة title وreflection — نقاط إلغاء ترتيب الكتابة الثنائي / العرض الصفري / كتلة الوسوم / BOM / C0 / C1 تُرفض. التعريف يطابق crate::handle::contains_hostile_text_codepoint في Rust وworkers/api/src/utils/unicode.ts::isHostileCodepoint في TS (حافظ على التوافق بينها).
  4. بدون مسافات أو أحرف تحكم ASCII. المسافات / DEL / البايتات تحت 0x20 في أيّ موضع من الرابط تُرفض — يُغلق ذلك ناقل حقن \n/\t الذي لا تغطّيه قاعدة bidi.
  5. مقطع مضيف غير فارغ. كل ما بين https:// وأول / أو ? أو # يَجِب أن يكون غير فارغ.

لا جلب من جانب الخادم. يَجِب ألّا يتولّى الـ Worker وكالة الرابط أو جلبه أو معاينته. البروتوكول يخزّن سلسلة فقط؛ العرض يحدث على جانب المُشاهِد بـ 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 (نية إنشاء مُتحقَّق منها بقائمة مسموح بها — مثلًا quote أو reply أو commitment)، وAuthor (بصمة المفتاح العام للمُنْشِئ وفقًا لـ §9.3 ست عشرية صغيرة من 64 رمزًا)، وParent-Tx-Id (معرّف معاملة التخزين لـ qub الأب لسلاسل الردود، base64url من 43 رمزًا).

وسم Author اختياري لكل qub: تطبيق المُنْشِئ المرجعي يُرفقه فقط عندما يُفعّل المستخدم صراحةً النسب العام وقت الختم. عند إيقاف التفعيل — وهو الافتراضي — لا يُكتب وسم Author ويكون qub غير منسوب على السلسلة: لا شيء في التخزين الدائم يربط الرفع بمعرّف المُنْشِئ أو بريده الإلكتروني أو qubs أخرى له. عند تفعيل التبديل، تنحلّ بصمة Author إلى @handle الذي اختاره المُنْشِئ عبر سلسلة التَّوْثيق في §9.5. علاقات سلاسل الردود وIntent لا تُعرّف الهوية. الغلاف الخارجي (§13) يحمي الجسم الداخلي من ارتباط النصوص المشفرة — مما يمنع الحاصد من التعرف على رفعات بصيغة qub وفكّ تشفيرها بالجملة بعد نشر جولتها على drand.

لا تُرفق الخدمة المرجعية عمدًا وسوم App-Name أو App-Version أو Type: أي مرشّح قائم على قيمة واحدة من هذا القبيل سيُرجع كامل مجموعة qubs لاستعلام GraphQL، وهذا يتعارض مع نطاق سرية الجسم فقط للغلاف.

يَجِب على المُتحقِّق المطابق ألّا يعتمد على أي وسم تخزين للتحقق من طرف ثالث وفقًا لـ §11؛ تجزئة الجسم / qub_id / التوقيع تلتزم بالـ CBOR الداخلي فقط، لا بمجموعة الوسوم.


8. بروتوكول فكّ القفل

تسلسل فكّ القفل الكامل. كل خطوة معيارية.

 1. Viewer opens delivery URL. Extract arweave_tx_id from path AND
    K = base64url_decode(fragment) from the URL fragment. If the fragment
    is absent or malformed → display "this URL is missing its decryption
    key" and stop; the viewer MUST NOT contact the storage gateway
    without K, since fetching wrapped bytes the viewer cannot decrypt
    serves no purpose and only leaks the access attempt.
 2. Check denylist. If tx_id is denylisted → display block message. Stop.
 3. Fetch OuterWrapper bytes from permanent storage (with multi-gateway fallback).
 3a. Unwrap: parse the bytes as OuterWrapper (§13), verify the wrapper
    `version` byte is `0x01`, and compute SealedQubCbor =
    unwrap_sealed_qub(OuterWrapper, key=K). Any AEAD authentication
    failure (wrong K, tampered ciphertext, swapped qub_id-as-AAD,
    swapped nonce) → display "this URL's decryption key does not match
    the stored qub" and stop. Authentication failures are
    indistinguishable to the viewer per §13.5.
 4. Parse SealedQubCbor → SealedQub.
 5. Validate: SealedQub.version is known (0x01). Reject unknown versions.
 6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
 6a. Round-binding check (V1.2). Recompute expected_round =
    ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
    Reject unless SealedQub.drand_round == expected_round AND the round baked
    into the tlock ciphertext stanza (read via the age/tlock header, no signature
    required) == expected_round. The stanza round is the one that actually gates
    decryption; without this check a malicious creator could bind the ciphertext
    to an already-past round while displaying a future countdown, so anyone
    reading the stored bytes could decrypt before unlock_at. Implementations with
    no chain identity (test mocks) skip this check.
 7. Once current time ≥ SealedQub.unlock_at:
    a. Fetch drand round signature for SealedQub.drand_round from drand network.
    b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
 8. Parse B → QubEnvelope.
 9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
    Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
    Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
    Fail → integrity error.
13. Verify: QubEnvelope.content_type is known and renderable.
    Known values: 0x01 (text), 0x03 (pact). Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using appropriate renderer (see §10 for text, §6 for pact).
17. Construct RevealedQub for display.

9. توقيع التأليف

9.1 المبرر

تُخزَّن 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]. بلا حشو.

البايت الزائل: يَجِب أن يكون البايت الـ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 أو عنوان بريد إلكتروني أو معرّف اجتماعي أو شهادة 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–2000 بايت، يُضاعف 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 رفض طلب التوقيع المشترك ما لم توجد علامة تحقق بريد إلكتروني قصيرة العمر تطابق كلًا من معرّف التحضير وتجزئة البريد المُطبَّع لتلك الجهة. تُكتب العلامة بواسطة /api/v1/auth/verify عندما يحمل رمز الرابط السحري staging_id ويطابق العنوان المُتحقَّق منه SHA-256(normalise_email(party_b.contact)) — حيث تُحافظ normalise_email(addr) على حالة الجزء المحلي وتُصغّر فقط جزء النطاق (وفقًا لـ RFC 5321 §2.3.11)، وSHA-256 هنا هي تجزئة NIST FIPS 180-4 (متميزة عن SHA3-256 المستخدمة في اشتقاقات §4) — وتنتهي صلاحيتها بعد 900 ثانية (15 دقيقة) من الإصدار. هذه بوابة تشغيلية لمكافحة انتحال الشخصية، وليست جزءًا من إثبات qub على السلسلة — يحتاج المُتحقِّق من طرف ثالث الذي يُعيد تنفيذ §11 فقط إلى التخزين الدائم و drand، دون أي بحث على جانب الخادم. توجد العلامة على جانب الخادم فقط ولا تكون أبدًا جزءًا من الجسم الموقّع.

أثر الحجم (مؤلف ML-DSA-65 + شريك التوقيع):

المكوّن الحجم
توقيع المؤلف 3,309 بايتات
مفتاح المؤلف العام 1,952 بايتًا
توقيع شريك التوقيع 3,309 بايتات
مفتاح شريك التوقيع العام 1,952 بايتًا
إجمالي العبء التشفيري 10,522 بايتًا
دلتا تكلفة التخزين ~$0.05

10. عرض الماركداون والتعقيم

هذا القسم حرج أمنيًا. يعرض المُشاهِد qubs النصية (content_type = 0x01) باستخدام مجموعة فرعية مقيَّدة من الماركداون.

10.1 العناصر المسموح بها

10.2 العناصر المحظورة

العنصر المعالجة
HTML خام (<div>، <script>، إلخ) يُجرَّد كليًا. لا يمرّ أي HTML.
الصور (![alt](url)) تُجرَّد. تُزال صيغة الصورة من المخرج.
الروابط ([text](url)) يُعرَض الـ URL كنصّ عادي مرئي. لا يُربط تلقائيًا. غير قابل للنقر دون إجراء صريح من المستخدم.
مخططات URL الخطرة javascript:، data:، vbscript:، file: — تُجرَّد.
الإطارات الداخلية، التضمينات، الكائنات تُجرَّد.
كيانات HTML تُفكَّك إلى أحرف عرض فقط إن كانت آمنة.

10.3 التنفيذ

يَجِب على التنفيذات استخدام محلّل قائمة سماح صارمة، لا قائمة حظر. النهج المُوصى به:

  1. تحليل الماركداون باستخدام pulldown-cmark (أو ما يكافئه).
  2. السير على الـ AST وإسقاط أي عقدة ليست في قائمة السماح (§10.1).
  3. لعُقد الروابط: إصدار الـ URL كنصّ مرئي، لا كعنصر <a> قابل للنقر.
  4. تحويل الـ AST المُرشَّحة إلى تمثيل وسيط من نوع مُحدَّد (مثل enum MarkdownNode بمتغيرات آمنة فقط). HTML الخام غير قابل للتمثيل بنيويًا في هذا الـ IR.
  5. العرض من الـ IR المُحدَّد النوع إلى طبقة العرض المستهدفة (مثل مكوّنات العرض التفاعلية، عُقد DOM). لا تسلسل سلسلة HTML ولا innerHTML في أي نقطة.

نهج قائمة الحظر هشّ لأن امتدادات الماركداون الجديدة أو غرابات المحلل يمكن أن تُدخل عناصر غير مرشَّحة. نهج الـ AST المُحدَّد النوع يجعل XSS مستحيلًا بنيويًا — لا يوجد متغيّر يمكن أن يحمل HTML تعسفيًا.

10.4 حدود الحجم والبنية


11. التحقق من طرف ثالث

يستطيع أي طرف ثالث التحقق من qub عام دون تعاون من qub. إجراء التحقق:

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

ما يُثبته التحقق:

الإثبات ما يُرسّخه
الالتزام كان النص المشفّر موجودًا بحلول طابع كتلة التخزين الزمني.
السلامة جسم النص العادي يطابق التجزئة المُلتزَم بها ولم يُعدَّل.
التوقيت كان المحتوى غير قابل للقراءة حتى جولة drand، التي تقابل وقت فكّ القفل المُختار (شرط افتراضات أمان tlock و drand).

ما لا يُثبته التحقق:

غير الإثبات السبب
التأليف sender_label زخرفي. بدون sig_alg0x01، كان يمكن لأي شخص ختم هذا المحتوى.
النية يُثبت qub المحتوى والتوقيت، لا ما قصده المُنْشِئ ذاتيًا.
التوقيت قبل الحدث قد يتأخر إدراج كتلة التخزين عن الرفع الفعلي بدقائق. طابع التزام الوقت هو وقت الكتلة، لا لحظة ضغط المستخدم على "ختم".

12. الإصدارات

12.1 إصدار البروتوكول

حقل version (u8) في كل من SealedQub وQubEnvelope يُحدّد الإصدار الرئيسي للبروتوكول.

12.2 سجل الإصدارات

الإصدار القيمة الوصف
v1 0x01 qubs نصية عامة (content_type 0x01)، اتفاقيات ميثاق ثنائية (0x03، مخطط structured/v1، ML-DSA-65 مؤلف + شريك توقيع)، tlock، SHA3-256

12.3 التوافق الأمامي

مُشاهِد v1 يصادف QubEnvelope بمفاتيح خريطة CBOR اختيارية غير معروفة (مفاتيح ليست في الترتيب القانوني في §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 المختوم مُقفلًا زمنيًا: الجسم غير قابل للقراءة حتى unlock_at وبعد نشر توقيع جولة drand. بعد فكّ القفل، مع ذلك، يصبح توقيع الجولة عامًا والشكل القانوني لـ CBOR لـ SealedQub قابلًا للتعرف، لذا يستطيع حاصد فهرس معاملات التخزين الدائم فكّ تشفير مجموعة qubs كاملة بالجملة.

يُغلق غلاف التشفير الخارجي تلك القناة بإقحام طبقة AEAD متماثلة إضافية بين SealedQubCbor القانوني والبايتات المكتوبة إلى التخزين الدائم. مفتاح 256-بت K يعيش فقط في جزء URL لرابط التَّسْليم وعلى أجهزة المستخدمين؛ لا تنقل المتصفحات أجزاء URL إلى الخوادم، لذا يكون qub.social وكل بوابة تخزين وكل CDN أمام أي منهما عميًا رصديًا عن K. لذا فإن كل qub في التخزين الدائم هو نص مشفّر غامض لا يمكن استرداد نصه العادي دون الـ URL الذي اختار المُنْشِئ مشاركته.

التأثير الصافي:

13.2 الطبقات

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

الختم وفكّ القفل في طبقة البروتوكول (§7، §8) لم يتغيّرا تحت حدود الغلاف؛ يلتصق الغلاف عند موقع استدعاء seal() وينفصل عند موقع استدعاء unlock().

13.3 هيكل بيانات OuterWrapper

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

ثوابت الحقول.

ترميز CBOR. CBOR قانوني وفقًا لـ §3، بنفس قاعدة ترتيب المفاتيح (مرتبة تصاعديًا حسب طول البايتات المُرمَّزة، ثم معجميًا). المفاتيح الأربعة هي:

المفتاح البايتات المُرمَّزة الترتيب
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

أول بايت من CBOR لـ OuterWrapper هو إذن رأس الخريطة بطول محدَّد لخريطة من 4 إدخالات (0xA4).

13.4 ربط AAD بـ qub_id

يربط الغلاف qub_id بوصفه بيانات AEAD المصادَق عليها إضافيًا. هذا هو الدفاع البنيوي الحامل للحمل ضد ثلاث فئات من الهجمات:

الهجوم الدفاع
نقل نص مشفّر تحت حقل qub_id مختلف في الغلاف عدم تطابق AAD ← تفشل مصادقة AEAD
خلط جزء URL لـ 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، بلا حشو) وإلحاقه برابط التَّسْليم كمكوّن الجزء:

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

لا يُنقل الجزء أبدًا إلى أي خادم بواسطة متصفح مطابق. قنوات الاستعادة (فهرس السجل على جانب الخادم، الإرسال التلقائي الاختياري بالبريد الإلكتروني) التي تستمر بحفظ كامل رابط التَّسْليم — بما في ذلك الجزء — وراء جهاز المستخدم هي مقايضة صريحة ضد الوضعية الافتراضية لتمزيق التشفير ويَجِب أن تكون مشروطة بموافقة المستخدم الصريحة.

فقدان الجزء. إذا فقد المستخدم جزء URL وليست لديه قناة استعادة، يكون qub غير قابل للقراءة. هذه هي المقايضة الحاملة للحمل في التصميم ويَجِب الإفصاح عنها للمستخدم وقت الختم. يُعزّز الـ MVP الإفصاح وقت الختم بنصّ صريح "احفظ هذا الرابط" وقناة استعادة ببريد إلكتروني مُتحقَّق منه للمستخدمين الذين يختارون التفعيل.

13.7 خارج نطاق هذا القسم

13.8 qubs العامة (حذف الغلاف)

الغلاف الخارجي اختياري في طبقة التَّسْليم. يجوز للمُنْشِئ ختم qub بوصفه عامًا، وفي هذه الحالة يُكتب SealedQubCbor القانوني إلى التخزين الدائم مباشرةً، دون طبقة OuterWrapper ودون مفتاح K:

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

qub العام مُقفل زمنيًا لكنه غير محكوم بالرابط: يبقى غير قابل للقراءة حتى تنشر جولة drand الخاصة به (طبقة tlock لم تتغير)، لكن بعد فكّ القفل يستطيع أي شخص يملك arweave_tx_id فكّ تشفيره — لا يلزم جزء URL، إذ لا وجود لـ K. هذه هي المقايضة المقصودة للأسطح التي يَجِب أن يقودها الخادم: رسائل إشعار الكشف بالبريد الإلكتروني، والتضمينات من أطراف ثالثة، وتحسين محركات البحث الأغنى بعد الكشف — كلها تحتاج إلى رابط يعمل دون سرّ لا يحوزه الخادم أبدًا (§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 متطابقة لهذا المدخل. يَنْبَغي أن يكون متّجه الاختبار هذا أول اختبار وحدة يُكتب. حُسبت القيم القانونية أعلاه بواسطة التنفيذ المرجعي ويَجِب أن تتطابق بتًا ببتٍ. ترتيبات الصورة الأولية التاريخية (ما قبل الإطلاق — لا توجد qubs نشطة كانت تعتمد عليها): كان 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 PactTerms CBOR (content_type 0x03)

Input:
  pact_version = 1
  title        = "Scooter deposit"
  terms        = [
    { key: "Item",    value: "Honda Metropolitan scooter" },
    { key: "Price",   value: "$100" },
    { key: "Deposit", value: "$10" }
  ]
  party_a      = { label: "Alice" }
  party_b      = { label: "Bob", contact: "bob@example.com" }
  notes        = absent

Canonical CBOR key order (PactTerms):
  "notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)

Canonical CBOR key order (PactTerm):
  "key"(4) < "value"(6)

Canonical CBOR key order (PartyIdentifier):
  "label"(6) < "contact"(8)

تُحسب بايتات CBOR القانونية وbody_hash لـ SHA3-256 بواسطة التنفيذ المرجعي. يَجِب على التنفيذات إنتاج CBOR متطابق بايتيًا لهذا المدخل.

كما يَجِب على التنفيذات التحقق من أن serialize(parse(serialize(pact))) == serialize(pact) لكل مدخلات PactTerms الصالحة (اختبار خصائص).

14.5 متّجهات الغلاف الخارجي عبر اللغات

يحتوي الغلاف الخارجي (§13) على تثبيت قانوني منفصل في crates/qub-core/tests/vectors/wrapper_v1.json. تثبت كل حالة رباعية (key, nonce, qub_id, sealed_cbor) كمدخلات ست عشرية غامضة وتؤكد مخرج expected_wrapper_hex محدَّدًا. يستهلك كلا التنفيذين المرجعيين ملف JSON نفسه:

يثبّت التثبيت حاليًا ثلاث حالات:

الحالة التغطية
basic-text-public أصغر شكل واقعي لـ SealedQub؛ بلا حقول اختيارية. يُؤسّس شكل الغلاف القانوني لـ qub نمطي بإصدار v1.0.
with-recipient-pubkey SealedQub مع تعيين recipient_pubkey (مسار المرحلة 2). مجموعة مفاتيح CBOR داخلية مختلفة وqub_id مختلف.
longer-body جسم بحجم ~4 KiB — يُمارس بادئات طول CBOR متعددة البايتات داخل كل من الغِلاف الداخلي والنص المشفّر الخارجي.

يَجِب على التنفيذات إنتاج expected_wrapper_hex متطابق بايتيًا للمدخلات المُسجَّلة. تتطلب إعادة توليد التثبيت QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors ومحجوزة لتغييرات صيغة متعمَّدة.


15. حوكمة ملف التشفير (مستقبلي)

هذا القسم إعلامي لـ v1 ويصبح معياريًا أول مرة تدخل فيها خوارزمية ثانية إلى أي من العناصر التشفيرية الأولية لـ qub.

15.1 الوضعية الحالية

يربط بروتوكول v1 بالضبط خوارزمية واحدة لكل عنصر أولي:

يُرسّخ المُتحقِّقون حاليًا أطوال المفتاح والتوقيع لكل عنصر أولي. لا يُعرَّض أي سطح مرونة بواسطة الصيغة السلكية.

15.2 الشكل المنشود

عندما تدخل خوارزمية ثانية إلى البروتوكول، سيُهيَّأ المُتحقِّق لـ CryptoProfile مُسمّى (مثل ExqubV1) يسرد المجموعة الدقيقة للقيم المسموح بها لكل عنصر أولي — sig_algs، سلاسل drand، إصدارات الغلاف، أنواع المحتوى. يُثبَّت الملف وقت التحقق، ولا يُتفاوَض عليه ضمن النطاق. أي قيمة خارج الملف النشط تُرفض.

يضمن هذا أن إضافة ML-DSA-87 أو تفعيل Ed25519 لا يمكن أن يُضعفا تكوينات المُتحقِّقين القائمة بأثر رجعي: مُتحقِّق v1 يبقى مُتحقِّق v1 حتى بعد نشر ملف v2.

15.3 شروط التفعيل

ارفع §15 إلى حالة معيارية عندما يُقترح أي مما يلي:

حتى ذلك الحين، §15 هو حافظ مكان يُثبّت شكل الهجرة كي تنزل PRs المستقبلية على هدف معروف بدلًا من إعادة التقاضي على سطح التفاوض من الصفر.