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.
Nieznane klucze Odrzucenie z błędem. Parsery NIE MOGĄ tolerować kluczy mapy spoza kanonicznego zestawu kluczy danego typu — dwa różne kanoniczne łańcuchy bajtów nigdy nie mogą dekodować się do tej samej wartości (encode(decode(x)) == x), a w podpisanych ładunkach dodatkowy klucz byłby ukrytą treścią, z którą wiążą się oba podpisy. Ewolucja schematu odbywa się przez version, nigdy przez dodatkowe klucze.
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. W swoim ciele qub werdyktu NIE niesie referencji do rodzica. 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

Istniały dwie wersje preimage. Wszystkie podpisy MUSZĄ używać V2, a weryfikatory MUSZĄ akceptować wyłącznie V2. Starszy preimage V1 (udokumentowany poniżej w celach historycznych) był akceptowany jako awaryjny wariant wyłącznie do weryfikacji podczas migracji do V2; ten wariant awaryjny został wycofany, a podpis oparty wyłącznie na V1 jest teraz odrzucany.

V2 (aktualna — produkowana przez każde nowe podpisywanie przez autora oraz przez oba podpisy w przepływie etapowania / współpodpisywania paktu):

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V2"  ||    // domain separator (17 bytes)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 bytes)
    body_hash            ||    // [u8; 32] (32 bytes)
    unlock_at            ||    // i64 big-endian (8 bytes)
    0x00                 ||    // u8 (1 byte): MUST be 0x00 in v1.x
    sender_label_hash    ||    // [u8; 32]: SHA3-256(NFC(sender_label)),
                               //   or 32 zero bytes when absent
    reply_to_or_zero           // [u8; 32]: parent qub_id, or 32 zero
                               //   bytes when absent
)

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

signature = Sign(author_secret_key, sig_input)

sender_label_hash stosuje tę samą konwencję sentinela nieobecności co title_hash (§4.2.1): 32 zerowe bajty nie są prawidłowym wyjściem SHA3-256, więc „nieobecność" nigdy nie może kolidować z obecną etykietą. Wszystkie pola mają stałą szerokość, więc preimage jest jednoznaczny bez prefiksów długości.

V1 (starsza — WYCOFANA; już nieprodukowana i już nieakceptowana przy weryfikacji):

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

Preimage V1 pomijał sender_label i reply_to. Był akceptowany jako awaryjny wariant wyłącznie do weryfikacji podczas migracji do V2; ten wariant awaryjny został od tego czasu wycofany — weryfikatory MUSZĄ akceptować wyłącznie preimage V2. Definicja jest zachowana tutaj w celach historycznych oraz aby wyjaśnić separator domeny poniżej. Podpis, który weryfikuje się wyłącznie względem V1, MUSI być traktowany jako niepowodzenie weryfikacji.

Separatory domeny: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" to po 17 bajtów ASCII każdy ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Bez wypełnienia. Różniący się separator rozdziela domenowo obie konstrukcje, więc podpis nad jednym preimage nigdy nie może zweryfikować się jako drugi.

Bajt org_id_present: bajt następujący po unlock_at 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 V2 wiąże się bezpośrednio z polami version, qub_id, body_hash, unlock_at, sender_label i reply_to (plus stały separator domeny i bajt org_id_present). 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
sender_label Bezpośrednie wejście przez sender_label_hash (preimage V2 — jedyna akceptowana forma)
reply_to Bezpośrednie wejście przez reply_to_or_zero (preimage V2 — jedyna akceptowana forma)
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
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.)

