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:
- Zmiana dowolnego pola w QubEnvelope (body, znaczniki czasu, typ treści, wersja) wytwarza inny qub_id.
- qub_id jest obliczany przed szyfrowaniem. Zarówno QubEnvelope, jak i SealedQub niosą ten sam qub_id. Widz weryfikuje ich zgodność po odszyfrowaniu.
- qub_id nie zależy od
sender_label,author_signatureaniauthor_pubkey. Oznacza to, że ta sama treść zapieczętowana w tym samym czasie produkuje ten sam qub_id niezależnie od tego, kto ją podpisuje. - Zmiana
titlew SealedQub (przy zachowaniu pozostałych wartości) zmieniaqub_idprzeztitle_hash. Brama nie może więc podmienić tytułu jawnego wyświetlanego na odliczaniu bez unieważnienia tożsamości qub. - Zmiana
outcome_atw SealedQub (przy zachowaniu pozostałych wartości) zmieniaqub_idprzez preimage. Brama nie może podmienić daty werdyktu wyświetlanej na odliczaniu przed ujawnieniem bez unieważnienia tożsamości qub. - Zmiana
drand_round(przy zachowaniu pozostałych wartości) zmieniaqub_idprzez preimage. Brama nie może powiązać ponownie tekstu zaszyfrowanego szyfrowania czasowego z inną rundą bez unieważnienia tożsamości qub; w połączeniu ze sprawdzeniem rundy stanzy w czasie odblokowania z §8 wyświetlaneunlock_atjest rundą, która faktycznie bramkuje odszyfrowywanie.
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ć:
- Wyłącznie HTTPS. Łańcuch MUSI zaczynać się od sekwencji bajtów
https://. Każdy inny schemat —http,ftp,javascript,data,fileitd. — jest odrzucany. - Ograniczenie długości. ≤ 2 048 bajtów (praktyczny limit URL w przeglądarce).
- NFC + sprawdzenie wrogich punktów kodowych. Ta sama reguła co dla
titleireflection— punkty kodowe bidi-override / zero-width / tag-block / BOM / C0 / C1 są odrzucane. Definicja zgodna z Rustcrate::handle::contains_hostile_text_codepointi TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(utrzymywać w zgodzie). - Bez białych znaków, bez znaków sterujących ASCII. Białe znaki / DEL / bajty poniżej
0x20w dowolnym miejscu URL są odrzucane — zamyka wektor wstrzyknięć\n/\t, którego reguła bidi nie pokrywa. - 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.
- W wycofanym preimage V1 strona z dostępem do zapisu przechowywanych bajtów mogła zamienić
sender_label(„Alice" → „Mallory") lub przekierowaćreply_todo innego rodzica — i ponownie zaszyfrować po rundzie — bez unieważniania podpisu autora, ponieważ żadnego z tych pól nie było w podpisanym preimage. V2 obejmuje oba, więc jakakolwiek zmiana któregokolwiek z pól przełącza weryfikację na „niepowodzenie". Ponieważ weryfikatory akceptują teraz wyłącznie V2, ta zamiana jest zamknięta dla każdego podpisu: podpis, który nie wiąże żadnego z tych pól (tzn. weryfikuje się wyłącznie względem V1), jest wprost odrzucany, a nie akceptowany przez obniżenie wersji. author_pubkeywewnątrz koperty pozostaje prawdziwą kotwicą tożsamości — widzowie MUSZĄ wyprowadzać tożsamość wyświetlaną zauthor_pubkey(przez warstwę poświadczeń z §9.5) zamiast ufaćsender_label.
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:
cosigner_pubkey: Klucz publiczny ML-DSA-65 współpodpisującego (Strona B).cosigner_signature: Podpis nad tym samymsig_inputco autor (§9.3).
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:
- Współpodpisujący podpisuje identyczny
sig_inputjak autor — obie strony wiążą się z tym samymqub_id,body_hashiunlock_at(a w V2 również z tym samymsender_label_hashireply_to_or_zero). - Aby umożliwić kontrahentowi rekonstrukcję preimage V2 bez dostępu do surowych bajtów koperty, usługa etapowania wymusza w czasie etapowania, że
sender_labelkoperty paktu równa siępact_terms.party_a.label, areply_tojest nieobecne. Oba warunki są spełnione dla każdego paktu klienta referencyjnego; koperty naruszające ten warunek są odrzucane podczas etapowania. - Derywacja
qub_id(§4.1) NIE zawiera pól współpodpisującego. Dodanie współpodpisującego do istniejącej koperty nie zmieniaqub_id. - Pakt może być podpisany tylko przez autora (jednostronne zobowiązanie), tylko przez współpodpisującego (nietypowe) lub przez obu (pełny dowód bilateralny).
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
- Nagłówki:
#do####(bez#####i######) - Wyróżnienie: pogrubienie (
**), kursywa (*), przekreślenie (~~) - Listy: uporządkowane (
1.) i nieuporządkowane (-,*) - Cytaty blokowe (
>) - Kod: zakresy inline (```) i bloki ogrodzone (`````)
- Linie poziome (
---) - Łamanie wierszy (dwie końcowe spacje lub pusta linia)
- Akapity
10.2 Zakazane elementy
| Element | Obsługa |
|---|---|
Surowy HTML (<div>, <script>, itd.) |
Całkowicie usuwany. Żaden HTML nie przechodzi. |
Obrazy () |
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:
- Sparsuj Markdown używając
pulldown-cmark(lub odpowiednika). - Przejdź AST i odrzuć każdy węzeł nieobecny na allowliście (§10.1).
- Dla węzłów linków: emituj URL jako widoczny tekst, a nie jako klikalny element
<a>. - Skonwertuj przefiltrowane AST na typowaną reprezentację pośrednią (np. enum
MarkdownNodez tylko bezpiecznymi wariantami). Surowy HTML jest strukturalnie niereprezentowalny w tym IR. - Renderuj z typowanego IR do docelowej warstwy widoku (np. reaktywnych komponentów widoku, węzłów DOM). Bez konkatenacji łańcuchów HTML ani
innerHTMLw ż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
- Maksymalna głębokość renderowanego nagłówka:
####(H4).#####i głębsze są renderowane jako tekst pogrubiony. - Brak limitu liczby akapitów (limity rozmiaru ciała z §6 są ograniczeniem).
- Bloki kodu ogrodzonego: bez podświetlania składni w MVP. Renderowane jako monospace preformatowany tekst.
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_alg ≥ 0x01, 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.
- Widzowie MUSZĄ odrzucać nieznane wersje główne z wyraźnym błędem.
- W obrębie znanej wersji głównej dekodery MUSZĄ odrzucać nieznane klucze mapy (§3.1) — ewolucja schematu odbywa się przez wprowadzenie nowej wartości
version, a nie przez dodawanie kluczy, które istniejące dekodery by pomijały. (Wcześniejsze rewizje tej specyfikacji pozwalały tolerować nieznane pola opcjonalne; ta klauzula zostaje wycofana — czyniłaencode(decode(x))nieiniektywnym i otwierała wektor ukrytej podpisanej treści w ładunkach paktów.) - Typy treści (
content_type) i schematy podpisów (sig_alg) są bramkowane wersją: nowe wartości mogą być wprowadzane tylko wraz z nową wersją protokołu lub wyraźną aktualizacją rejestru.
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 |
| — | 0x02–0xFF |
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:
- Domyślna odporność na enumerację. Zapakowane bajty w trwałym magazynie są bajtowo nierozróżnialne od dowolnego tekstu zaszyfrowanego. Strategia zbieracza "zapytaj GraphQL o uploady o kształcie qub, masowo odszyfruj publicznymi podpisami drand" nie kończy się tekstem jawnym.
- Postawa prywatności krypto-niszczenia. qub.social dosłownie nie może odszyfrować własnego korpusu. Wezwania sądowe sięgają tekstu zaszyfrowanego, nie tekstu jawnego.
- Dwupoziomowa drabina poufności. Domyślnie = dostęp kontrolowany linkiem (ta sekcja). Prywatne quby szyfrowane na odbiorcę (zarezerwowana funkcja Fazy 2, jeszcze nie wyspecyfikowana) warstwują się na wierzchu jako drugi poziom.
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.
versionMUSI równać się0x01dla bajtów opakowania v1.0.qub_idMUSI równać się poluqub_idSealedQub odzyskanego po rozpakowaniu. Krok rozpakowania nie egzekwuje tego bezpośrednio (powiązanie AEAD AAD czyni manipulację bajtową niemożliwą), ale warstwa odblokowania sprawdza relację tranzytywnie: jeśli twórca opakujeSealedQubCbor, którego wewnętrznyqub_idnie pasuje doqub_idopakowania, §8 krok 11 zawiedzie.nonceMUSI mieć 96 bitów (12 bajtów), generowane świeżo przez CSPRNG dla każdej operacji opakowania. Ponowne użycie liczby jednorazowej pod tym samym kluczem pozwala na ataki ponownego użycia liczby jednorazowej AEAD, które odzyskują tekst jawny; producenci MUSZĄ traktować pary (key,nonce) jako jednorazowe.ciphertextto wyjście AES-256-GCM: bajty tekstu zaszyfrowanego skonkatenowane z 16-bajtowym znacznikiem uwierzytelnienia.ciphertext.len() == SealedQubCbor.len() + 16dokładnie.
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:
- Twórca WASM:
getrandom(WebCrypto pod backendemwasm_js). - Wywołujący API pieczętowania po stronie serwera: jego lokalny CSPRNG; wywołujący dostarcza i zachowuje
Kjakowrapper_key_b64url. Worker używaKw pamięci na potrzeby opakowania, ale NIE MOŻE go utrwalać. Pozwala to idempotentnemu ponowieniu odzyskać zredagowaną odpowiedź z użyciem zachowanej przez wywołującego zdolności, zamiast polegać na jednorazowym sekrecie wygenerowanym przez serwer.
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
- Podpisywanie autorstwa (§9) jest niezmienione: podpisy są obliczane wewnątrz wewnętrznego
QubEnvelopei są odzyskiwane po rozpakowaniu → odszyfrowaniu tlock → parsowaniu CBOR. - Prywatne quby szyfrowane na odbiorcę (zarezerwowana funkcja Fazy 2, jeszcze nie wyspecyfikowana) komponują się na wierzchu tego opakowania jako drugi poziom poufności; oba poziomy mogą być aktywne jednocześnie.
- Pakty (§6, content_type
0x03) są opakowywane dokładnie jak quby tekstowe; opakowanie jest bajtowo ślepe na wewnętrzny typ treści.
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ć:
- Brak odporności na enumerację. Publiczne quby z założenia rezygnują z właściwości odporności na enumerację z §13.1. Referencyjna usługa uploadu znakuje je (i tylko je) tagiem trwałego magazynu
Visibility: public, dzięki czemu są celowo wykrywalne; prywatne quby nie niosą takiego tagu i zachowują swoją bajtową nierozróżnialność. - Tytuł jawny ujawniony w czasie pieczętowania. Pole
titlez §3.2 jest tekstem jawnym wewnątrzSealedQubCbor. Pod opakowaniem pozostaje ukryte, dopóki widz nie dostarczyK; bez opakowania jest czytelne dla wszystkich w trwałym magazynie od momentu uploadu, przed odblokowaniem. Zgodne aplikacje twórcy MUSZĄ ujawnić to w czasie pieczętowania. - Wykrywanie jest strukturalne. Zgodny widz/osadzenie rozróżnia oba kształty przez parsowanie: bajty, które parsują się jako
OuterWrapper, idą ścieżką rozpakowania kluczemK; bajty, które parsują się jako gołySealedQubCbor, są przyjmowane bezpośrednio. Żadna flaga na drucie nie jest wymagana, aqub_idnie wiąże widoczności — ta sama treść jest bajtowo identyczna w warstwieSealedQub, niezależnie od tego, czy zapieczętowano ją jako publiczną, czy prywatną.
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:
- Rust:
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors). - TypeScript:
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test).
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:
- Podpis: ML-DSA-65 (
sig_alg = 0x01; klucz publiczny 1952 bajtów, podpis 3309 bajtów) i niepodpisany (sig_alg = 0x00). Rejestr §9.2 nie definiuje żadnych innych wartości; weryfikator v1 MUSI odrzucić każdysig_algspoza{0x00, 0x01}. Przyszły wpis Ed25519 jest przewidywany (§15.3), ale nie jest przydzielony w v1. - Blokada czasowa: tylko drand quicknet — hash łańcucha, klucz publiczny, czas genesis i okres to stałe parametry sieci przenoszone przez referencyjny
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) iconfig/drand-endpoints.json. - Zewnętrzna koperta: tylko AES-256-GCM v1 (§13).
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:
- Drugi bajt
sig_alg(aktywacja Ed25519, ML-DSA-87 lub jakikolwiek nowy wpis w rejestrze §9). - Drugi łańcuch drand w użyciu produkcyjnym.
- Druga wersja zewnętrznego opakowania.
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 klienta — body_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):
- Przypięte podpisane potwierdzenie pieczętowania — analog SCT zwracany w odpowiedzi uploadu (§16.10), podpisany przypiętym, podpisanym krzyżowo przez
anchor_ownerkluczem potwierdzenia. Liść porzucony przed swoją kotwicą pozostawia ofierze niezaprzeczalne potwierdzenie do opublikowania, zamykając dziurę cichego pominięcia. - Opublikowana metodologia monitora + przejście łańcucha prev — łańcuch
prevkotwicy jest przechodzony od głowy do genesis; rozgałęzienie (dwie kotwice o jednymsizez różnymrootlub złamaneprev) jest publikowalnym dowodem niewłaściwego postępowania. Wykrywanie ekwiwokacji jest deklarowanym zobowiązaniem operacyjnym, a nie cichym założeniem. - 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):
- Wielojęzyczny fixture
tlog_v1.json(Rust + TS, wzorzecwrapper_v1.jsonz §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). - 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).
- Ś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. - 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.
InclusionProof — GET /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 }.
ConsistencyProof — GET /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ę:
- Bramy pierwszej połowy (uwierzytelnienie, walidacja, klucz sharda idempotencji) — bez zmian.
- Synchronicznie
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— podłoga trwałości; zamyka również wyścig precache z W1 (dawniejctx.waitUntilpo wysyłce do Arweave). - Synchronicznie
await LogDO.append(leaf)— jedno RPC DO w kolo; jedyny piszący przydzielaseq, rozszerza łańcuch wpisów i aktualizuje frontier. (RMW na współdzielonym stanie → DO, nigdy KV.) RPCappendrobi wyłącznie to — pracaO(batch)zamknięcia wsadu Merkle biegnie poza tym RPC, na alarmie LogDO, albo p95appendskacze przy coLOG_BATCH_MAX_LEAVES-tym pieczętowaniu. - 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. - Jedno
ctx.waitUntilkolejkuje 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. Brakctx.waitUntilwewną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=0x02 → leaf_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.
- Uczciwość liścia ścieżki domyślnej (
kind=0x02) — ROZSTRZYGNIĘTE. Dostarcz podział na dwa rodzaje liścia zgodnie ze specyfikacją:kind=0x02nie zobowiązuje się ani dobody_hash, ani dodrand_round. Brak pola*_body_hashna ś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. - Rozliczalność ekwiwokacji / pominięcia — ROZSTRZYGNIĘTE. Klucz potwierdzenia pieczętowania jest przypięty w
LogProfile+ podpisany krzyżowo przezanchor_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. - Przypięty korzeń zaufania anchor-owner + rotacja — ROZSTRZYGNIĘTE. Przyjmij przypięcie
LogProfile(§16.6); weryfikator sprawdzaanchor_tx.owner == anchor_owneri 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. - Zaślepianie liścia prywatnego quba — ROZSTRZYGNIĘTE. Zachowaj zaślepianie dla prywatnych qubów (
ref = SHA3-256(qub_id ‖ log_blind_secret)), surowyqub_iddla publicznych qubów (już §16.2.1),chashjako samodzielne powiązanie.log_blind_secretto sekret rangi korelacji/Sybil, wyłącznie rotacja w przód (§16.2.1). 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 ArweaveT, a nie kontrolowanego przez operatoraanchored_at(§16.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).
- 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 msto mierzona brama startowa nad dwoma zserializowanymi DO (§16.10). - 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.subtlei monitorze akceptacji Arweave po wsadzie (§16.8).
Wiążące ograniczenia startowe (przenieś do implementacji + przeglądu produktu/prawnego):
- Sufit twierdzenia (Q1/Q6). Żadna powierzchnia nie może mówić, że dziennik dowodzi treści lub rundy odblokowania domyślnego quba; dozwolonym twierdzeniem jest uporządkowane, w sposób odporny na manipulację, z beztrwałościowym czasem zobowiązania w górnej granicy. Copy znacznika czasu poziomu darmowego nie niesie liczbowego opóźnienia; dowód co do dokładnej godziny jest wyłącznie płatnym T3.
- Uczciwość świadka (Q2). Reklamuj ekwiwokację jako wykrywalną + potwierdzoną, nigdy niezależnie zaświadczoną.
- Klucze potwierdzenia + kotwicy (Q2/Q8). Klucz potwierdzenia jest przypięty + podpisany krzyżowo; portfel kotwicy to jego własny JWK odrębny od portfela uploadu.
- Brama deep-hash (Q8). Żadna kotwica ani transakcja T3 nie jest dostarczana, dopóki nie przejdą dwukierunkowy fixture + kontrola interoperacyjności; monitor akceptacji wywołuje alarm przy niepowodzeniu.
- Warunek wstępny magazynu (Q7). Magazyn węzłów kluczowany współrzędnymi + wektor zimnego liścia z wymazanym DO są warunkami wstępnymi gwarancji „odzysk nigdy nie unieważnia dowodu".