Specyfikacja protokołu qub

qub to protokół kryptograficznych zobowiązań czasowych: system do pieczętowania słów do przyszłej daty i udowadniania, gdy ta data nadejdzie, dokładnie co zostało powiedziane i kiedy.

Działanie zapewniają trzy prymitywy. drand to zdecentralizowane źródło losowości — data ujawnienia jest egzekwowalna fizyką, a nie dobrą wolą jakiejkolwiek strony. Trwały publiczny magazyn to odporny na manipulacje publiczny rejestr — żadna strona nie może edytować ani usunąć qub po jego zapieczętowaniu. ML-DSA-65 to postkwantowy podpis cyfrowy — każdy qub jest powiązany z parą kluczy, której tajny klucz nigdy nie opuszcza urządzenia autora.

Razem te prymitywy tworzą wypowiedź, która jest zablokowana czasowo, odporna na manipulacje i przypisywalna — pokwitowanie, którego wartość rośnie wraz z poprawą zdolności świata do fałszowania przeszłości.

Pozostała część tego dokumentu to specyfikacja normatywna wymagana dla interoperacyjnych implementacji.


Specyfikacja protokołu qub

Pole Wartość
Wersja 1.0 (wersja protokołu 0x01, wersja zewnętrznej koperty 0x01)
Data 2026-05-01
Status Szkic
Zrecenzowano do 2026-05-01

Ten dokument jest normatywną specyfikacją protokołu dla systemu zobowiązań czasowych qub. Definiuje struktury danych, reguły serializacji, formuły derywacji oraz procedury weryfikacji wymagane dla interoperacyjnych implementacji.

Zakres: warstwa protokołu jest celowo neutralna językowo — ciało qub to nieprzezroczysty tekst jawny / markdown / bajty paktu, a renderowanie z uwzględnieniem lokalizacji jest odpowiedzialnością widza (aplikacja webowa qub.social, ramka iframe <qub-embed>, klienci MCP, itd.).


1. Notacja i konwencje