Dlaczego V2 jest jedynym akceptowanym preimage.

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 (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
   only accepted form — the legacy V1 fallback is retired (§9.3), so a
   signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."

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 (V2 only — the
   legacy V1 fallback is retired; all pact clients produce V2 signatures).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."

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 mapy CBOR (klucze nieobecne w kanonicznej kolejności z §3.2) MUSI go odrzucić z błędem dekodowania (§3.1). Kompatybilność wprzód opiera się na polu version, a nie na tolerancji kluczy: przyszłe dodatki — nawet drobne metadane — są dostarczane pod nową wartością version, którą widz v1 odrzuca z wyraźnym błędem "nowszy protokół", zamiast po cichu odrzucać treść, z którą wiążą się podpisy.

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.


16. Dziennik transparentności i poziomy trwałości (projekt — przegląd ukończony)

Status. Ta sekcja jest specyfikacją projektową. Formaty na drucie, haszowanie i model zaufania poniżej są normatywne dla implementacji, ale żaden kod dziennika transparentności nie został jeszcze dostarczony. Zewnętrzny przegląd W5 jest ukończony: §16.15 odnotowuje rozstrzygnięte decyzje oraz wiążące ograniczenia startowe, które z niego wynikły. Implementacja może postępować w ramach tych ograniczeń. §16 pozostaje wyprzedzająca w tym samym sensie co §15 — ustala cel, aby implementacja wylądowała przy ustabilizowanym projekcie, a nie wyprowadzała ponownie modelu zaufania podczas przeglądu kodu. Jest ściśle addytywna — każdy istniejący qub zachowuje swoją indywidualną transakcję Arweave i nie ma zmiany w formacie na drucie SealedQub / QubEnvelope.

16.1 Uzasadnienie i poziomy trwałości

Dzisiaj trwałość quba i jego zobowiązanie czasowe opierają się oba na pojedynczej transakcji Arweave per qub (§11). Sprzęga to opóźnienie pieczętowania z finalnością Arweave, czyni upload per qub limitem kosztu produktu (ARWEAVE_DAILY_CEILING) i nie daje uporządkowania odpornego na manipulację między qubami. Dziennik transparentności dodaje dwie warstwy pod tym pojedynczym poziomem i wokół niego:

Poziom Nazwa Gwarancja Kiedy
T1 Synchroniczne potwierdzenie z R2 na pierwszym miejscu Podłoga trwałości — zapieczętowane bajty są zapisywane do trwałego magazynu, zanim pieczętowanie zwróci wynik (< 300 ms p95). Każdy qub, synchronicznie (§16.10).
T2 Wsadowa inkluzja w dzienniku transparentności Uniwersalne zobowiązanie tylko-do-dopisywania, odporne na manipulację + całkowite uporządkowanie, zakotwiczone w Arweave. Każdy qub, odroczone + wsadowane (§16.5–16.7).
T3 Trwałość Arweave per qub Indywidualna transakcja Arweave dla quba. Płatny upsell oraz awaryjne rozwiązanie na wypadek niedostępności Arweave (§16.8).

T2 czyni Arweave per qub wyborem (T3), a nie jedyną ścieżką trwałości. ARWEAVE_DAILY_CEILING zostaje wycofany jako limit produktu i zdegradowany do bezpiecznika obwodu wyłącznie na dedykowanym portfelu kotwicy (§16.7); pieczętowania użytkowników nigdy nie są odrzucane za jego przekroczenie.

Uczciwość trwałości (rozstrzygnięte — §16.15 Q6). Trwałość nie ulega regresji: zapis T1 do R2 jest synchroniczny i jednorazowy, więc qub na poziomie darmowym, który nie kupił T3, jest w pełni trwały w chwili, gdy pieczętowanie zwróci wynik. Co ulega zgrubieniu, to dowodliwy czas zobowiązania w górnej granicy: dla darmowego quba staje się on czasem bloku kotwicy, a nie czasem bloku transakcji per qub. Przy niskim wolumenie — realistycznym stanie wczesnego startu i poza szczytem — pełna dzienna kadencja jest typową podłogą, a nie rzadkim przypadkiem brzegowym. Ramowanie produktu jest zatem górną granicą bez zobowiązanego liczbowo opóźnienia — „zapieczętowane i trwałe teraz; niezależny publiczny znacznik czasu jest dodawany przy następnej kotwicy dziennika (zwykle codziennie)" — a dowód zobowiązania co do dokładnej godziny jest płatną właściwością T3, ujawnioną na powierzchni porównania poziomów oraz w regulaminie (§16.11, §16.15 Q6). Każda granica czasowa jest wyłącznie wewnętrznym SLO, nigdy reklamowanym SLA.

16.2 Struktura LogLeaf (dwa zobowiązane kształty)

Wpis dziennika to LogLeaf, zakodowany jako ręcznie pisany kanoniczny CBOR w profilu §3.1 (określona długość, brak tagów, brak liczb zmiennoprzecinkowych, liczby całkowite w najkrótszej formie, tekst NFC, pola opcjonalne pominięte gdy nieobecne, klucze uporządkowane rosnąco po długości zakodowanych bajtów, następnie leksykograficznie). Strażnik kanoniczny §3.1 parse → re-encode → compare jest stosowany na ścieżce kodowania przed haszowaniem (nie tylko przy dekodowaniu), więc dwie implementacje nie mogą nie zgadzać się co do bajtów liścia przez różnicę w szerokości liczby całkowitej lub kolejności kluczy. Wszystkie liczby całkowite to u8 / u64 / i64; wszystkie skróty to 32-bajtowe ciągi bajtów (bstr[32]). Przechowywany identyfikator transakcji Arweave to surowy 32-bajtowy skrót SHA-256 niesiony jako bstr[32], nigdy jako ciąg tekstowy base64url (zgodnie z §3.3).

Liść ma dwa kształty wybierane przez bajt kind, ponieważ na domyślnej ścieżce uploadu Worker jest ślepy na bajty: POST /api/v1/upload otrzymuje tylko qub_id i unlock_at jako niezaufane asercje klientabody_hash, drand_round, created_at i drand_chain_version są wszystkie zapieczętowane wewnątrz zewnętrznej koperty §13, której klucza Worker nigdy nie posiada. Tylko ścieżka pieczętowania po stronie serwera (POST /api/v1/seal) wyprowadza body_hash / drand_round z tekstu jawnego. Pojedynczy kształt liścia niosący body_hash + drand_round zobowiązywałby zatem do wartości, których operator nigdy nie zweryfikował dla większości realnych qubów. Podział utrzymuje każdą zobowiązaną wartość uczciwą:

Klucz Dł. zakod. Typ Obecność Znaczenie
seq 4 u64 wymagane Globalny indeks liścia liczony od 0; pozycja, do której zobowiązuje się dowód inkluzji.
kind 5 u8 wymagane 0x01 poświadczony (pieczętowanie po stronie serwera) lub 0x02 asertowany (pieczętowanie po stronie klienta / upload ślepy na bajty).
ref 4 bstr[32] wymagane Identyfikator referencyjny liścia. Poświadczony → surowy qub_id. Asertowany → zaślepiony identyfikator SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1).
chash 6 bstr[32] wymagane Adres treści SHA3-256(stored_bytes) — jedyne powiązanie z treścią, które Worker może zawsze uczciwie obliczyć, na obu ścieżkach.
unlock_at 10 i64 wymagane Skopiowane (poświadczony) lub asertowane (asertowany); walidowane > 0 przed wejściem do liścia.
received_at 12 i64 wymagane Zegar ścienny Workera w chwili potwierdzenia z R2. Niedowodowe (asertowane przez operatora; §16.6). Obecne dla samoopisu, nigdy jako dowód. Walidowane > 0.
body_hash 10 bstr[32] tylko kind=0x01 Pominięte przy 0x02 — Worker go nie posiada pod §13.
drand_round 12 u64 tylko kind=0x01 Pominięte przy 0x02.

Liść kind=0x02 celowo nie zobowiązuje ani do body_hash, ani do drand_round: poświadcza zobowiązanie i uporządkowanie nieprzezroczystego tekstu zaszyfrowanego pod adresem treści chash, twierdząc qub_id i unlock_at — a nie jego tekst jawny czy rundę. Człony tekstu jawnego/rundy dla asertowanego quba pochodzą z istniejącej weryfikacji pakietu .qub z §11, a nie z dziennika (§16.11). drand_chain_version nie znajduje się w liściu (na domyślnej ścieżce jest wewnątrz koperty); granularność łańcucha żyje na kotwicy (§16.7). Dyscyplina kodera: odrzuć ref lub chash złożone z samych zer oraz odrzuć niedodatnie unlock_at / received_at, odzwierciedlając strażnik sentinela outcome_at > 0 w cbor.rs.

16.2.1 Zaślepianie prywatnego quba

Dziennik nie może stać się wyrocznią enumeracji, której zapobiega zewnętrzna koperta §13 (§13.1). Dla prywatnego (opakowanego) quba liść asserted zobowiązuje się do zaślepionego identyfikatora SHA3-256(qub_id ‖ log_blind_secret), gdzie log_blind_secret jest sekretem przechowywanym przez serwer, i pomija body_hash. Strona trzecia nie może powiązać takiego liścia z konkretnym qub_id; posiadacz quba, który ma link udostępniania i tym samym qub_id, może ponownie obliczyć zaślepienie, aby potwierdzić własną inkluzję. Publiczny qub (już enumerowalny, już niosący tag Arweave Visibility: public zgodnie z §13.8) zobowiązuje się do surowego qub_id. Jest to jedyne miejsce, w którym samodzielna weryfikowalność celowo ustępuje nośnemu niezmiennikowi prywatności; samodzielnym powiązaniem dla prywatnych qubów jest chash (§16.9).

Kustodia log_blind_secret (rozstrzygnięte — §16.15 Q4). Zaślepienie chroni niełączliwość liści, a nie poufność tekstu jawnego (kopertę §13 trzyma to niezależnie). Przy kompromitacji log_blind_secret dla dowolnego qub_id, który przeciwnik już posiada lub może odtworzyć (każdy qub, którego pakiet/URL posiada, plus każdy qub_id o niskiej entropii lub publiczny), ponownie oblicza ref liścia w jednym haszu i go łączy — jest to bezpośrednie powiązanie znanej populacji, a nie atak siłowy nad nieznaną przestrzenią. Klasyfikuj log_blind_secret jako sekret rangi korelacji/Sybil w tej samej warstwie kustodii co inne sekrety serwera i rotuj wyłącznie w przód (rotacja ponownie zaślepia przyszłe liście; nie może retroaktywnie rozłączyć już zakotwiczonych).

16.3 Haszowanie liści i węzłów

Haszowanie z separacją domen RFC 6962 §2.1 z SHA-256 zastąpionym przez SHA3-256:

leaf_hash      = SHA3-256(0x00 || canonical_cbor(LogLeaf))
node_hash(l,r) = SHA3-256(0x01 || l || r)
empty tree     = SHA3-256("")          // defined but never anchored

Bajty prefiksu domeny 0x02 (łańcuch wpisów, §16.4) i 0x03 (skrót STH, §16.6) są zarezerwowane i rozłączne z powyższymi. Są pojedynczymi bajtami i tym samym nie mogą kolidować z istniejącymi 10-bajtowymi separatorami domeny ASCII (QUB_ID_V2 itd.). Drzewo to lewostronnie pełne niezrównoważone drzewo RFC 6962 (każdy podział wewnętrzny przy największej potędze dwójki ściśle mniejszej niż liczba liści poddrzewa), co pozwala dowodom inkluzji i spójności współdzielić jeden algorytm ścieżki audytowej. Specyfikacja referencyjna niesie jawny pseudokod derywacji lewo/prawo i ustala wektor testowy o liczbie liści niebędącej potęgą dwójki (5 liści), aby przypadek promocji prawej krawędzi — który wektor 4-liściowy ukrywa — został przećwiczony.

16.4 Łańcuchowanie haszy (wewnętrzne)

LogDO utrzymuje wewnętrzny łańcuch wpisów wyłącznie dla spójności po awarii. Nigdy nie jest publikowany i nigdy nie jest widoczny dla weryfikatora:

entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1]  = SHA3-256("QUB_TLOG_GENESIS_V1")

Publikowanym autorytetem tylko-do-dopisywania jest kumulatywny korzeń Merkle + jego kotwica (§16.5–16.6), nigdy surowa kolejność, w jakiej operator akurat serwuje liście: łańcuch przelicza się na nowo dla dowolnej serwowanej kolejności, więc tylko zakotwiczony korzeń ustala kanoniczną pozycję.

16.5 Kumulatywne drzewo Merkle i wsadowanie