Notacja Znaczenie
u8, u64, i64 Liczby całkowite bez znaku/ze znakiem o określonej szerokości bitowej
[u8; N] Tablica bajtów o stałej długości N bajtów
Vec<u8> Tablica bajtów o zmiennej długości
Option<T> Wartość typu T, albo nieobecna
String Łańcuch tekstowy UTF-8, znormalizowany NFC
`
SHA3-256(x) Hash NIST SHA3-256 łańcucha bajtów x (FIPS 202)
ceil(x) Funkcja sufitu: najmniejsza liczba całkowita ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Najbardziej znaczący bajt jako pierwszy

Wszystkie liczby całkowite w konstrukcjach preimage są kodowane jako tablice bajtów big-endian o stałej szerokości (i64 → 8 bajtów, u8 → 1 bajt), o ile nie określono inaczej.

Wszystkie znaczniki czasu to sekundy uniksowe w UTC.


2. Struktury danych

2.1 ComposeQub (stan twórcy w pamięci)

Nieserializowany do CBOR. Niezapisywany w trwałym magazynie. Lokalny dla aplikacji twórcy.

ComposeQub {
    draft_id:       [u8; 16],        // Losowy, generowany lokalnie
    created_at:     i64,             // Sekundy uniksowe UTC
    unlock_at:      Option<i64>,     // Sekundy uniksowe UTC; None podczas komponowania
    visibility:     u8,              // 0x01 = publiczny (jedyna wartość w MVP)
    content_type:   u8,              // 0x01 = tekst (jedyna wartość w MVP)
    plaintext:      Vec<u8>,         // Ciało qub w UTF-8
    sender_label:   Option<String>,  // Dekoracyjna nazwa wyświetlana; nieuwierzytelniona
    status:         DraftStatus,     // Composing | Sealed | Uploaded | Failed
}

2.2 QubEnvelope (odszyfrowany ładunek)

Serializowany przy użyciu kanonicznego CBOR (§3). Zaszyfrowany wewnątrz SealedQub. To jest struktura, która dowodzi integralności treści po odszyfrowaniu.

QubEnvelope {
    version:             u8,              // Główna wersja protokołu (0x01 dla v1)
    qub_id:              [u8; 32],        // Wyprowadzony (zob. §4.1)
    content_type:        u8,              // Rejestr typów treści (zob. §6)
    created_at:          i64,             // Sekundy uniksowe UTC
    unlock_at:           i64,             // Sekundy uniksowe UTC
    outcome_at:          Option<i64>,     // V1.1 — gdy rzeczywistość wydaje werdykt (verdict-uplift-plan §3.1)
    sender_label:        Option<String>,  // Dekoracyjny; nieuwierzytelniony w MVP
    reply_to:            Option<[u8; 32]>,// qub_id rodzica dla łańcuchów odpowiedzi; nieobecny w preimage qub_id; niepodpisany (zob. §9.3)
    body:                Vec<u8>,         // Ładunek treści (UTF-8 dla tekstu, CBOR dla paktu)
    body_hash:           [u8; 32],        // SHA3-256(body) (zob. §4.2)
    sig_alg:             u8,              // Algorytm podpisu (zob. §9.2)
    author_signature:    Option<Vec<u8>>, // Ustawione gdy sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Ustawione gdy sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Ustawione dla współpodpisanych umów bilateralnych typu pact
    cosigner_signature:  Option<Vec<u8>>, // Ustawione dla współpodpisanych umów bilateralnych typu pact
}

Wariant bazowy (niepodpisany qub tekstowy): version = 0x01, content_type = 0x01, sig_alg = 0x00, wszystkie pola Option nieobecne.

Inne konfiguracje v1: content_type = 0x03 (ciało paktu, zob. §6.1); sig_alg = 0x01 (ML-DSA-65) z obecnymi author_signature i author_pubkey (zob. §9.3); cosigner_pubkey i cosigner_signature obecne razem dla współpodpisanych paktów (zob. §9.7); reply_to ustawione na qub_id rodzica qub dla qubów w łańcuchu odpowiedzi (zob. §9.3 dla implikacji zakresu podpisu).

2.3 SealedQub (kanoniczny format na drucie)

Serializowany przy użyciu kanonicznego CBOR (§3). Wysyłany do trwałego magazynu. To jest artefakt on-chain.

SealedQub {
    version:           u8,              // Główna wersja protokołu (0x01 dla v1)
    qub_id:            [u8; 32],        // Taki sam jak QubEnvelope.qub_id
    visibility:        u8,              // 0x01 = publiczny; widzowie v1 odrzucają inne wartości
    unlock_at:         i64,             // Sekundy uniksowe UTC
    outcome_at:        Option<i64>,     // V1.1 — wyświetlane na CTA śledzenia werdyktu
                                        //   przed ujawnieniem; lustro QubEnvelope.outcome_at;
                                        //   związane z qub_id przez preimage z §4.1.
    drand_chain_id:    String,          // Hash łańcucha drand (ciąg hex)
    drand_round:       u64,             // Docelowy numer rundy drand
    tlock_ciphertext:  Vec<u8>,         // Bajty CBOR QubEnvelope zaszyfrowane tlock
    recipient_pubkey:  Option<[u8; 32]>,// Pole zarezerwowane; akceptowane przez kanoniczny CBOR
                                        //   ale nieinterpretowane przez referencyjnego widza v1
    title:             Option<String>,  // Tytuł w postaci tekstu jawnego pokazywany na
                                        //   odliczaniu widza przed ujawnieniem. Powiązany
                                        //   z qub_id przez title_hash (§4.1). 1..=100 punktów
                                        //   kodowych NFC, bez znaków sterujących.
}

2.4 RevealedQub (stan aplikacji widza)

Nieserializowany do CBOR. Lokalny dla aplikacji widza. Konstruowany po pomyślnym odszyfrowaniu i weryfikacji.

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 — przeniesione z QubEnvelope.outcome_at / SealedQub.outcome_at; napędza blok obserwowania werdyktu na stronie ujawnienia (verdict-uplift-plan §5.1)
    drand_chain_id:      String,
    drand_round:         u64,
    sender_label:        Option<String>,
    title:               Option<String>,    // Przeniesione z 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. Profil kanonicznego CBOR

Cała serializacja SealedQub i QubEnvelope MUSI być zgodna z tym profilem. Dwie implementacje otrzymujące tę samą logiczną strukturę MUSZĄ wytworzyć identyczne bajty.

3.1 Reguły kodowania

Reguła Specyfikacja
Standard RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Kolejność kluczy mapy Sortowane najpierw po długości zakodowanych bajtów (krótsze przed dłuższymi), następnie leksykograficznie (bajt po bajcie dla kodowań tej samej długości)
Kodowanie liczb całkowitych Najkrótsza forma: 0–23 w bajcie początkowym; 24–255 w 2 bajtach; 256–65535 w 3 bajtach; itd.
Kodowanie długości Tylko określone długości. Bez tablic, map, łańcuchów bajtów ani łańcuchów tekstowych o nieokreślonej długości (additional info = 31 jest zabronione).
Tagi Bez tagów CBOR (główny typ 6 jest zabroniony).
Zmiennoprzecinkowe Bez liczb zmiennoprzecinkowych (główne typy 7 wartości 0xF9–0xFB są zabronione).
Łańcuchy tekstowe Kodowane UTF-8, znormalizowane NFC (Unicode Normalization Form C).
Łańcuchy bajtów Surowe bajty. Bez kodowania base64 w warstwie CBOR.
Duplikaty kluczy Odrzucenie z błędem. Parsery NIE MOGĄ po cichu akceptować zduplikowanych kluczy mapy.
Wartości proste Dozwolone są tylko true (0xF5), false (0xF4) i null (0xF6).
Pola opcjonalne Nieobecne pola opcjonalne są pomijane całkowicie z mapy CBOR (nie są kodowane jako null). Obecne pola opcjonalne są dołączane w posortowanej kolejności kluczy.

3.2 Zweryfikowane kanoniczne kolejności kluczy

Te kolejności kluczy są normatywne. Implementacje MUSZĄ emitować klucze dokładnie w tej kolejności. Asercje debugujące POWINNY weryfikować kolejność w buildach nierelease.

QubEnvelope (wersja 0x01, niepodpisany, wszystkie pola opcjonalne nieobecne):

"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)

Wyprowadzenie kolejności kluczy QubEnvelope: każdy klucz jest łańcuchem tekstowym CBOR. Długość zakodowana = 1 bajt nagłówka + długość łańcucha (dla łańcuchów poniżej 24 bajtów). Sortowanie najpierw po całkowitej długości zakodowanej, następnie leksykograficznie dla kluczy o tej samej długości.

SealedQub (wersja 0x01, publiczny, bez odbiorcy):

"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 (ciało paktu, 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 (wiersz tablicy terms):

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

PartyIdentifier (mapa party_a / party_b):

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

3.3 Odniesienie kodowania bajtów

Typ Kodowanie CBOR Przykład
Hash SHA3-256 (32 bajty) 0x58 0x20 + 32 bajty body_hash, qub_id
Znaczniki czasu (i64) Główny typ 0 (dodatni) lub 1 (ujemny), najkrótsze kodowanie Sekundy uniksowe
Wersja (u8, wartość 1) 0x01 (pojedynczy bajt)
Typ treści (u8, wartość 1) 0x01 (pojedynczy bajt)
sig_alg (u8, wartość 0) 0x00 (pojedynczy bajt)
Podpis ML-DSA-65 (3 309 bajtów) 0x59 0x0C 0xED + 3 309 bajtów author_signature, cosigner_signature
Klucz publiczny ML-DSA-65 (1 952 bajty) 0x59 0x07 0xA0 + 1 952 bajty author_pubkey, cosigner_pubkey

4. Derywacje normatywne

4.1 qub_id

qub_id jednoznacznie identyfikuje qub i wiąże QubEnvelope z SealedQub. Jest wyprowadzany deterministycznie z zawartości koperty.

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

Kodowanie separatora domeny: Łańcuch "QUB_ID_V2" ma 9 bajtów ASCII. Dla wyrównania dołączany jest pojedynczy bajt wypełniający 0x00, aby osiągnąć 10 bajtów. Implementacje MUSZĄ używać dokładnie tych 10 bajtów: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Kodowanie outcome_at: V1.1 rozszerzyło preimage z 92 do 100 bajtów, aby wpleść opcjonalne pole outcome_at w wiązanie. Nieobecne outcome_at jest kodowane jako 8 zerowych bajtów; walidatory protokołu wszędzie odrzucają outcome_at <= 0, więc ten sentinel nie może kolidować z prawowitą wartością. Zob. §3.2 (format na drucie) oraz wewnętrzny tasks/verdict-uplift-plan.md dla mechaniki werdyktu, która motywuje to pole.

Kodowanie drand_round: V1.2 rozszerzyło preimage z 100 do 108 bajtów, aby wpleść drand_round (docelową rundę drand, §4.3) w wiązanie, i podbiło separator domeny do QUB_ID_V2. To wiąże rundę szyfrowania czasowego z tożsamością qub: brama nie może powiązać ponownie tekstu zaszyfrowanego z inną rundą (np. już minioną) niż implikuje wyświetlane unlock_at. Procedura odblokowania (§8) dodatkowo weryfikuje, że runda zaszyta w stanzie tekstu zaszyfrowanego tlock pasuje do unlock_round(unlock_at), więc wyświetlany czas odblokowania jest dowodliwie rundą, która bramkuje odszyfrowywanie.

Właściwości:

4.2 body_hash

body_hash = SHA3-256(body)

Gdzie body to surowy ładunek treści Vec<u8>. Dla qubów tekstowych jest to ciało qub zakodowane w 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

Gdzie title to opcjonalny tytuł jawny pokazywany na odliczaniu widza przed ujawnieniem (zob. §3.2). Normalizacja NFC odbywa się w czasie obliczania hasha, dzięki czemu skrót jest stabilny dla wizualnie równoważnych sekwencji punktów kodowych. Wartość-strażnik zawierająca same zera jest zarezerwowana dla przypadku nieobecnego; pusty łańcuch jest odrzucany na granicy kanonicznego CBOR jako niekanoniczne kodowanie "nieobecny" (kanoniczne kodowanie pomija to pole całkowicie).

4.3 Mapowanie rundy odblokowania

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
Parametr Źródło Przykład
unlock_at Sekundy uniksowe UTC wybrane przez użytkownika 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time Informacje o łańcuchu drand (genesis_time) 1595431050
chain_period_seconds Informacje o łańcuchu drand (period) 30

Operacja ceil() wybiera pierwszą rundę drand, której czas ujawnienia jest ≥ unlock_at. Zapewnia to, że qub nie staje się odszyfrowywalny przed wybranym czasem odblokowania.

Przypadek brzegowy: jeśli (unlock_at - chain_genesis_time) jest dokładnie podzielne przez chain_period_seconds, wynikiem jest dokładnie ta runda — qub odblokowuje się precyzyjnie w czasie ujawnienia tej rundy.

Walidacja: unlock_at MUSI być w przyszłości w czasie pieczętowania. unlock_at NIE MOŻE być więcej niż 10 lat od created_at (aby ograniczyć ryzyko zależności od drand w długim horyzoncie; UI POWINIEN ostrzegać dla dat odblokowania powyżej 2 lat).


5. Newtype'y formatu drutowego

Newtype'y formatu drutowego zapewniają bezpieczeństwo w czasie kompilacji przed myleniem bajtów CBOR z JSON, surowym tekstem jawnym lub innymi kodowaniami bajtów.

Typ Zawiera Produkowany przez Konsumowany przez
SealedQubCbor Kanoniczny CBOR SealedQub serialize_sealed_qub() Wysyłanie do trwałego magazynu, pobieranie widza
QubEnvelopeCbor Kanoniczny CBOR QubEnvelope serialize_qub_envelope() Wejście szyfrowania tlock, wyjście deszyfrowania tlock

5.1 Reguły konstrukcji

// 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 Walidacja przy konstrukcji

from_encoded() POWINIEN walidować, że wejście zaczyna się od prawidłowego nagłówka mapy CBOR. Pełna walidacja strukturalna odbywa się w czasie parsowania, a nie konstrukcji, aby uniknąć podwójnego parsowania.


6. Rejestr typów treści

Wartość Typ Maks. rozmiar ciała Uwagi
0x00 Zarezerwowane (nieprawidłowe) NIE MOŻE być używane
0x01 Tekst jawny (UTF-8, ograniczony Markdown) 50 KB płatne / 10 KB darmowe Zobacz §10 dla reguł renderowania. Podział darmowy / płatny jest egzekwowany przez usługę uploadu; twarda granica warstwy protokołu to 50 KB.
0x02 Zarezerwowane (przyszłe) Przydzielone dla przyszłego typu treści; nieprawidłowe w v1. Widzowie MUSZĄ odrzucić zgodnie z regułą poniżej.
0x03 Pact (umowa bilateralna, ciało CBOR) 100 KB Ciało to kanoniczny CBOR PactTerms (§6.1). Współpodpisywanie cosignera zgodnie z §9.7.
0x04 Werdykt (samoocena twórcy, ciało CBOR) 8 KB Ciało to kanoniczny CBOR VerdictBody (§6.2). Emitowane wyłącznie przez systemowy intent verdict. Powiązanie z qubem rodzicielskim jest na tagu Arweave Parent-Tx-Id, nie w ciele. Zobacz verdict-uplift-plan §3.4.

Widzowie MUSZĄ odrzucać nieznane typy treści z wyraźnym błędem widocznym dla użytkownika. Widzowie NIE MOGĄ próbować renderować nieznanych typów jako tekstu.

6.1 Ciało paktu (content_type = 0x03)

Ciało paktu to kanoniczne kodowanie CBOR wartości 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)> }

Kanoniczne kolejności kluczy CBOR dla wszystkich trzech map są podane w §3.2. Całkowity serializowany CBOR paktu NIE MOŻE przekraczać 100 KB (zgodnie z §6).

Dyskryminator schematu. Pierwszy wiersz w terms dla paktu structured/v1 MUSI być { key: "pact_schema", value: "structured/v1" }. Wiersze bez tego znacznika to pakty "custom" i nie otrzymują strukturalnej walidacji ani renderowania świadomego schematu.

Zamrożone sloty potwierdzeń. Pakty structured/v1 niosą dokładnie cztery wiersze potwierdzeń pod tymi kluczami:

"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"

value dla każdego jest jednym z ośmiu zamrożonych angielskich łańcuchów wybranych przez parę (role, kind), gdzie role ∈ { seller, buyer, provider, client } i kind ∈ { standard, capacity }. Same łańcuchy są normatywnymi danymi protokołu — podpisy ML-DSA-65 obu stron wiążą się z dokładnymi bajtami przez body_hash. NIE są one zlokalizowane; podpisane ciało jest neutralne językowo. Każda zmiana sformułowania wymaga nowej wersji schematu (structured/v2).

Osiem łańcuchów, ich wyszukiwanie (acknowledgement_for(role, kind)) i uzasadnienie dla każdego są przypięte przez implementację referencyjną. Zgodne implementacje MUSZĄ emitować bajtowo identyczne wartości potwierdzeń; testy hasha ciała SHA3-256 na fikstach złotych obejmujące wszystkie cztery kombinacje ról wychwytują wszelki drift.

Kolejność wyświetlania u widza. Łańcuchy potwierdzeń zawierają frazy takie jak "described above", co zakłada, że wiersze opisu / zakresu renderują się przed potwierdzeniami. Widzowie MUSZĄ renderować tablicę terms w kolejności CBOR; zmiana kolejności łamie semantykę prozy.

Kontakt strony przeciwnej. Gdy contact Strony B to prawidłowy adres email, usługa uploadu qub automatycznie wysyła email z zaproszeniem do recenzji / współpodpisu w czasie etapu i wiąże ewentualny współpodpis z weryfikacją tego samego adresu (§9.7). Pakty, w których kontakt Strony B jest nieobecny, mogą być nadal współpodpisane, ale tylko przez kanał poza pasmem — usługa odmawia żądań współpodpisu, które nie mogą wyprodukować pasującego 15-minutowego markera weryfikacji email.

6.2 Ciało werdyktu (content_type = 0x04)

Ciało werdyktu to kanoniczne kodowanie CBOR wartości 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
}

Kanoniczna kolejność kluczy 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)

Całkowity serializowany CBOR werdyktu NIE MOŻE przekraczać 8 KB (zgodnie z wierszem rejestru powyżej).

Enum rozstrzygnięcia. Bajt na drucie jest neutralny względem intencji; cztery kategorie Right / Partial / Wrong / Unfalsifiable pokrywają przestrzeń rozstrzygnięć każdego intentu niosącego werdykt. Etykiety zależne od intencji ("Trafione" / "Dotrzymane" / "Wydane" / "Potwierdzona" dla Right itd.) są kwestią renderowania po stronie widza, rozstrzyganą względem intencji rodzicielskiego quba — drut pozostaje neutralny językowo i intencyjnie. Wartości spoza zakresu 1..=4 MUSZĄ być odrzucane przy dekodowaniu.

Powiązanie z rodzicem. Qub werdyktu NIE niesie referencji do rodzica w swoim ciele. Identyfikator transakcji Arweave rodzicielskiego quba jest emitowany jako tag magazynowy Parent-Tx-Id w czasie uploadu (warstwa tagów magazynowych §7). Utrzymuje to ciało jako samowystarczalne podpisane oświadczenie samooceny; łańcuch audytu („racja co do czego?") jest ustanawiany przez wyszukiwanie tagu Arweave.

Bezpieczeństwo URL dowodu (normatywne). Gdy evidence_url jest obecny, walidatory (po stronie kompozycji, po stronie drutu, na krawędzi Workera) MUSZĄ wymusić:

  1. Wyłącznie HTTPS. Łańcuch MUSI zaczynać się od sekwencji bajtów https://. Każdy inny schemat — http, ftp, javascript, data, file itd. — jest odrzucany.
  2. Ograniczenie długości. ≤ 2 048 bajtów (praktyczny limit URL w przeglądarce).
  3. NFC + sprawdzenie wrogich punktów kodowych. Ta sama reguła co dla title i reflection — punkty kodowe bidi-override / zero-width / tag-block / BOM / C0 / C1 są odrzucane. Definicja zgodna z Rust crate::handle::contains_hostile_text_codepoint i TS workers/api/src/utils/unicode.ts::isHostileCodepoint (utrzymywać w zgodzie).
  4. Bez białych znaków, bez znaków sterujących ASCII. Białe znaki / DEL / bajty poniżej 0x20 w dowolnym miejscu URL są odrzucane — zamyka wektor wstrzyknięć \n/\t, którego reguła bidi nie pokrywa.
  5. Niepusty segment hosta. Wszystko między https:// a pierwszym /, ? lub # MUSI być niepuste.

Brak pobierania po stronie serwera. Worker NIE MOŻE proxować, pobierać ani podglądać URL. Protokół przechowuje łańcuch; renderowanie odbywa się po stronie widza z rel="nofollow noopener noreferrer" target="_blank" i widocznym hostem wyświetlanym obok tekstu linku.

Refleksja. Opcjonalny tekst refleksji napisany przez twórcę („co się zmieniło, czego można się nauczyć"). Ta sama walidacja NFC + wrogich punktów kodowych co dla title. Wejście puste / złożone wyłącznie z białych znaków zwija się do nieobecnego w czasie konstrukcji.

Wersja schematu. v1 wspiera wyłącznie verdict_version = 0x01. Przyszłe rewizje schematu zwiększają ten bajt i lądują wraz z nową wersją protokołu zgodnie z §12.


7. Protokół pieczętowania

Pełna sekwencja pieczętowania. Każdy krok jest normatywny.

 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.

Warstwa tagów magazynu (poza pasmem). Usługa uploadu qub dołącza celowo niewielki zestaw tagów transakcji magazynu obok zapakowanego ładunku. Content-Type=application/octet-stream jest normatywnie wymagany. Usługa referencyjna dodatkowo dołącza trzy opcjonalne tagi, gdy twórca zdecyduje się je ujawnić: Intent (walidowany allowlistą intencję komponowania — np. quote, reply, commitment), Author (odcisk klucza publicznego twórcy zgodny z §9.3 jako 64-znakowy małymi literami hex) i Parent-Tx-Id (ID transakcji magazynu rodzica qub dla łańcuchów odpowiedzi, 43-znakowy base64url).

Tag Author jest opcjonalny per qub: aplikacja referencyjna twórcy dołącza go tylko wtedy, gdy użytkownik wyraźnie włącza publiczne przypisanie w czasie pieczętowania. Gdy przełącznik jest wyłączony — domyślnie — żaden tag Author nie jest zapisywany, a qub jest nieprzypisany w łańcuchu: nic w trwałym magazynie nie łączy uploadu z uchwytem twórcy, emailem ani innymi qubami. Gdy przełącznik jest włączony, odcisk Author rozwiązuje się do wybranego @uchwytu twórcy poprzez łańcuch poświadczeń z §9.5. Relacje łańcucha odpowiedzi i Intent nie są identyfikujące. Zewnętrzna koperta (§13) chroni wewnętrzne ciało przed korelacją tekstu zaszyfrowanego — uniemożliwiając zbieraczowi rozpoznanie i masowe odszyfrowywanie uploadów o kształcie qub po opublikowaniu ich rundy drand.

Usługa referencyjna celowo NIE dołącza tagów App-Name, App-Version ani Type: każdy taki filtr jednowartościowy zwracałby cały korpus qub na zapytanie GraphQL, co jest niezgodne z zakresem poufności tylko-ciała opakowania.

Zgodny weryfikator NIE MOŻE polegać na żadnym tagu magazynu dla weryfikacji przez stronę trzecią z §11; hash ciała / qub_id / podpis wiążą się wyłącznie z wewnętrznym CBOR, nigdy ze zbiorem tagów.


8. Protokół odblokowania

Pełna sekwencja odblokowania. Każdy krok jest normatywny.

 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. Podpisywanie autorstwa

9.1 Uzasadnienie

Quby są przechowywane w trwałym magazynie. Podpisy autorstwa muszą pozostawać niemożliwe do podrobienia bezterminowo, dlatego v1.0 używa postkwantowego schematu ML-DSA-65 (FIPS 204) zamiast klasycznego schematu, którego bezpieczeństwo może osłabnąć w trwałym okresie życia qub.

9.2 Rejestr algorytmów

sig_alg Schemat Rozmiar klucza Rozmiar podpisu
0x00 Brak podpisu (niepodpisany)
0x01 ML-DSA-65 (FIPS 204) 1 952 bajty 3 309 bajtów

Widzowie MUSZĄ odrzucać nieznane wartości sig_alg.

9.3 Konstrukcja podpisanego preimage

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)

Separator domeny: "QUB_AUTHOR_SIG_V1" to 17 bajtów ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Bez wypełnienia.

Bajt końcowy: 91. bajt preimage MUSI być 0x00. Implementacja referencyjna eksponuje to jako stałą ORG_ID_PRESENT_INDIVIDUAL = 0x00 w crates/qub-core/src/signing.rs; widzowie rekonstruujący sig_input do weryfikacji MUSZĄ emitować ten sam bajt.

Zakres podpisu — co jest, a co nie jest objęte. sig_input wiąże się z czterema polami koperty: version, qub_id, body_hash, unlock_at (plus stały separator domeny i bajt org_id_present). Trzy z tych czterech są strukturalnymi niezmiennikami: qub_id sam jest wyprowadzony z version, content_type, created_at, unlock_at, outcome_at, drand_round i body_hash przez preimage z §4.1, więc jakakolwiek zmiana tych pól produkuje inne qub_id i tranzytywnie unieważnia podpis. Bezpośrednio uwierzytelniona powierzchnia jest zatem:

Pole Uwierzytelnione przez podpis Jak
version Bezpośrednie wejście do sig_input
qub_id Bezpośrednie wejście
body_hash Bezpośrednie wejście
unlock_at Bezpośrednie wejście
content_type Tranzytywnie, przez preimage qub_id
created_at Tranzytywnie, przez preimage qub_id
outcome_at Tranzytywnie, przez preimage qub_id
drand_round Tranzytywnie, przez preimage qub_id (V1.2)
body Tranzytywnie, przez body_hash = SHA3-256(body)
author_pubkey — (niejawnie) Klucz, który zweryfikował podpis, jest autorem z definicji
sender_label Tekst tylko do wyświetlenia; modyfikowalny bez złamania podpisu
reply_to Wskaźnik wątkowania; modyfikowalny bez złamania podpisu
cosigner_pubkey / cosigner_signature Niezależnie podpisane nad tym samym sig_input (zob. §9.7)
drand_chain_id, tlock_ciphertext, visibility Pola zewnętrznego SealedQub, nie wewnątrz koperty — objęte własnymi niezmiennikami strukturalnymi (spójność rundy / łańcucha), ale nie przez podpis autora. (drand_round jest teraz wiązane tranzytywnie przez preimage qub_id — zob. wyżej.)

Implikacje bezpieczeństwa pól nieuwierzytelnionych.

Implementacje wyświetlające sender_label lub reply_to użytkownikom końcowym MUSZĄ wystawiać uwierzytelnioną tożsamość (odcisk klucza publicznego, poświadczenie) jako podstawowy sygnał tożsamości, a nie etykietę.

9.4 Procedura weryfikacji

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."

Weryfikacja podpisu jest najbardziej kosztowną operacją (szczególnie ML-DSA-65). POWINNA być wykonywana po przejściu wszystkich tańszych kontroli (hash, qub_id, unlock_at).

9.5 Poświadczenia tożsamości

Poświadczenia tożsamości — mapowanie author_pubkey na rozpoznawalne przez człowieka roszczenia tożsamości takie jak uchwyt qub, adres email, uchwyt społecznościowy lub poświadczenie passkey — są progresywnym ulepszeniem po stronie widza i nie są wymagane do weryfikacji podpisu. Widzowie, którzy rozwiązują poświadczenia do tożsamości wyświetlanej, MUSZĄ stosować precedencję:

handle > email > social > fingerprint

Fallback odcisku to małymi literami hex z SHA3-256(author_pubkey); jest zawsze dostępny dla dowolnego podpisanego qub. Widzowie MOGĄ go skrócić do wyświetlania — referencyjny widz renderuje qub: z następującymi czterema pierwszymi i ostatnimi bajtami (qub:<8 hex>…<8 hex>).

Zgodny weryfikator może wykonać każdą kontrolę z §9.4 bez kontaktowania się z API qub, bez żadnej sieci poza trwałym magazynem i drand i bez żadnego wyszukiwania po stronie serwera. Rozwiązanie poświadczenia jest osobnym krokiem best-effort wykonywanym tylko po pomyślnej weryfikacji podpisu.

9.6 Wpływ na rozmiar

Ed25519 ML-DSA-65
Podpis 64 bajty 3 309 bajtów
Klucz publiczny 32 bajty 1 952 bajty
Razem na qub 96 bajtów 5 261 bajtów
Delta kosztu magazynu (przy ~5 USD/MB) ~$0,0005 ~$0,026

Dla qub tekstowego 500–2 000 bajtów ML-DSA-65 mniej więcej potraja przechowywany rozmiar. Bezwzględny koszt jest pomijalny.

9.7 Weryfikacja współpodpisującego (umowy bilateralne typu pact)

Dla umów bilateralnych (content_type = 0x03) druga warstwa podpisu dowodzi, że obie strony wyraziły zgodę na te same warunki.

Pola koperty:

Oba pola MUSZĄ być obecne razem albo oba nieobecne. Jeśli obecne jest dokładnie jedno, widzowie MUSZĄ zgłosić błąd integralności.

Procedura weryfikacji:

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."

Właściwości:

Brama wiązania email (operacyjna). Gdy zaetapowany pakt niesie kontakt email Strony B (§6.1), usługa uploadu qub MUSI odmówić żądania współpodpisu, chyba że istnieje krótkotrwały marker weryfikacji email pasujący zarówno do id etapu, jak i hash znormalizowanego email tego kontaktu. Marker jest zapisywany przez /api/v1/auth/verify gdy token magic-link niesie staging_id, a zweryfikowany adres pasuje do SHA-256(normalise_email(party_b.contact)) — gdzie normalise_email(addr) zachowuje wielkość liter części lokalnej i obniża do małych liter tylko część domeny (zgodnie z RFC 5321 §2.3.11), a SHA-256 tutaj to hash NIST FIPS 180-4 (różny od SHA3-256 używanego w derywacjach z §4) — i wygasa 900 sekund (15 minut) po wystawieniu. Jest to operacyjna brama anty-podszywania, NIE część dowodu qub on-chain — weryfikator strony trzeciej odtwarzający §11 potrzebuje tylko trwałego magazynu i drand, bez żadnego wyszukiwania po stronie serwera. Marker istnieje tylko po stronie serwera i nigdy nie jest częścią podpisanego ciała.

Wpływ na rozmiar (ML-DSA-65 autor + współpodpisujący):

Komponent Rozmiar
Podpis autora 3 309 bajtów
Klucz publiczny autora 1 952 bajty
Podpis współpodpisującego 3 309 bajtów
Klucz publiczny współpodpisującego 1 952 bajty
Całkowity narzut kryptograficzny 10 522 bajty
Delta kosztu magazynu ~$0,05

10. Renderowanie i sanityzacja Markdown

Ta sekcja jest krytyczna dla bezpieczeństwa. Widz renderuje quby tekstowe (content_type = 0x01) używając ograniczonego podzbioru Markdown.

10.1 Dozwolone elementy

10.2 Zakazane elementy

Element Obsługa
Surowy HTML (<div>, <script>, itd.) Całkowicie usuwany. Żaden HTML nie przechodzi.
Obrazy (![alt](url)) Usuwane. Składnia obrazów jest usuwana z wyjścia.
Linki ([text](url)) URL renderowany jako widoczny tekst jawny. Bez auto-linkowania. Nie klikalny bez wyraźnej akcji użytkownika.
Niebezpieczne schematy URL javascript:, data:, vbscript:, file: — usuwane.
Iframe'y, embedy, obiekty Usuwane.
Encje HTML Dekodowane do znaków wyświetlanych tylko jeśli bezpieczne.

10.3 Implementacja

Implementacje MUSZĄ używać ścisłego parsera z allowlistą, a nie blocklistą. Zalecane podejście:

  1. Sparsuj Markdown używając pulldown-cmark (lub odpowiednika).
  2. Przejdź AST i odrzuć każdy węzeł nieobecny na allowliście (§10.1).
  3. Dla węzłów linków: emituj URL jako widoczny tekst, a nie jako klikalny element <a>.
  4. Skonwertuj przefiltrowane AST na typowaną reprezentację pośrednią (np. enum MarkdownNode z tylko bezpiecznymi wariantami). Surowy HTML jest strukturalnie niereprezentowalny w tym IR.
  5. Renderuj z typowanego IR do docelowej warstwy widoku (np. reaktywnych komponentów widoku, węzłów DOM). Bez konkatenacji łańcuchów HTML ani innerHTML w żadnym momencie.

Podejścia blocklist są kruche, ponieważ nowe rozszerzenia Markdown lub osobliwości parsera mogą wprowadzać niefiltrowane elementy. Podejście typowanego AST sprawia, że XSS jest strukturalnie niemożliwy — nie ma wariantu, który mógłby nieść dowolny HTML.

10.4 Limity rozmiaru i struktury


11. Weryfikacja przez stronę trzecią

Każda strona trzecia może zweryfikować publiczny qub bez współpracy qub. Procedura weryfikacji:

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.

Co weryfikacja dowodzi:

Dowód Co ustanawia
Zobowiązanie Tekst zaszyfrowany istniał do znacznika czasu bloku magazynu.
Integralność Ciało tekstu jawnego pasuje do zobowiązanego hasha i nie zostało zmienione.
Czas Treść była nieczytelna do rundy drand, która odpowiada wybranemu czasowi odblokowania (z zastrzeżeniem założeń bezpieczeństwa tlock i drand).

Czego weryfikacja NIE dowodzi:

Niedowód Dlaczego
Autorstwo sender_label jest dekoracyjny. Bez sig_alg0x01, ktokolwiek mógł zapieczętować tę treść.
Intencja qub dowodzi treści i czasu, a nie tego, co twórca subiektywnie miał na myśli.
Czas przed zdarzeniem Inkluzja bloku magazynu może opóźnić rzeczywisty upload o minuty. Znacznik czasu zobowiązania to czas bloku, a nie moment, w którym użytkownik nacisnął "pieczętuj".

12. Wersjonowanie

12.1 Wersja protokołu

Pole version (u8) zarówno w SealedQub, jak i QubEnvelope identyfikuje główną wersję protokołu.

12.2 Historia wersji

Wersja Wartość Opis
v1 0x01 Publiczne quby tekstowe (content_type 0x01), umowy bilateralne typu pact (0x03, schemat structured/v1, autor + współpodpisujący ML-DSA-65), tlock, SHA3-256

12.3 Kompatybilność wprzód

Widz v1 napotykający QubEnvelope z nieznanymi kluczami opcjonalnej mapy CBOR (klucze nieobecne w kanonicznej kolejności z §3.2) POWINIEN zignorować te klucze i kontynuować weryfikację używając znanych pól. Pozwala to na przyszłe drobne dodatki (np. nowe metadane) bez wymagania głównego skoku wersji.

Widz v1 napotykający sig_alg = 0x01 (ML-DSA-65), ale pozbawiony wsparcia weryfikacji ML-DSA-65 POWINIEN wyświetlić treść qub z notą "podpis obecny, ale nieweryfikowalny", a nie odrzucać qub całkowicie. Implementacja referencyjna dzisiaj odrzuca każdą wartość sig_alg inną niż 0x00 i 0x01, ponieważ rejestr v1 nie zawiera innego ważnego algorytmu — ścisłe odrzucenie i miękka porażka są obserwacyjnie identyczne aż do zarejestrowania trzeciego algorytmu. Powyższe zachowanie miękkiej porażki staje się nośne, gdy §9.2 dopuści nowy wpis, a widz referencyjny zostanie zaktualizowany do miękkiej porażki w tym momencie.

12.4 Wersja zewnętrznej koperty

OuterWrapper opisany w §13 niesie własny bajt version, niezależny od SealedQub.version i QubEnvelope.version. Te dwie przestrzenie wersji ewoluują oddzielnie: przyszły postkwantowo bezpieczny zamiennik symetryczny zwiększa bajt opakowania bez dotykania wewnętrznej wersji protokołu, a przyszły dodatek warstwy protokołu (np. nowe pole koperty) zwiększa wewnętrzną wersję bez dotykania bajtu opakowania.

OUTER_WRAPPER_VERSION_* Wartość Algorytm Status
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM z liczbą jednorazową 12 bajtów, znacznikiem uwierzytelnienia 16 bajtów, AAD powiązanym z qub_id Domyślny v1
0x020xFF Zarezerwowane Przyszłość

Widzowie MUSZĄ odrzucać nieznane wersje opakowania z wyraźnym błędem. Protokół celowo utrzymuje przestrzeń wersji opakowania wąską aż do pojawienia się konkretnego sterownika migracji (np. wytyczne NIST faworyzujące inny AEAD); slot 0x02 zostanie przydzielony w tej samej rewizji, która wprowadza algorytm.


13. Zewnętrzna koperta szyfrowania

13.1 Uzasadnienie

Warstwy protokołu (QubEnvelope → tlock → SealedQub) sprawiają, że zapieczętowany qub jest zablokowany czasowo: ciało jest nieczytelne do unlock_at i opublikowania podpisu rundy drand. Jednak po odblokowaniu podpis rundy jest publiczny, a kanoniczny kształt CBOR SealedQub jest rozpoznawalny, więc zbieracz, który zaindeksował transakcje trwałego magazynu, mógłby masowo odszyfrować cały korpus qub.

Zewnętrzna koperta szyfrowania zamyka ten kanał, wstawiając dodatkową warstwę symetryczną AEAD między kanonicznym SealedQubCbor a bajtami wysłanymi do trwałego magazynu. 256-bitowy klucz K żyje tylko we fragmencie URL linku udostępniania i na urządzeniach użytkowników; przeglądarki nie transmitują fragmentów URL do serwerów, więc qub.social, każda brama magazynu i każdy CDN przed którymkolwiek z nich są obserwacyjnie ślepi na K. Każdy qub w trwałym magazynie jest zatem nieprzezroczystym tekstem zaszyfrowanym, którego tekst jawny jest nie do odzyskania bez URL, który twórca zdecydował się udostępnić.

Efekt netto:

13.2 Warstwowanie

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)

Pieczętowanie i odblokowanie w warstwie protokołu (§7, §8) są niezmienione poniżej granicy opakowania; opakowanie podłącza się w miejscu wywołania seal() i odłącza w miejscu wywołania unlock().

13.3 Struktura danych 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
}

Niezmienniki pól.

Kodowanie CBOR. Kanoniczny CBOR zgodnie z §3, z tą samą regułą uporządkowania kluczy (sortowane rosnąco po długości zakodowanych bajtów, następnie leksykograficznie). Cztery klucze to:

Klucz Zakodowane bajty Kolejność
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

Pierwszy bajt CBOR OuterWrapper to zatem nagłówek mapy o określonej długości dla 4-wpisowej mapy (0xA4).

13.4 Powiązanie AAD z qub_id

Opakowanie wiąże qub_id jako dodatkowe dane uwierzytelnione AEAD. Jest to nośna strukturalna obrona przeciwko trzem klasom ataków:

Atak Obrona
Przenieś tekst zaszyfrowany pod innym polem qub_id w opakowaniu Niezgodność AAD → uwierzytelnienie AEAD zawodzi
Wymieszaj fragment URL qub A z bajtami trwałego magazynu qub B Niezgodność AAD → uwierzytelnienie AEAD zawodzi
Manipuluj polem qub_id opakowania po uploadzie Niezgodność AAD → uwierzytelnienie AEAD zawodzi

Niesienie qub_id w tekście jawnym opakowania nie osłabia istotnie odporności na enumerację — qub_id sam jest hashem SHA3-256 preimage z §4.1 bez odzyskiwalnego preimage ze skrótu, a enumerator, który już zebrał bajty opakowania, nie dowiaduje się z widocznego qub_id niczego, czego nie mógłby wywnioskować z samego istnienia uploadu.

13.5 Algorytmy opakowania i rozpakowania

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

Kolaps trybu porażki. Niewłaściwy K, niewłaściwa liczba jednorazowa, niezgodność AAD i sfałszowany tekst zaszyfrowany — wszystkie produkują ten sam błąd DECRYPT_FAILED. Jest to celowa właściwość AEAD: rozróżnianie trybu porażki tworzyłoby kanał boczny, który zdalny atakujący mógłby sondować, wysyłając zniekształcone opakowania i mierząc czas odpowiedzi. Implementacje referencyjne MUSZĄ zwijać wszystkie porażki AEAD do jednego kształtu błędu.

13.6 Materiał klucza i dystrybucja

Klucz opakowujący K to 256-bitowa wartość losowa jednostajna generowana per qub przez CSPRNG. Implementacje referencyjne pozyskują go z:

Dystrybucja: K MUSI być zakodowany jako URL-bezpieczny base64 (RFC 4648 §5, bez wypełnienia) i dołączony do linku udostępniania jako komponent fragmentu:

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

Fragment nigdy nie jest transmitowany do żadnego serwera przez zgodną przeglądarkę. Kanały odzyskiwania (indeks historii po stronie serwera, opcjonalne automatyczne wysyłanie email) trwale zachowujące pełny link udostępniania — wraz z fragmentem — poza urządzeniem użytkownika są wyraźnym kompromisem względem domyślnej postawy krypto-niszczenia i MUSZĄ być bramkowane wyraźną zgodą użytkownika.

Utrata fragmentu. Jeśli użytkownik zgubi fragment URL i nie ma kanału odzyskiwania, qub jest nieczytelny. To jest nośny kompromis projektu i MUSI być ujawniony użytkownikowi w czasie pieczętowania. MVP wzmacnia ujawnienie w czasie pieczętowania wyraźnym tekstem "zachowaj ten URL" i kanałem odzyskiwania z weryfikowanym email dla użytkowników, którzy się zdecydują.

13.7 Poza zakresem dla tej sekcji

13.8 Publiczne quby (pominięcie opakowania)

Zewnętrzne opakowanie jest opcjonalne w warstwie dostarczania. Twórca może zapieczętować qub jako publiczny — wtedy kanoniczny SealedQubCbor jest zapisywany do trwałego magazynu bezpośrednio, bez warstwy OuterWrapper i bez klucza K:

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

Publiczny qub jest zablokowany czasowo, ale nie bramkowany linkiem: pozostaje nieczytelny do opublikowania jego rundy drand (warstwa tlock jest niezmieniona), lecz po odblokowaniu każdy, kto ma arweave_tx_id, może go odszyfrować — żaden fragment URL nie jest wymagany, ponieważ nie ma K. To celowy kompromis dla powierzchni, którymi musi sterować serwer: e-maile z powiadomieniem o ujawnieniu, osadzenia stron trzecich oraz bogatsze SEO po ujawnieniu — wszystkie potrzebują linku działającego bez sekretu, którego serwer nigdy nie posiada (§13.6).

Konsekwencje, które producent MUSI uwzględnić:

Prywatny (opakowany) pozostaje wartością domyślną; publiczny jest wyraźnym wyborem twórcy dla pojedynczego quba.


14. Wektory testowe

14.1 Derywacja 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

Implementacje MUSZĄ produkować identyczne wartości body_hash i qub_id dla tego wejścia. Ten wektor testowy POWINIEN być pierwszym napisanym testem jednostkowym. Kanoniczne wartości powyżej zostały obliczone przez implementację referencyjną i MUSZĄ pasować bit-w-bit. Historyczne układy preimage (przed uruchomieniem — żaden żywy qub nie zależał od tych wartości): qub_id V1.0 o długości 92 bajtów wynosił 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; qub_id V1.1 o długości 100 bajtów (po wpleceniu outcome_at_or_zero) wynosił b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 wplata drand_round i podbija separator domeny do QUB_ID_V2.

14.2 Mapowanie rundy odblokowania

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 Round-trip kanonicznego CBOR

Implementacje MUSZĄ zweryfikować, że serialize(parse(serialize(qub))) == serialize(qub) dla wszystkich prawidłowych wejść. To jest test właściwości, a nie pojedynczy wektor.

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)

Kanoniczne bajty CBOR i body_hash SHA3-256 są obliczane przez implementację referencyjną. Implementacje MUSZĄ produkować bajtowo identyczny CBOR dla tego wejścia.

Implementacje MUSZĄ także zweryfikować, że serialize(parse(serialize(pact))) == serialize(pact) dla wszystkich prawidłowych wejść PactTerms (test właściwości).

14.5 Wektory wieloryzykowe zewnętrznej koperty

Zewnętrzna koperta (§13) ma osobną kanoniczną fikturę w crates/qub-core/tests/vectors/wrapper_v1.json. Każdy przypadek ustala krotkę (key, nonce, qub_id, sealed_cbor) jako nieprzezroczyste wejścia hex i potwierdza określone wyjście expected_wrapper_hex. Obie implementacje referencyjne konsumują ten sam plik JSON:

Fikstura obecnie przypina trzy przypadki:

Przypadek Pokrycie
basic-text-public Najmniejszy realistyczny kształt SealedQub; bez pól opcjonalnych. Ustanawia kanoniczny kształt opakowania dla typowego qub v1.0.
with-recipient-pubkey SealedQub z ustawionym recipient_pubkey (ścieżka Fazy 2). Inny zbiór kluczy wewnętrznego CBOR, inny qub_id.
longer-body Ciało ~4 KiB — ćwiczy wielobajtowe prefiksy długości CBOR wewnątrz zarówno wewnętrznej koperty, jak i zewnętrznego tekstu zaszyfrowanego.

Implementacje MUSZĄ produkować bajtowo identyczny expected_wrapper_hex dla zapisanych wejść. Regeneracja fikstury wymaga QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors i jest zarezerwowana dla celowych zmian formatu.


15. Zarządzanie profilem kryptograficznym (przyszłość)

Ta sekcja jest informacyjna dla v1 i staje się normatywna przy pierwszym wprowadzeniu drugiego algorytmu do dowolnego z prymitywów kryptograficznych qub.

15.1 Obecna postawa

Protokół v1 wiąże dokładnie jeden algorytm na prymityw:

Weryfikatory obecnie hardcodują długości klucza i podpisu na prymityw. Format na drucie nie eksponuje żadnej powierzchni zwinności.

15.2 Zamierzony kształt

Gdy drugi algorytm wejdzie do protokołu, weryfikator zostanie skonfigurowany dla nazwanego CryptoProfile (np. ExqubV1) wymieniającego dokładny zbiór dozwolonych wartości na prymityw — sig_algs, łańcuchy drand, wersje opakowania, typy treści. Profil jest ustalony w czasie weryfikacji, nigdy negocjowany w paśmie. Każda wartość poza aktywnym profilem jest odrzucana.

Gwarantuje to, że dodanie ML-DSA-87 lub aktywacja Ed25519 nie może retroaktywnie osłabić istniejących konfiguracji weryfikatora: weryfikator v1 pozostaje weryfikatorem v1 nawet po opublikowaniu profilu v2.

15.3 Warunki wyzwalające

Promuj §15 do statusu normatywnego, gdy zaproponowane jest którekolwiek z poniższych:

Do tego czasu §15 jest placeholderem, który ustala kształt migracji, aby przyszłe PR-y lądowały przy znanym celu, a nie ponownie spierały się o powierzchnię negocjacji od zera.