Istnieje jedno wiecznie rosnące drzewo RFC 6962 nad wszystkimi liśćmi w kolejności seq — a nie izolowane drzewa per wsad. (Konstrukcja per wsad łańcuchowana liściem przeniesienia została odrzucona: nie jest prawdziwą relacją prefiksu, więc jej „dowody spójności" są niepoprawne.) Kumulatywne drzewo daje autentyczne dowody spójności RFC 9162 i pozwala pojedynczej niedawnej kotwicy udowodnić inkluzję dla dowolnego starszego quba.

Obiekt Durable Object LogDO jest jedynym piszącym (blockConcurrencyWhile, odzwierciedlając QuotaDO / EntitlementDO) — dopisywanie do współdzielonego dziennika to odczyt-modyfikacja-zapis na współdzielonym stanie i dlatego MUSI przechodzić przez DO, nigdy przez KV. Buforuje frontier prawej krawędzi drzewa (O(log n) haszy), więc zamknięcie wsadu to O(batch). Wsad to zbiór liści zakotwiczonych razem; jego wyzwalacze są konfigurowalne, nie zamrożone w protokole: przyrost tree_size o co najmniej LOG_BATCH_MAX_LEAVES (domyślnie 4096), lub osiągnięcie przez wiek kadencji kotwiczenia, lub wymuszone opróżnienie, gdy ląduje płatne pieczętowanie T3. root_i to kumulatywny skrót drzewa Merkle (Merkle Tree Hash) nad liśćmi 0 .. tree_size_i.

16.6 Podpisana głowa drzewa przez kotwicę Arweave

Transakcja kotwiczenia Arweave jest podpisaną głową drzewa (Signed Tree Head) i zastępuje podpis operatora dla samej głowy drzewa: dzienna kotwica nie potrzebuje żadnego klucza quba, ponieważ podpisem jest owner transakcji Arweave. Teza fosy się utrzymuje — nośny dla zakotwiczonego korzenia jest niezmienny substrat, a nie sekret trzymany przez qub.

W projekcie istnieje dokładnie jeden ciepły klucz podpisujący quba i jest on przypięty: klucz potwierdzenia per pieczętowanie (§16.10). Jego klucz publiczny jest zobowiązany w LogProfile (dystrybuowanym z weryfikatorem) oraz podpisany krzyżowo przez anchor_owner, więc weryfikator waliduje potwierdzenie wobec tego samego przypiętego korzenia co kotwica. Jest to rozstrzygnięcie §16.15 Q2 — nieprzypięty, rotowalny przez operatora klucz potwierdzenia byłby zaprzeczalny (operator mógłby zaprzeczyć, że klucz był jego), co unieważniłoby wartość rozliczalności potwierdzenia wobec przeciwnika na poziomie operatora, którego potwierdzenie ma odstraszać. Zatem: qub nie trzyma żadnego nieprzypiętego klucza podpisującego dziennik; klucz potwierdzenia jest przypięty i podpisany krzyżowo przez anchor_owner.

SignedTreeHead to kanoniczny CBOR (klucze po długości zakodowanej): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (poprzedni sth_hash; genesis = 32 bajty zerowe), log_id:bstr[32], first_seq:u64, anchored_at:i64. Jego skrót to sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).

Przypięty korzeń zaufania. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Zgodny weryfikator MUSI wymagać anchor_tx.owner == LogProfile.anchor_owner, gdzie anchor_owner (oraz klucz publiczny klucza potwierdzenia) jest wbudowany w qub_core jako LogProfile — obok stałych quicknet już obecnych w DrandTimelockProvider::quicknet() — i dystrybuowany z binarką weryfikatora. Weryfikator MUSI również zweryfikować powiązanie dane transakcji Arweave → tx_id lokalnie, zamiast ufać odpowiedzi /raw/ z bramy. Zamyka to dziurę ekwiwokacji nieuczciwego portfela: „zakotwiczone w Arweave" jest bez znaczenia, dopóki weryfikator nie przypnie, który portfel.

Rotacja jest rozszerzeniem zarządzania §15, a nie ponownym użyciem (rozstrzygnięte — §16.15 Q3). Powierzchnia profilu §15.2 obecnie wymienia tylko sig_algs / łańcuchy drand / wersje opakowania / typy treści, a lista wyzwalaczy §15.3 nie wymienia żadnego z nich — LogProfile / anchor_owner nie jest jeszcze w powierzchni §15. Zarządzanie rotacją musi zatem zostać zbudowane: §15.3 jest rozszerzone (poniżej) o dodanie wyzwalacza LogProfile, a rotacja to podpisany bump LogProfile dostarczony w aktualizacji weryfikatora. Planowana rotacja niesie podpis krzyżowy wychodzący → przychodzący; rotacja wywołana kompromitacją nie może (wychodzący klucz jest niezaufany/niedostępny właśnie wtedy) i wraca do bumpu rządzonego przez §15, przy czym kontrola rozgałęzienia poprzedniej kotwicy (poniżej) ogranicza szkody w międzyczasie.

Okno ekwiwokacji (parametr zaufania pierwszej klasy). Liść jest odporny na ekwiwokację dopiero, gdy obejmująca go kotwica jest potwierdzona w Arweave. Okno to received_at → potwierdzenie kotwicy (≤ kadencja + finalność Arweave). W jego obrębie jedynymi gwarancjami są przypięte potwierdzenie pieczętowania (§16.10) i operacyjna integralność quba. Trzy artefakty rozliczalności czynią to uczciwym, a nie machaniem ręką (model świadka jest rozstrzygnięciem §16.15 Q2):

  1. Przypięte podpisane potwierdzenie pieczętowania — analog SCT zwracany w odpowiedzi uploadu (§16.10), podpisany przypiętym, podpisanym krzyżowo przez anchor_owner kluczem potwierdzenia. Liść porzucony przed swoją kotwicą pozostawia ofierze niezaprzeczalne potwierdzenie do opublikowania, zamykając dziurę cichego pominięcia.
  2. Opublikowana metodologia monitora + przejście łańcucha prev — łańcuch prev kotwicy jest przechodzony od głowy do genesis; rozgałęzienie (dwie kotwice o jednym size z różnym root lub złamane prev) jest publikowalnym dowodem niewłaściwego postępowania. Wykrywanie ekwiwokacji jest deklarowanym zobowiązaniem operacyjnym, a nie cichym założeniem.
  3. Podwójnie samodzielnie publikowane głowy — każda nowa głowa {sth_hash, tree_size} jest publikowana w dedykowanym, należącym do qub publicznym repozytorium GitHub tylko-do-dopisywania (nośny człon samodzielnej publikacji odpornej na manipulację), z postem w mediach społecznościowych jako jedynie najlepszym wysiłkiem potwierdzającym. Nieudana publikacja MUSI wywołać alarm (a nie zawieść cicho).

Granica uczciwości (wiążące ograniczenie). Ponieważ qub kontroluje obie powierzchnie publikacji, jest to samodzielnie publikowane, a nie niezależnie zaświadczone. Żadna powierzchnia produktu, marketingu ani prawna nie może twierdzić, że dziennik jest „niezależnie zaświadczony"; dozwolonym twierdzeniem jest, że ekwiwokacja jest wykrywalna i pozostawia niezaprzeczalne potwierdzenie. Prawdziwy niezależny świadek strony trzeciej jest odroczony do przyszłego bumpu zarządzania §15.

received_at jest asertowane przez operatora i żadne twierdzenie nie może się na nim opierać — nigdy nie jest pokazywane jako dowód ani jako potwierdzenie sporu na żadnej powierzchni produktu / prawnej / API / renderowania dowodu. Czas bloku kotwicy Arweave T jest jedynym beztrwałym znacznikiem czasu wymagającym zaufania (górna granica „zalogowano przez"). Każda kontrola poprawności monitora na received_at MUSI porównywać wobec T, a nie wobec kontrolowanego przez operatora pola STH anchored_at; taka kontrola jest zabezpieczeniem wyłącznie przed błędem zegara uczciwego operatora, a nie kontrolą rozliczalności wobec złośliwego operatora (§16.15 Q5).

16.7 Format transakcji kotwicy i kadencja

AnchorBundle to ciało transakcji Arweave w kanonicznym CBOR, zapisywane przez bundler z §16.8: ver:u8, sth:bstr (kanoniczne bajty SignedTreeHead), prev_anchor:bstr (surowe bajty identyfikatora poprzedniej transakcji kotwicy; pominięte przy genesis), chain_hash:tstr (obowiązujący łańcuch drand — quicknet) oraz strumień CBOR liści wsadu w kolejności seq, aby kotwica była samodzielna: monitor wyprowadza ponownie root z ciała bez żadnej zależności od qub. (Jeśli strumień liści stanie się duży przy wysokim wolumenie, przyszła rewizja może zobowiązywać się tylko do zakresu liści przez referencję; odnotowane, nieprzyjęte w v1.)

Tagi Arweave są celowo enumerowalne — dziennik ma być znajdowany, w przeciwieństwie do prywatnych qubów: App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. Tagi są niezaufanymi wskazówkami; ciało CBOR jest jedynym autorytetem.

Kadencja: domyślnie codzienna, rewidowana wraz z wolumenem (wyzwalacz rozmiaru automatycznie skraca efektywną kadencję pod obciążeniem); płatne pieczętowanie T3 wymusza kotwicę, aby płacący klienci nigdy nie czekali doby. Portfel kotwicy jest dedykowany i o niskiej prędkości, oddzielony od portfela uploadu — MUSI być jego własnym JWK (osobny klucz, a nie logiczna rola na portfelu uploadu), aby kompromitacja portfela uploadu nie mogła sfałszować kotwic — z twardym dziennym budżetem transakcji kotwiczenia (zdegradowany ARWEAVE_DAILY_CEILING). Postawa kustodii jest stwierdzona wprost: wąsko zakresowany ciepły klucz z ciasnym bezpiecznikiem obwodu i niskim saldem, a nie „zimny" — portfel, który automatycznie podpisuje codziennie, nie może być zimny, a specyfikacja tego nie udaje.

16.8 Bundler ANS-104

Własny enkoder DataItem ANS-104 i podpisujący deep-hash, mniej więcej 300 linii, wyłącznie Web Crypto, zero zależności npm (oba SDK Turbo nie przechodzą bramy łańcucha dostaw npm ci --ignore-scripts). Układ bajtów DataItem:

signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data

Podpisywanie to Arweave deepHash — rekurencyjny skrót SHA-384 (wymóg na drucie Arweave, crypto.subtle.digest("SHA-384")) nad ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — następnie RSA-PSS nad deep hashem kluczem JWK portfela przez crypto.subtle; id = base64url(SHA-256(signature)). Tutejszy SHA-384 jest odizolowany jako prymityw wyłącznie dla drutu Arweave, nigdy jako prymityw zaufania quba (§15 odnotowuje to ogrodzenie; haszowanie zaufania quba to SHA3-256 wszędzie).

Jedna ścieżka kodu obsługuje trzech konsumentów: płatną trwałość T3 per qub, awaryjne rozwiązanie na wypadek niedostępności Arweave (zakolejkuj DataItem, zwróć potwierdzenie z R2 na pierwszym miejscu niezależnie — zamyka to obecny ślepy zaułek 503 ARWEAVE_UNAVAILABLE) oraz zapis AnchorBundle. Schemat podpisu (rozstrzygnięte — §16.15 Q8): v1 podpisuje przez RSA-PSS (typ podpisu 1) ponownie wykorzystując istniejący mechanizm JWK portfela Arweave (zero nowej kustodii długowiecznego klucza, służąc tezie „o jeden sekret mniej"); Ed25519 jest odroczony do ścieżki migracji PQ z §15.

Ręcznie napisany deep hash jest kodem o najwyższym ryzyku i najniższym naturalnym pokryciu w W5, więc jego bramkowanie jest nienegocjowalne (§16.15 Q8):

  1. Wielojęzyczny fixture tlog_v1.json (Rust + TS, wzorzec wrapper_v1.json z §14.5) pokrywa deep-hash, bajty + id DataItem, hasze liści, korzeń 5-liściowy + ścieżkę audytową, skrót STH, dowód inkluzji i dowód spójności — w obu kierunkach: podpisu i weryfikacji (kierunek weryfikacji ma znaczenie, ponieważ lokalna kontrola transakcji → tx_id z §16.6 wciąga deep hash do każdego samodzielnego weryfikatora, a nie tylko piszącego).
  2. Jednorazowy interoperacyjny round-trip przez referencyjny bundler ANS-104, konsumowany wyłącznie jako statyczne dane testowe — nigdy jako zależność uruchomieniowa npm (postawa wyłącznie-Web-Crypto / brak skryptów instalacyjnych pozostaje w mocy).
  3. Ścieżka deep-hash + RSA-PSS musi wykonać round-trip przez te same prymitywy crypto.subtle, których używa produkcja, aby własny enkoder był bajtowo kompatybilny.
  4. Ciągły monitor akceptacji po wsadzie potwierdza, że każdy DataItem kotwicy / awaryjny faktycznie osiąga akceptację Arweave, z alarmem + bezpiecznikiem obwodu — ponieważ deep hash obsługuje również awaryjną kolejkę na wypadek niedostępności Arweave, więc cicha regresja wypełniłaby tę kolejkę elementami odrzuconymi przez sieć dokładnie podczas tej awarii, którą ma pokrywać.

16.9 Dowody inkluzji i spójności

Oba są RFC 9162, SHA3-256, serwowane jako kanoniczny CBOR.

InclusionProofGET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (dokładny CBOR liścia — weryfikator sam przelicza leaf_hash i nigdy nie ufa dostarczonemu skrótowi), index:u64, size:u64, audit:[bstr[32]], root:bstr[32], anchor:{ txid:bstr[32], batch:u64, sth:bstr[32], log_id:bstr[32], block_height?:u64, anchored_at?:i64 }.

ConsistencyProofGET /api/v1/log/consistency?first=<size_a>&second=<size_b>: ver:u8, first_size:u64, second_size:u64, first_root:bstr[32], second_root:bstr[32], nodes:[bstr[32]], first_anchor, second_anchor. Pojedyncza jednoznaczna lista kluczy, ustalona wektorem testowym.

Samodzielna weryfikacja (brak serwera qub, rozszerza §11):

1.  Parse .qub bundle → SealedQub; recompute qub_id (§4.1).
2.  Read leaf.kind.
3a. kind=0x01 (attested):
      assert leaf.ref == qub_id
      assert leaf.body_hash   == SHA3-256(body)
      assert leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserted):
      assert leaf.ref == SHA3-256(qub_id || blind)   // holder supplies blind
      OR treat ref as opaque and bind via leaf.chash == SHA3-256(stored_bytes)
4.  Recompute leaf_hash = SHA3-256(0x00 || leaf); fold `audit` per RFC 6962
    using index/size; require derived root == proof.root.
5.  Fetch anchor.txid from any gateway; verify the tx data → tx_id binding
    (do not trust a gateway /raw/ response); REQUIRE anchor_tx.owner ==
    LogProfile.anchor_owner.
6.  Parse AnchorBundle; require committed root == proof.root and size ==
    proof.size; read the Arweave block time T.
7.  Emit the claim scoped by leaf.kind (§16.11).

Magazyn serwujący dowody MUSI być kluczowany współrzędnymi (rozstrzygnięte — §16.15 Q7, blokujący warunek wstępny). Generowanie dowodu dla zimnego liścia jest neutralne dla poprawności tylko jeśli materiał audytowy w R2 jest trwałym magazynem węzłów Merkle kluczowanym absolutną współrzędną drzewa (level, index) — a nie deltami węzłów per batch. Przy magazynie kluczowanym współrzędnymi dowolna ścieżka audytowa (leaf i, size N) to zbiór O(log N) bezpośrednich GET-ów R2 z brakiem przeliczania przez granice wsadów; przy magazynie kluczowanym wsadem tak nie jest, co stanowi lukę układu magazynu, którą to rozstrzygnięcie zamyka. Ciała liści są podobnie adresowalne treścią przez seq. Wektor testowy W5 MUSI udowodnić zimny liść z epoki genesis wobec znacznie późniejszego korzenia używając wyłącznie R2 + Arweave przy wymazanym magazynie LogDO, aby twierdzenie o bezpieczeństwie odzysku z §16.13 było poparte, a nie tylko asertowane. O(log N) sekwencyjnych GET-ów R2 należy wyłącznie na asynchronicznym punkcie końcowym dowodu — nigdy na gorącej ścieżce pieczętowania (§16.10) ani na cronie per tick.

16.10 Uporządkowanie potwierdzenia z R2 na pierwszym miejscu

Sekwencja POST /api/v1/upload staje się:

  1. Bramy pierwszej połowy (uwierzytelnienie, walidacja, klucz sharda idempotencji) — bez zmian.
  2. Synchronicznie await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes) — podłoga trwałości; zamyka również wyścig precache z W1 (dawniej ctx.waitUntil po wysyłce do Arweave).
  3. Synchronicznie await LogDO.append(leaf) — jedno RPC DO w kolo; jedyny piszący przydziela seq, rozszerza łańcuch wpisów i aktualizuje frontier. (RMW na współdzielonym stanie → DO, nigdy KV.) RPC append robi wyłącznie to — praca O(batch) zamknięcia wsadu Merkle biegnie poza tym RPC, na alarmie LogDO, albo p95 append skacze przy co LOG_BATCH_MAX_LEAVES-tym pieczętowaniu.
  4. Zwróć potwierdzenie teraz — z potwierdzeniem pieczętowania (podpisanym przypiętym kluczem potwierdzenia, §16.6) oraz { tx_id, log_seq, anchor_status: "pending" }. Wielosekundowy fan-out Arweave jest usunięty ze ścieżki krytycznej.
  5. Jedno ctx.waitUntil kolejkuje pracę odroczoną: wysyłkę Arweave per qub (teraz najlepszy wysiłek / płatna; przy niepowodzeniu trafia do awaryjnej kolejki bundlera, zamiast zwracać użytkownikowi 503) plus istniejące zapisy prowizorycznych metadanych. Zamknięcie wsadu i kotwiczenie biegną niezależnie z alarmu LogDO i dziennego cronu kotwicy. Brak ctx.waitUntil wewnątrz pętli; istniejący klucz sharda idempotencji jest zachowany.

Budżet opóźnienia (rozstrzygnięte — §16.15 Q7). Cel < 300 ms p95 jest mierzoną bramą startową, a nie założeniem. Uczciwa ścieżka krytyczna to odczyty KV pierwszej połowy + jeden PUT R2 + dwa zserializowane Durable Objects — istniejące obciążenie limitu pieczętowania QuotaDO oraz nowy append LogDO — więc budżet musi uwzględniać dwa przebiegi DO w kolo, a nie jeden. Dostarcz alarm opóźnienia LogDO odzwierciedlający QuotaDO i traktuj regresję p95 jako blokadę wydania.

16.11 Model zaufania — precyzyjne twierdzenie, zakresowane rodzajem liścia

Dla kind=0x01 (poświadczony): „Ta treść — ciało pasujące do body_hash, identyfikowane przez qub_id — została zobowiązana do dziennika tylko-do-dopisywania qub na pozycji seq i istniała nie później niż czas bloku Arweave T; była kryptograficznie nieczytelna do rundy drand R = unlock_round(unlock_at)." Jest to pełna trójka {powiązanie rundy tlock + inkluzja Merkle + zakotwiczony korzeń}.

Dla kind=0x02 (asertowany, domyślny): „Nieprzezroczysty tekst zaszyfrowany o adresie treści chash, twierdzący qub_id i unlock_at, został zobowiązany do dziennika tylko-do-dopisywania na pozycji seq i istniał nie później niż czas bloku Arweave T." Człony rundy i ciała są dostarczane przez istniejącą weryfikację pakietu .qub z §11 (qub_core::unlock), a nie przez dziennik; co dziennik dodaje ponad gołą transakcję per qub, to uporządkowanie odporne na manipulację, beztrwałościowy czas zobowiązania w górnej granicy i odporność na ekwiwokację.

Oba twierdzenia wykluczają, zgodnie z §11: autorstwo bez sig_alg ≥ 0x01, intencję i czas o granularności poniżej kotwicy. Żadne nie pozwala, by jakiekolwiek twierdzenie opierało się na received_at.

Sufit twierdzenia (wiążące ograniczenie startowe — rozstrzygnięte §16.15 Q1). Dla darmowych / domyślnych (kind=0x02) qubów zakresowane twierdzenie kind=0x02 powyżej jest sufitem tego, co jakakolwiek powierzchnia produktu, marketingu, regulaminu czy renderowania dowodu może asertować. Żadna powierzchnia nie może stwierdzać ani sugerować, że dziennik dowodzi treści lub rundy odblokowania domyślnego quba — dziennik dowodzi uporządkowania + beztrwałościowego czasu zobowiązania w górnej granicy nieprzezroczystego tekstu zaszyfrowanego. Dowód treści i rundy pochodzi wyłącznie z istniejącej weryfikacji pakietu .qub z §11, która jest niezależna od dziennika. Jest to twarda blokada startowa na copy, a nie preferencja stylistyczna; jest to rozstrzygnięcie, które utrzymuje uczciwą domyślną ścieżkę ślepą na bajty.

16.12 Wersjonowanie i koordynacja W3

Nie ma bumpu na drucie SealedQub i tym samym nie ma bumpu wersji protokołu (§12.1): dziennik jest sidecar-em zobowiązującym się do istniejących pól i bajtów, więc nie wchodzi do historii wersji protokołu z §12.2. Opcjonalne drand_chain_version z W3 jest nietknięte i pozostaje jedynym opcjonalnym polem SealedQub. Dziennik wprowadza natomiast własne niezależne przestrzenie wersji — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — odzwierciedlając niezależność wersji opakowania z §12.4 (opakowanie niesie bajt wersji niezależny od wersji protokołu, a wersje dziennika podążają za tą samą separacją).

Dostarczanie dowodu jest domyślnie pobierane, z opcjonalnym dołączeniem. Dowód nie może istnieć w czasie pieczętowania (kotwica nie została jeszcze zapisana), więc pakiet .qub z czasu pieczętowania pozostaje bez dowodu. Weryfikator z W7 pobiera GET …/proof raz, albo w trybie w pełni offline rekonstruuje dowód z publicznego AnchorBundle przez zapytanie Arweave o Log-Id. Pakiet .qub (W7) rezerwuje opcjonalny człon inclusion_proof — nieobecny przy pieczętowaniu, wypełniany przez ponowny eksport po zakotwiczeniu dla zimnej archiwizacji — podążając za tym samym wzorcem „opcjonalne, domyślnie pominięte, addytywne" co drand_chain_version z W3.

16.13 Retencja

Okna retencji dla otwartego ogona LogDO, podłoża R2 serwującego dowody, liczników bezpiecznika obwodu kotwicy i awaryjnej kolejki bundlera są wyspecyfikowane w docs/DATA-RETENTION.md. Zasada: gorący magazyn dziennika per wpis (LogDO) jest odzyskiwalny po zakotwiczeniu; jego materiał audytowy — magazyn węzłów Merkle kluczowany współrzędnymi (level, index) + ciała liści adresowane przez seq (§16.9) + kotwice Arweave — jest trwały. Odzyskanie zimnego liścia z DO nigdy nie unieważnia wydanego dowodu, ponieważ dowód rozwiązuje się wobec tego trwałego magazynu węzłów R2 i kotwicy Arweave, a nie DO (a wektor testowy wymazanego DO z §16.9 to dowodzi).

16.14 Wektory testowe

W5 dostarcza wielojęzyczny fixture tlog_v1.json (§16.8) plus wypracowane wektory: liść kind=0x01 i liść kind=0x02leaf_hash; 5-liściowy kumulatywny korzeń; jeden dowód inkluzji; jeden dowód spójności; jeden AnchorBundle; i jeden id DataItem. Żyją one obok wektorów zewnętrznego opakowania z §14.5 i są przećwiczone zarówno przez implementację Rust (qub-core), jak i TypeScript (Worker).

16.15 Decyzje przeglądu (W5 — rozstrzygnięte)

Zewnętrzny przegląd W5 (adwersaryjny przegląd projektu + akceptacja właściciela) jest ukończony. Każda decyzja poniżej jest ustalona i odzwierciedlona w tekście §16 powyżej; wiążące ograniczenia startowe są powtórzone na końcu. Implementacja może postępować w ich ramach.

  1. Uczciwość liścia ścieżki domyślnej (kind=0x02) — ROZSTRZYGNIĘTE. Dostarcz podział na dwa rodzaje liścia zgodnie ze specyfikacją: kind=0x02 nie zobowiązuje się ani do body_hash, ani do drand_round. Brak pola *_body_hash na ścieżce ślepej na bajty (byłby najbardziej czytelnym fałszywym sygnałem „zweryfikowane" dla integratorów i jest wygodą, którą §11 już dostarcza z pakietu). Nie wymagaj pieczętowania po stronie serwera dla qubów poświadczonych dziennikiem (zmusiłoby to tekst jawny przez Workera i zniszczyło fosę krypto-niszczenia). Każdy samoopisowy skrót należy do pakietu .qub / koperty dowodu jako pole przeliczane przez weryfikatora, nigdy jako pole liścia. Potwierdzony przez właściciela sufit twierdzenia: §16.11.
  2. Rozliczalność ekwiwokacji / pominięcia — ROZSTRZYGNIĘTE. Klucz potwierdzenia pieczętowania jest przypięty w LogProfile + podpisany krzyżowo przez anchor_owner (zamykając poprzednią sprzeczność „brak klucza podpisującego"; §16.6). Startowy model świadka: przypięte potwierdzenie + metodologia monitora + przejście łańcucha prev + podwójnie samodzielnie publikowane głowy (publiczne repozytorium GitHub należące do qub, media społecznościowe najlepszym wysiłkiem), reklamowane jako wykrywalne + potwierdzone, nigdy niezależnie zaświadczone. Prawdziwy świadek strony trzeciej jest odroczony do bumpu zarządzania §15.
  3. Przypięty korzeń zaufania anchor-owner + rotacja — ROZSTRZYGNIĘTE. Przyjmij przypięcie LogProfile (§16.6); weryfikator sprawdza anchor_tx.owner == anchor_owner i weryfikuje powiązanie dane transakcji → tx_id lokalnie. Zarządzanie rotacją to rozszerzenie do zbudowania w §15 (dodany wyzwalacz §15.3), a nie ponowne użycie; planowane rotacje podpisują krzyżowo, rotacje wywołane kompromitacją wracają do bumpu §15 przy kontroli rozgałęzienia ograniczającej szkody.
  4. Zaślepianie liścia prywatnego quba — ROZSTRZYGNIĘTE. Zachowaj zaślepianie dla prywatnych qubów (ref = SHA3-256(qub_id ‖ log_blind_secret)), surowy qub_id dla publicznych qubów (już §16.2.1), chash jako samodzielne powiązanie. log_blind_secret to sekret rangi korelacji/Sybil, wyłącznie rotacja w przód (§16.2.1).
  5. received_at — ROZSTRZYGNIĘTE. Zachowaj go w liściu, zobowiązany, ale wyraźnie niedowodowy; nigdy nie pokazywany jako dowód ani potwierdzenie sporu na żadnej powierzchni. Każda kontrola poprawności monitora porównuje wobec czasu bloku Arweave T, a nie kontrolowanego przez operatora anchored_at (§16.6).
  6. Dowodliwy czas na poziomie darmowym — ROZSTRZYGNIĘTE (akceptacja właściciela). Trwałość nie ulega regresji; zgrubieniu do czasu bloku kotwicy ulega tylko dowodliwy czas zobowiązania w górnej granicy. Copy poziomu darmowego nie używa liczbowego SLA („…dodawany przy następnej kotwicy dziennika, zwykle codziennie"); dowód co do dokładnej godziny jest płatną właściwością T3, ujawnioną na powierzchni porównania poziomów + w regulaminie (§16.1).
  7. Kumulatywne drzewo na Workers — ROZSTRZYGNIĘTE. Pojedyncze kumulatywne drzewo RFC 9162 + jedyny piszący LogDO z buforowanym frontierem (wygodny zapas wobec pułapu ~1k zapisów/s DO; odrocz sharding Merkle-of-shard-roots, dopóki się do niego nie zbliżysz). Blokujący warunek wstępny: magazyn węzłów R2 kluczowany współrzędnymi (level, index) + wektor testowy zimnego liścia z wymazanym DO (§16.9); < 300 ms to mierzona brama startowa nad dwoma zserializowanymi DO (§16.10).
  8. Schemat podpisu ANS-104 + deep-hash — ROZSTRZYGNIĘTE. RSA-PSS (typ podpisu 1, ponownie wykorzystujący dedykowany JWK portfela kotwicy); Ed25519 odroczony do ścieżki PQ z §15. Ręcznie napisany deep hash SHA-384 jest bramkowany na dwukierunkowym fixture między implementacjami, kontroli interoperacyjności wyłącznie statycznej z referencyjnym bundlerem, round-tripie współdzielonym crypto.subtle i monitorze akceptacji Arweave po wsadzie (§16.8).

Wiążące ograniczenia startowe (przenieś do implementacji + przeglądu produktu/prawnego):