Specifica del protocollo qub
qub è un protocollo per impegni temporali crittografici: un sistema per sigillare le parole a una data futura e dimostrare, quando tale data arriva, esattamente cosa è stato detto e quando.
Tre primitive lo rendono possibile. drand è un beacon di casualità decentralizzato — la data di rivelazione è applicabile dalla fisica, non dalla buona volontà di alcuna parte. L'archivio pubblico permanente è un registro pubblico a prova di manomissione — nessuna parte può modificare o cancellare un qub una volta che è stato sigillato. ML-DSA-65 è una firma digitale post-quantistica — ogni qub è legato a una coppia di chiavi il cui segreto non lascia mai il dispositivo dell'autore.
Insieme queste primitive producono una dichiarazione che è bloccata nel tempo, a prova di manomissione e attribuibile — una ricevuta il cui valore cresce man mano che migliora la capacità del mondo di fabbricare il passato.
Il resto di questo documento è la specifica normativa richiesta per implementazioni interoperabili.
Specifica del protocollo qub
| Campo | Valore |
|---|---|
| Versione | 1.0 (versione del protocollo 0x01, versione del wrapper esterno 0x01) |
| Data | 2026-05-01 |
| Stato | Bozza |
| Revisionato fino a | 2026-05-01 |
Questo documento è la specifica normativa del protocollo per il sistema di impegni temporizzati qub. Definisce le strutture dati, le regole di serializzazione, le formule di derivazione e le procedure di verifica necessarie per implementazioni interoperabili.
Ambito: il livello del protocollo è intenzionalmente neutrale rispetto alla lingua — il corpo del qub è plaintext / markdown / byte di patto opachi, e il rendering localizzato è responsabilità del visualizzatore (app web qub.social, iframe <qub-embed>, client MCP, ecc.).
1. Notazione e convenzioni
| Notazione | Significato |
|---|---|
u8, u64, i64 |
Interi senza segno/con segno della larghezza in bit specificata |
[u8; N] |
Array di byte a lunghezza fissa di N byte |
Vec<u8> |
Array di byte a lunghezza variabile |
Option<T> |
Valore di tipo T, o assente |
String |
Stringa di testo UTF-8, normalizzata NFC |
| ` | |
SHA3-256(x) |
Hash NIST SHA3-256 della stringa di byte x (FIPS 202) |
ceil(x) |
Funzione ceiling: il più piccolo intero ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Byte più significativo per primo |
Tutti gli interi nelle costruzioni di preimage sono codificati come array di byte a larghezza fissa big-endian (i64 → 8 byte, u8 → 1 byte) salvo diversa indicazione.
Tutti i timestamp sono secondi Unix in UTC.
2. Strutture dati
2.1 ComposeQub (stato in memoria del creatore)
Non serializzato in CBOR. Non scritto nell'archivio permanente. Locale all'app del creatore.
ComposeQub {
draft_id: [u8; 16], // Casuale, generato localmente
created_at: i64, // Secondi Unix UTC
unlock_at: Option<i64>, // Secondi Unix UTC; None durante la composizione
visibility: u8, // 0x01 = pubblico (unico valore in MVP)
content_type: u8, // 0x01 = testo (unico valore in MVP)
plaintext: Vec<u8>, // Corpo del qub UTF-8
sender_label: Option<String>, // Nome visualizzato decorativo; non autenticato
status: DraftStatus, // Composing | Sealed | Uploaded | Failed
}
2.2 QubEnvelope (payload decifrato)
Serializzato utilizzando CBOR canonico (§3). Cifrato all'interno del SealedQub. Questa è la struttura che dimostra l'integrità del contenuto dopo la decifratura.
QubEnvelope {
version: u8, // Versione major del protocollo (0x01 per v1)
qub_id: [u8; 32], // Derivato (vedi §4.1)
content_type: u8, // Registro dei tipi di contenuto (vedi §6)
created_at: i64, // Secondi Unix UTC
unlock_at: i64, // Secondi Unix UTC
outcome_at: Option<i64>, // V1.1 — quando la realtà emette il verdetto (verdict-uplift-plan §3.1)
sender_label: Option<String>, // Decorativo; non autenticato in MVP
reply_to: Option<[u8; 32]>,// qub_id padre per catene di risposte; non nel preimage di qub_id; non firmato (vedi §9.3)
body: Vec<u8>, // Payload del contenuto (UTF-8 per testo, CBOR per patto)
body_hash: [u8; 32], // SHA3-256(body) (vedi §4.2)
sig_alg: u8, // Algoritmo di firma (vedi §9.2)
author_signature: Option<Vec<u8>>, // Impostato quando sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Impostato quando sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Impostato per accordi bilaterali di patto cofirmati
cosigner_signature: Option<Vec<u8>>, // Impostato per accordi bilaterali di patto cofirmati
}
Baseline (qub testuale non firmato): version = 0x01, content_type = 0x01, sig_alg = 0x00, tutti i campi Option assenti.
Altre configurazioni v1: content_type = 0x03 (corpo di patto, vedi §6.1); sig_alg = 0x01 (ML-DSA-65) con author_signature e author_pubkey presenti (vedi §9.3); cosigner_pubkey e cosigner_signature presenti insieme per patti cofirmati (vedi §9.7); reply_to impostato sul qub_id del qub padre per qub di catena di risposte (vedi §9.3 per le implicazioni sull'ambito della firma).
2.3 SealedQub (formato wire canonico)
Serializzato utilizzando CBOR canonico (§3). Scritto nell'archivio permanente. Questo è l'artefatto on-chain.
SealedQub {
version: u8, // Versione major del protocollo (0x01 per v1)
qub_id: [u8; 32], // Uguale a QubEnvelope.qub_id
visibility: u8, // 0x01 = pubblico; i visualizzatori v1 rifiutano altri valori
unlock_at: i64, // Secondi Unix UTC
outcome_at: Option<i64>, // V1.1 — esposto sulla CTA di verdict-watch
// prima della rivelazione; rispecchia QubEnvelope.outcome_at;
// legato a qub_id tramite il preimage §4.1.
drand_chain_id: String, // hash della catena drand (stringa esadecimale)
drand_round: u64, // Numero del drand round target
tlock_ciphertext: Vec<u8>, // Byte CBOR del QubEnvelope cifrati tlock
recipient_pubkey: Option<[u8; 32]>,// Campo riservato; accettato dal CBOR canonico
// ma non interpretato dal visualizzatore di riferimento v1
title: Option<String>, // Titolo in chiaro mostrato sul conto alla rovescia
// del visualizzatore prima della rivelazione. Legato a qub_id
// tramite title_hash (§4.1). 1..=100 code point
// NFC, nessun carattere di controllo.
}
2.4 RevealedQub (stato dell'applicazione visualizzatore)
Non serializzato in CBOR. Locale all'app del visualizzatore. Costruito dopo decifratura e verifica riuscite.
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 — trasportato da QubEnvelope.outcome_at / SealedQub.outcome_at; alimenta il blocco di attesa del verdetto nella pagina di rivelazione (verdict-uplift-plan §5.1)
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Trasportato da 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. Profilo CBOR canonico
Tutta la serializzazione di SealedQub e QubEnvelope DEVE essere conforme a questo profilo. Due implementazioni dotate della stessa struttura logica DEVONO produrre byte identici.
3.1 Regole di codifica
| Regola | Specifica |
|---|---|
| Standard | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Ordinamento delle chiavi di mappa | Ordinato prima per lunghezza in byte codificati (più corti prima dei più lunghi), poi lessicograficamente (byte per byte per codifiche della stessa lunghezza) |
| Codifica degli interi | Forma più breve: 0–23 nel byte iniziale; 24–255 in 2 byte; 256–65535 in 3 byte; ecc. |
| Codifica della lunghezza | Solo lunghezze definite. Nessun array, mappa, byte string o stringa di testo a lunghezza indefinita (additional info = 31 è vietato). |
| Tag | Nessun tag CBOR (major type 6 è vietato). |
| Virgola mobile | Nessun float (i valori 0xF9–0xFB del major type 7 sono vietati). |
| Stringhe di testo | Codificate UTF-8, normalizzate NFC (Unicode Normalization Form C). |
| Byte string | Byte grezzi. Nessuna codifica base64 a livello CBOR. |
| Chiavi duplicate | Rifiuto con errore. I parser NON DEVONO accettare silenziosamente chiavi di mappa duplicate. |
| Chiavi sconosciute | Rifiuto con errore. I parser NON DEVONO tollerare chiavi di mappa al di fuori dell'insieme di chiavi canonico del tipo — due stringhe di byte canoniche distinte non devono mai essere decodificate nello stesso valore (encode(decode(x)) == x), e per i payload firmati una chiave extra sarebbe contenuto nascosto su cui entrambe le firme si impegnano. L'evoluzione dello schema passa attraverso version, mai attraverso chiavi extra. |
| Valori semplici | Solo true (0xF5), false (0xF4) e null (0xF6) sono ammessi. |
| Campi opzionali | I campi opzionali assenti sono omessi completamente dalla mappa CBOR (non codificati come null). I campi opzionali presenti sono inclusi nell'ordine di chiavi ordinato. |
3.2 Ordini di chiavi canonici verificati
Questi ordini di chiavi sono normativi. Le implementazioni DEVONO emettere le chiavi esattamente in quest'ordine. Le asserzioni di debug DOVREBBERO verificare l'ordinamento nelle build non-release.
QubEnvelope (version 0x01, non firmato, tutti i campi opzionali assenti):
"body" (5 byte codificati)
"qub_id" (7 byte codificati)
"sig_alg" (8 byte codificati)
"version" (8 byte codificati)
"reply_to" (9 byte codificati) ← solo se presente (catene di risposta)
"body_hash" (10 byte codificati)
"unlock_at" (10 byte codificati)
"created_at" (11 byte codificati)
"outcome_at" (11 byte codificati) ← solo se presente (meccanica del verdetto V1.1)
"content_type" (13 byte codificati)
"sender_label" (13 byte codificati) ← solo se presente
"author_pubkey" (14 byte codificati) ← solo se presente
"cosigner_pubkey" (16 byte codificati) ← solo se presente (cofirma di patto)
"author_signature" (17 byte codificati) ← solo se presente
"cosigner_signature" (19 byte codificati) ← solo se presente (cofirma di patto)
Derivazione dell'ordine delle chiavi di QubEnvelope: ogni chiave è una stringa di testo CBOR. Lunghezza codificata = 1 byte di header + lunghezza della stringa (per stringhe sotto i 24 byte). Ordinare prima per lunghezza codificata totale, poi lessicograficamente per chiavi della stessa lunghezza.
SealedQub (version 0x01, pubblico, nessun destinatario):
"title" (6 byte codificati) ← solo se presente
"qub_id" (7 byte codificati)
"version" (8 byte codificati)
"unlock_at" (10 byte codificati)
"outcome_at" (11 byte codificati) ← solo se presente (meccanica del verdetto V1.1)
"visibility" (11 byte codificati)
"drand_round" (12 byte codificati)
"drand_chain_id" (15 byte codificati)
"recipient_pubkey" (17 byte codificati) ← solo se presente
"tlock_ciphertext" (17 byte codificati)
PactTerms (corpo del patto, content_type 0x03):
"notes" (6 byte codificati) ← solo se presente
"terms" (6 byte codificati)
"title" (6 byte codificati)
"party_a" (8 byte codificati)
"party_b" (8 byte codificati)
"pact_version" (13 byte codificati)
PactTerm (riga dell'array terms):
"key" (4 byte codificati)
"value" (6 byte codificati)
PartyIdentifier (mappa party_a / party_b):
"label" (6 byte codificati)
"contact" (8 byte codificati) ← solo se presente
3.3 Riferimento di codifica dei byte
| Tipo | Codifica CBOR | Esempio |
|---|---|---|
| Hash SHA3-256 (32 byte) | 0x58 0x20 + 32 byte |
body_hash, qub_id |
| Timestamp (i64) | Major type 0 (positivo) o 1 (negativo), codifica più breve | Secondi Unix |
| Versione (u8, valore 1) | 0x01 (singolo byte) |
|
| Tipo di contenuto (u8, valore 1) | 0x01 (singolo byte) |
|
| sig_alg (u8, valore 0) | 0x00 (singolo byte) |
|
| Firma ML-DSA-65 (3.309 byte) | 0x59 0x0C 0xED + 3.309 byte |
author_signature, cosigner_signature |
| Chiave pubblica ML-DSA-65 (1.952 byte) | 0x59 0x07 0xA0 + 1.952 byte |
author_pubkey, cosigner_pubkey |
4. Derivazioni normative
4.1 qub_id
Il qub_id identifica in modo univoco un qub e lega il QubEnvelope al SealedQub. È derivato deterministicamente dal contenuto dell'envelope.
qub_id = SHA3-256(
"QUB_ID_V2" || // separatore di dominio: byte ASCII [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 byte) + padding 0x00 (1 byte) = 10 byte
version || // u8 (1 byte)
content_type || // u8 (1 byte)
created_at || // i64 big-endian (8 byte)
unlock_at || // i64 big-endian (8 byte)
outcome_at_or_zero || // i64 big-endian (8 byte; 0 quando outcome_at è assente)
drand_round || // u64 big-endian (8 byte)
body_hash || // [u8; 32] (32 byte)
title_hash // [u8; 32] (32 byte; sentinella di assenza = [0u8; 32])
)
// Preimage totale: 108 byte → output di 32 byte
Codifica del separatore di dominio: La stringa "QUB_ID_V2" è 9 byte ASCII. Un singolo byte di padding 0x00 viene aggiunto per raggiungere 10 byte di allineamento. Le implementazioni DEVONO utilizzare esattamente questi 10 byte: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
Codifica di outcome_at: la versione V1.1 ha esteso il preimage da 92 a 100 byte per integrare il campo opzionale outcome_at nel binding. L'assenza di outcome_at è codificata come 8 byte zero; i validatori del protocollo rifiutano ovunque outcome_at <= 0, quindi questa sentinella non può collidere con un valore legittimo. Vedi §3.2 (formato wire) e il documento in-tree tasks/verdict-uplift-plan.md per la meccanica del verdetto che motiva questo campo.
Codifica di drand_round: la versione V1.2 ha esteso il preimage da 100 a 108 byte per integrare drand_round (il drand round di destinazione, §4.3) nel binding, e ha portato il separatore di dominio a QUB_ID_V2. Questo lega il round del timelock all'identità del qub: un gateway non può rilegare il ciphertext a un round diverso (per esempio già passato) da quello implicato dall'unlock_at mostrato. La procedura di sblocco (§8) verifica inoltre che il round inserito nella stanza del ciphertext tlock corrisponda a unlock_round(unlock_at), così l'unlock time mostrato è in modo dimostrabile il round che governa la decifratura.
Proprietà:
- Modificare qualsiasi campo nel QubEnvelope (body, timestamp, content type, version) produce un qub_id diverso.
- Il qub_id viene calcolato prima della cifratura. Sia QubEnvelope sia SealedQub portano lo stesso qub_id. Il visualizzatore verifica che corrispondano dopo la decifratura.
- qub_id non dipende da
sender_label,author_signatureoauthor_pubkey. Ciò significa che lo stesso contenuto sigillato nello stesso momento produce lo stesso qub_id indipendentemente da chi lo firma. - Modificare il
titledel SealedQub (con tutto il resto invariato) modificaqub_idtramitetitle_hash. Un gateway non può quindi sostituire il titolo in chiaro mostrato nel conto alla rovescia senza invalidare l'identità del qub. - Modificare l'
outcome_atdel SealedQub (con tutto il resto invariato) modificaqub_idtramite il preimage. Un gateway non può sostituire la data di verdict-on pre-rivelazione mostrata sul conto alla rovescia senza invalidare l'identità del qub. - Modificare
drand_round(con tutto il resto invariato) modificaqub_idtramite il preimage. Un gateway non può rilegare il ciphertext del timelock a un round diverso senza invalidare l'identità del qub; combinato con il controllo del round della stanza allo sblocco §8, l'unlock_atmostrato è il round che governa effettivamente la decifratura.
4.2 body_hash
body_hash = SHA3-256(body)
Dove body è il payload del contenuto Vec<u8> grezzo. Per qub testuali, è il corpo del qub codificato in UTF-8.
4.2.1 title_hash
title_hash = SHA3-256(NFC(title).utf8_bytes) se title è presente
title_hash = [0u8; 32] se title è assente
Dove title è il titolo in chiaro opzionale mostrato nel conto alla rovescia del visualizzatore prima della rivelazione (vedi §3.2). La normalizzazione NFC viene eseguita al momento dell'hash in modo che il digest sia stabile tra sequenze di code point visivamente equivalenti. La sentinella di tutti zeri è riservata per il caso di assenza; una stringa vuota viene rifiutata al confine CBOR canonico come codifica non canonica di "assente" (la codifica canonica omette completamente il campo).
4.3 Mapping del round di sblocco
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Parametro | Fonte | Esempio |
|---|---|---|
unlock_at |
Secondi Unix UTC scelti dall'utente | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
info della catena drand (genesis_time) |
1595431050 |
chain_period_seconds |
info della catena drand (period) |
30 |
L'operazione ceil() seleziona il primo drand round il cui reveal time è ≥ unlock_at. Questo garantisce che il qub non diventi decifrabile prima dell'unlock time scelto.
Caso limite: se (unlock_at - chain_genesis_time) è esattamente divisibile per chain_period_seconds, il risultato è esattamente quel round — il qub si sblocca precisamente al reveal time di quel round.
Validazione: unlock_at DEVE essere nel futuro al momento del sigillo. unlock_at NON DEVE superare i 10 anni da created_at (per limitare il rischio di dipendenza drand a lungo termine; l'UI DOVREBBE avvisare per date di sblocco oltre i 2 anni).
5. Newtype del formato wire
I newtype del formato wire forniscono sicurezza in fase di compilazione contro la confusione tra byte CBOR e JSON, plaintext grezzo o altre codifiche di byte.
| Tipo | Contiene | Prodotto da | Consumato da |
|---|---|---|---|
SealedQubCbor |
CBOR canonico di SealedQub | serialize_sealed_qub() |
Upload nell'archivio permanente, fetch del visualizzatore |
QubEnvelopeCbor |
CBOR canonico di QubEnvelope | serialize_qub_envelope() |
Input cifratura tlock, output decifratura tlock |
5.1 Regole di costruzione
// Codice di produzione — solo tramite serializzatori CBOR:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);
// Non c'è deliberatamente alcuna implementazione From<Vec<u8>>.
// Non si possono accidentalmente avvolgere byte arbitrari in un tipo di formato wire.
// Accesso ai byte grezzi:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();
5.2 Validazione alla costruzione
from_encoded() DOVREBBE validare che l'input inizi con un header valido di mappa CBOR. La validazione strutturale completa avviene al momento del parsing, non al momento della costruzione, per evitare il doppio parsing.
6. Registro dei tipi di contenuto
| Valore | Tipo | Dimensione massima del corpo | Note |
|---|---|---|---|
0x00 |
Riservato (non valido) | — | NON DEVE essere utilizzato |
0x01 |
Testo semplice (UTF-8, Markdown ristretto) | 50 KB a pagamento / 10 KB gratuito | Vedi §10 per le regole di rendering. La separazione gratuito / a pagamento è imposta dal servizio di upload; il limite massimo a livello di protocollo è 50 KB. |
0x02 |
Riservato (futuro) | — | Allocato per un tipo di contenuto futuro; non valido in v1. I visualizzatori DEVONO rifiutare secondo la regola sottostante. |
0x03 |
Patto (accordo bilaterale, corpo CBOR) | 100 KB | Il corpo è un PactTerms CBOR canonico (§6.1). Firma del cofirmatario secondo §9.7. |
0x04 |
Verdetto (autovalutazione del creatore, corpo CBOR) | 8 KB | Il corpo è un VerdictBody CBOR canonico (§6.2). Emesso solo dall'intento verdict lato sistema. Il riferimento al qub genitore è sul tag Arweave Parent-Tx-Id, non nel corpo. Vedi verdict-uplift-plan §3.4. |
I visualizzatori DEVONO rifiutare i tipi di contenuto sconosciuti con un errore chiaramente visibile all'utente. I visualizzatori NON DEVONO tentare di renderizzare tipi sconosciuti come testo.
6.1 Corpo del patto (content_type = 0x03)
Un corpo di patto è la codifica CBOR canonica di un valore PactTerms:
PactTerms {
pact_version: u8, // 0x01 per structured/v1
title: String, // ≤ 200 byte, NFC
terms: Vec<PactTerm>, // ≤ 20 righe
party_a: PartyIdentifier, // iniziatore
party_b: PartyIdentifier, // cofirmatario
notes: Option<String>, // ≤ 5.000 byte, NFC; chiave assente se nessuna
}
PactTerm { key: String (≤ 100), value: String (≤ 2.000) } // NFC su entrambi i lati
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }
Gli ordini di chiavi CBOR canonici per tutte e tre le mappe sono indicati in §3.2. Il CBOR del patto serializzato totale NON DEVE superare i 100 KB (coincide con §6).
Discriminatore di schema. La prima riga in terms per un patto structured/v1 DEVE essere { key: "pact_schema", value: "structured/v1" }. Le righe senza questo marcatore sono patti "personalizzati" e non ricevono validazione strutturata o rendering consapevole dello schema.
Slot di riconoscimento congelati. I patti structured/v1 portano esattamente quattro righe di riconoscimento sotto queste chiavi:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
Il value di ciascuna è una di otto stringhe inglesi congelate scelte dalla coppia (role, kind), dove role ∈ { seller, buyer, provider, client } e kind ∈ { standard, capacity }. Le stringhe stesse sono dati normativi del protocollo — le firme ML-DSA-65 di entrambe le parti si impegnano sui byte esatti tramite body_hash. NON sono localizzate; il corpo firmato è neutrale rispetto alla lingua. Qualsiasi modifica di formulazione richiede una nuova versione dello schema (structured/v2).
Le otto stringhe, la loro lookup (acknowledgement_for(role, kind)) e il razionale di ciascuna sono fissati dall'implementazione di riferimento. Le implementazioni conformi DEVONO emettere valori di riconoscimento byte-identici; i test golden-fixture sul body-hash SHA3-256 che coprono tutte e quattro le combinazioni di ruolo catturano qualsiasi deriva.
Ordine di visualizzazione del visualizzatore. Le stringhe di riconoscimento contengono frasi come "descritto sopra", che presuppongono che le righe di descrizione / ambito siano renderizzate prima dei riconoscimenti. I visualizzatori DEVONO renderizzare l'array terms nell'ordine CBOR; il riordino spezza la semantica del testo.
Contatto della controparte. Quando il contact della Party B è un indirizzo email valido, il servizio di upload del qub invia automaticamente un'email di invito alla revisione / cofirma al momento dello staging e lega l'eventuale cofirma alla verifica dello stesso indirizzo (§9.7). I patti il cui contatto della Party B è assente possono comunque essere cofirmati, ma solo tramite un canale fuori banda — il servizio rifiuta le richieste di cofirma che non possono produrre un marker di verifica email corrispondente di 15 minuti.
6.2 Corpo del verdetto (content_type = 0x04)
Un corpo di verdetto è la codifica CBOR canonica di un valore VerdictBody:
VerdictBody {
verdict_version: u8, // 0x01 per structured/v1
outcome: u8, // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
reflection: Option<String>, // ≤ 2.000 byte NFC; "cosa è cambiato, cosa hai imparato"
evidence_url: Option<String>, // ≤ 2.048 byte; solo HTTPS; chiave assente quando omesso
}
Ordine canonico delle chiavi CBOR:
"outcome" (8 byte codificati)
"reflection" (11 byte codificati) ← solo se presente
"evidence_url" (13 byte codificati) ← solo se presente
"verdict_version" (16 byte codificati)
Il CBOR del verdetto serializzato totale NON DEVE superare gli 8 KB (coincide con la riga del registro qui sopra).
Enum di esito. Il byte sul wire è neutrale rispetto all'intento; le quattro categorie Right / Partial / Wrong / Unfalsifiable coprono lo spazio degli esiti di ogni intento che porta un verdetto. Le etichette specifiche per intento ("L'ho indovinata" / "L'ho mantenuto" / "Consegnato" / "Confermata" per Right, ecc.) sono una preoccupazione di rendering lato visualizzatore risolta rispetto all'intento del qub genitore — il wire resta neutrale rispetto alla lingua e all'intento. I valori al di fuori di 1..=4 DEVONO essere rifiutati in fase di decodifica.
Collegamento al qub genitore. Un qub di verdetto NON porta il riferimento al qub genitore nel proprio corpo. L'identificatore di transazione Arweave del qub genitore è emesso come tag di archiviazione Parent-Tx-Id al momento dell'upload (§7, livello dei tag di archiviazione). Questo mantiene il corpo una dichiarazione di autovalutazione firmata e autocontenuta; la catena di audit ("avere ragione su cosa?") è stabilita tramite la lookup sul tag Arweave.
Sicurezza dell'URL delle prove (normativa). Quando evidence_url è presente, i validatori (lato compose, lato wire, edge del Worker) DEVONO imporre:
- Solo HTTPS. La stringa DEVE iniziare con la sequenza di byte
https://. Qualsiasi altro schema —http,ftp,javascript,data,file, ecc. — è rifiutato. - Limite di lunghezza. ≤ 2.048 byte (limite pratico degli URL del browser).
- Controllo NFC + codepoint ostili. Stessa regola di
titleereflection— i codepoint bidi-override / a larghezza zero / del blocco tag / BOM / C0 / C1 sono rifiutati. La definizione coincide concrate::handle::contains_hostile_text_codepointin Rust e conworkers/api/src/utils/unicode.ts::isHostileCodepointin TS (da tenere allineati). - Nessuno spazio bianco, nessun carattere di controllo ASCII. Spazi bianchi / DEL / byte sotto
0x20ovunque nell'URL sono rifiutati — chiude il vettore di iniezione\n/\tche la regola bidi non copre. - Segmento host non vuoto. Tutto ciò che si trova tra
https://e il primo/,?o#DEVE essere non vuoto.
Nessun fetch lato server. Il Worker NON DEVE fare da proxy, fetch o anteprima dell'URL. Il protocollo memorizza una stringa; il rendering avviene lato visualizzatore con rel="nofollow noopener noreferrer" target="_blank" e un host visibile mostrato accanto al testo del link.
Riflessione. Testo di riflessione facoltativo scritto dal creatore ("cosa è cambiato, cosa hai imparato"). Stessa validazione NFC + codepoint ostili di title. Input vuoto / fatto di soli spazi bianchi si comprime ad assente al momento della costruzione.
Versione di schema. v1 supporta solo verdict_version = 0x01. Le revisioni future di schema incrementano questo byte e arrivano insieme a una nuova versione del protocollo per §12.
7. Protocollo di sigillo
La sequenza completa di sigillo. Ogni passo è normativo.
1. L'utente compone plaintext e metadati in ComposeQub.
2. Validare:
a. body è non vuoto.
b. body size ≤ max per content_type e tier utente (vedi §6).
c. unlock_at è nel futuro.
d. unlock_at ≤ created_at + 10 anni.
e. content_type è un valore noto e supportato.
3. Calcolare body_hash = SHA3-256(body).
4. Impostare created_at = secondi Unix UTC correnti.
5. Selezionare la catena drand. Caricare chain_genesis_time e chain_period_seconds, e
calcolare drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
(Calcolato qui, prima di qub_id, perché drand_round è legato nel preimage
di qub_id — §4.1, V1.2.)
6. Calcolare qub_id (vedi §4.1), integrando drand_round dal passo 5.
7. Costruire QubEnvelope con tutti i campi.
8. Serializzare QubEnvelope utilizzando CBOR canonico → byte B.
Asserire: l'output serializzato corrisponde al profilo canonico (§3).
9. Calcolare C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Costruire SealedQub con tlock_ciphertext = C, e corrispondenti qub_id, version,
unlock_at, drand_chain_id, drand_round.
12. Serializzare SealedQub utilizzando CBOR canonico → SealedQubCbor.
12a. Generare K = 32 byte casuali (CSPRNG) e N = 12 byte casuali (CSPRNG).
Calcolare W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
secondo §13. I byte caricati su archivio permanente sono il CBOR OuterWrapper W,
mai il SealedQubCbor nudo. K lascia il dispositivo solo come frammento URL
nel passo 16.
13. Mostrare la disclosure al momento del sigillo. L'utente conferma.
14. Validare l'idoneità per il caricamento tramite il servizio di upload del qub (bot-detection, entitlement, rate limit).
15. Inviare W (i byte OuterWrapper) al servizio di upload del qub; il servizio
firma e carica su archivio permanente. Il servizio è byte-blind rispetto al SealedQubCbor
interno e non riceve mai K.
16. Ricevere arweave_tx_id dal servizio. Costruire il link di consegna come
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (oppure `<origin>/s/<short_code>#<base64url(K)>`
quando viene assegnato un codice corto). I browser non trasmettono i frammenti URL
ai server, quindi K non viene mai osservato da qub.social o da qualsiasi
gateway di archiviazione.
Livello dei tag dell'archivio (fuori banda). Il servizio di upload del qub allega un insieme deliberatamente piccolo di tag della transazione dell'archivio insieme al payload avvolto. Content-Type=application/octet-stream è normativamente richiesto. Il servizio di riferimento allega inoltre tre tag opzionali quando il creatore sceglie di esporli: Intent (intento di composizione validato da allowlist — ad es. quote, reply, commitment), Author (fingerprint della pubkey §9.3 del creatore come hex minuscolo a 64 caratteri) e Parent-Tx-Id (ID della transazione dell'archivio del qub padre per catene di risposta, base64url a 43 caratteri).
Il tag Author è opt-in per qub: l'app creatore di riferimento lo allega solo quando l'utente abilita esplicitamente l'attribuzione pubblica al momento del sigillo. Quando il toggle è disattivato — il default — non viene scritto alcun tag Author e il qub è non attribuito sulla catena: nulla nell'archivio permanente collega l'upload all'handle del creatore, all'email o ad altri qub. Quando il toggle è attivato, il fingerprint Author risolve all'@handle scelto dal creatore tramite la catena di attestazione §9.5. Le relazioni di catena di risposta e Intent non sono identificative. Il wrapper esterno (§13) protegge il corpo interno dalla correlazione del ciphertext — impedendo a un harvester di riconoscere e decifrare massivamente upload a forma di qub dopo che il loro drand round è pubblicato.
Il servizio di riferimento intenzionalmente NON allega i tag App-Name, App-Version o Type: qualsiasi filtro a valore singolo del genere restituirebbe l'intero corpus di qub a una query GraphQL, il che è incompatibile con l'ambito di riservatezza solo-corpo del wrapper.
Un verificatore conforme NON DEVE dipendere da alcun tag dell'archivio per la verifica di terze parti §11; il body hash / qub_id / firma si impegnano solo sul CBOR interno, mai sull'insieme dei tag.
8. Protocollo di sblocco
La sequenza completa di sblocco. Ogni passo è normativo.
1. Il visualizzatore apre il link di consegna. Estrarre arweave_tx_id dal path E
K = base64url_decode(fragment) dal frammento dell'URL. Se il frammento
è assente o malformato → mostrare "questo URL manca della sua chiave
di decifratura" e fermarsi; il visualizzatore NON DEVE contattare il gateway
di archiviazione senza K, poiché recuperare byte avvolti che il visualizzatore non può
decifrare non serve a nulla e fa solo trapelare il tentativo di accesso.
2. Controllare la denylist. Se tx_id è in denylist → mostrare il messaggio di blocco. Fermarsi.
3. Recuperare i byte OuterWrapper da archivio permanente (con fallback multi-gateway).
3a. Sciogliere il wrapper: parsare i byte come OuterWrapper (§13), verificare che
il byte `version` del wrapper sia `0x01` e calcolare SealedQubCbor =
unwrap_sealed_qub(OuterWrapper, key=K). Qualsiasi fallimento di autenticazione
AEAD (K errata, ciphertext manomesso, qub_id-come-AAD scambiato,
nonce scambiato) → mostrare "la chiave di decifratura di questo URL non corrisponde
al qub memorizzato" e fermarsi. I fallimenti di autenticazione sono
indistinguibili per il visualizzatore secondo §13.5.
4. Parsare SealedQubCbor → SealedQub.
5. Validare: SealedQub.version è nota (0x01). Rifiutare versioni sconosciute.
6. Se tempo corrente < SealedQub.unlock_at → mostrare conto alla rovescia. Fare polling o attendere.
6a. Controllo del binding del round (V1.2). Ricalcolare expected_round =
ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
Rifiutare a meno che SealedQub.drand_round == expected_round E il round inserito
nella stanza del ciphertext tlock (letto tramite l'header age/tlock, senza firma
richiesta) == expected_round. Il round della stanza è quello che governa
effettivamente la decifratura; senza questo controllo un creatore malevolo
potrebbe legare il ciphertext a un round già passato mostrando un conto alla
rovescia futuro, così chiunque legga i byte memorizzati potrebbe decifrare prima
di unlock_at. Le implementazioni senza identità di catena (mock di test) saltano
questo controllo.
7. Una volta che tempo corrente ≥ SealedQub.unlock_at:
a. Recuperare la firma del drand round per SealedQub.drand_round dalla rete drand.
b. Calcolare B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
8. Parsare B → QubEnvelope.
9. Validare che QubEnvelope.version sia nota.
10. Verificare: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
Fallimento → errore di integrità.
11. Verificare: QubEnvelope.qub_id == SealedQub.qub_id.
Fallimento → errore di integrità.
12. Verificare: QubEnvelope.unlock_at == SealedQub.unlock_at.
Fallimento → errore di integrità.
13. Verificare: QubEnvelope.content_type è noto e renderizzabile.
Valori noti: 0x01 (testo), 0x03 (patto). Sconosciuto → mostrare errore.
14. Se QubEnvelope.sig_alg != 0x00 → verificare la firma dell'autore (vedi §9.4).
15. Se cosigner_pubkey o cosigner_signature presenti → verificare il cofirmatario (vedi §9.7).
16. Renderizzare il contenuto utilizzando il renderer appropriato (vedi §10 per testo, §6 per patto).
17. Costruire RevealedQub per la visualizzazione.
9. Firma di autorialità
9.1 Razionale
I qub sono memorizzati nell'archivio permanente. Le firme di autorialità devono rimanere non falsificabili a tempo indeterminato, motivo per cui v1.0 utilizza lo schema post-quantistico ML-DSA-65 (FIPS 204) anziché uno schema classico la cui sicurezza potrebbe degradarsi entro la vita permanente del qub.
9.2 Registro degli algoritmi
sig_alg |
Schema | Dimensione chiave | Dimensione firma |
|---|---|---|---|
0x00 |
Nessuna firma (non firmato) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1.952 byte | 3.309 byte |
I visualizzatori DEVONO rifiutare valori di sig_alg sconosciuti.
9.3 Costruzione del preimage firmato
Sono esistite due versioni del preimage. Tutte le firme DEVONO usare V2, e i verificatori DEVONO accettare solo V2. Il preimage V1 legacy (documentato di seguito come riferimento storico) era accettato come fallback solo in verifica durante la migrazione a V2; tale fallback è stato ritirato e una firma solo V1 ora viene rifiutata.
V2 (corrente — prodotto da ogni nuova firma dell'autore e da entrambe le firme del flusso di staging / cofirma del patto):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V2" || // separatore di dominio (17 byte)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 byte)
body_hash || // [u8; 32] (32 byte)
unlock_at || // i64 big-endian (8 byte)
0x00 || // u8 (1 byte): DEVE essere 0x00 in v1.x
sender_label_hash || // [u8; 32]: SHA3-256(NFC(sender_label)),
// o 32 byte a zero se assente
reply_to_or_zero // [u8; 32]: qub_id padre, o 32 byte a
// zero se assente
)
// Preimage totale: 155 byte → hash di 32 byte
signature = Sign(author_secret_key, sig_input)
sender_label_hash segue la stessa convenzione della sentinella di assenza di title_hash (§4.2.1): 32 byte a zero non sono un output SHA3-256 valido, quindi "assente" non può mai collidere con un'etichetta presente. Tutti i campi sono a larghezza fissa, quindi il preimage è non ambiguo senza prefissi di lunghezza.
V1 (legacy — RITIRATO; non più prodotto e non più accettato in verifica):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V1" || // separatore di dominio (17 byte)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 byte)
body_hash || // [u8; 32] (32 byte)
unlock_at || // i64 big-endian (8 byte)
0x00 // u8 (1 byte): DEVE essere 0x00 in v1.0
)
// Preimage totale: 91 byte → hash di 32 byte
Il preimage V1 ometteva sender_label e reply_to. Era accettato come fallback
solo in verifica durante la migrazione a V2; tale fallback è stato da allora
ritirato — i verificatori DEVONO accettare solo il preimage V2. La definizione
è mantenuta qui come riferimento storico e per spiegare il separatore di dominio
più sotto. Una firma che si verifica solo contro V1 DEVE essere trattata come una
verifica fallita.
Separatori di dominio: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" sono 17 byte ASCII ciascuno ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Nessun padding. Il separatore differente separa a livello di dominio le due costruzioni, quindi una firma su un preimage non può mai verificarsi come l'altra.
Byte org_id_present: il byte che segue unlock_at DEVE essere 0x00. L'implementazione di riferimento espone questo come la costante ORG_ID_PRESENT_INDIVIDUAL = 0x00 in crates/qub-core/src/signing.rs; i visualizzatori che ricostruiscono sig_input per la verifica DEVONO emettere lo stesso byte.
Ambito della firma — cosa è e cosa non è coperto. Il sig_input V2 si impegna direttamente su version, qub_id, body_hash, unlock_at, sender_label e reply_to (più il separatore di dominio fisso e il byte org_id_present). qub_id è esso stesso derivato da version, content_type, created_at, unlock_at, outcome_at, drand_round e body_hash tramite il preimage §4.1, quindi qualsiasi modifica a tali campi produce un qub_id diverso e invalida la firma transitivamente. La superficie autenticata è quindi:
| Campo | Autenticato dalla firma | Come |
|---|---|---|
version |
✓ | Input diretto a sig_input |
qub_id |
✓ | Input diretto |
body_hash |
✓ | Input diretto |
unlock_at |
✓ | Input diretto |
sender_label |
✓ | Input diretto tramite sender_label_hash (preimage V2 — l'unica forma accettata) |
reply_to |
✓ | Input diretto tramite reply_to_or_zero (preimage V2 — l'unica forma accettata) |
content_type |
✓ | Transitivamente, tramite il preimage di qub_id |
created_at |
✓ | Transitivamente, tramite il preimage di qub_id |
outcome_at |
✓ | Transitivamente, tramite il preimage di qub_id |
drand_round |
✓ | Transitivamente, tramite il preimage di qub_id (V1.2) |
body |
✓ | Transitivamente, tramite body_hash = SHA3-256(body) |
author_pubkey |
— (implicito) | La chiave che ha verificato la firma è l'autore, per definizione |
cosigner_pubkey / cosigner_signature |
— | Firmati indipendentemente sullo stesso sig_input (vedi §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Campi SealedQub esterni, non all'interno dell'envelope — coperti dalle proprie invarianti strutturali (coerenza round / chain) ma non dalla firma dell'autore. (drand_round ora è legato in modo transitivo tramite il preimage di qub_id — vedi sopra.) |
Perché V2 è l'unico preimage accettato.
- Con il preimage V1 ritirato, una parte con accesso in scrittura ai byte memorizzati poteva sostituire
sender_label("Alice" → "Mallory") o ri-orientarereply_to— e ricifrare dopo il round — senza invalidare la firma dell'autore, perché nessuno dei due campi era nel preimage firmato. V2 copre entrambi, quindi qualsiasi modifica a uno dei due campi porta la verifica a "fallita". Poiché i verificatori ora accettano solo V2, questa sostituzione è chiusa per ogni firma: una firma che non lega nessuno dei due campi (cioè si verifica solo contro V1) viene rifiutata categoricamente anziché essere accettata tramite un downgrade a V1. - L'
author_pubkeyall'interno dell'envelope rimane il vero ancoraggio dell'identità — i visualizzatori DEVONO derivare l'identità di visualizzazione daauthor_pubkey(tramite il livello di attestazione §9.5) piuttosto che fidarsi disender_label.
Le implementazioni che mostrano sender_label o reply_to agli utenti finali DEVONO esporre l'identità autenticata (fingerprint della pubkey, attestazione) come segnale di identità primario, non l'etichetta.
9.4 Procedura di verifica
1. Leggere sig_alg dal QubEnvelope.
2. Se sig_alg == 0x00 → non firmato. Nessuna verifica. Mostrare "qub non firmato."
3. Se sig_alg è sconosciuto → rifiutare. Mostrare "schema di firma non riconosciuto."
4. Estrarre author_signature e author_pubkey. Se uno dei due è assente → errore di integrità.
5. Ricostruire sig_input utilizzando i campi del QubEnvelope (formula V2, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). Il preimage V2 è l'unica
forma accettata — il fallback V1 legacy è ritirato (§9.3), quindi una firma
che non si verifica contro V2 fallisce, punto.
7. Se la verifica ha successo → mostrare "firmato da [fingerprint chiave]."
8. Se la verifica fallisce → mostrare "verifica della firma fallita."
La verifica della firma è l'operazione più costosa (in particolare ML-DSA-65). DOVREBBE essere eseguita dopo che tutti i controlli più economici (hash, qub_id, unlock_at) sono stati superati.
9.5 Attestazioni di identità
Le attestazioni di identità — la mappatura di author_pubkey a rivendicazioni di identità riconoscibili dall'uomo come un handle qub, un indirizzo email, un handle sociale o una credenziale passkey — sono un miglioramento progressivo lato visualizzatore e non sono richieste per la verifica della firma. I visualizzatori che risolvono attestazioni a un'identità di visualizzazione DEVONO applicare la precedenza:
handle > email > social > fingerprint
Il fallback al fingerprint è l'hex minuscolo di SHA3-256(author_pubkey); è sempre disponibile per qualsiasi qub firmato. I visualizzatori POSSONO abbreviarlo per la visualizzazione — il visualizzatore di riferimento rende qub: seguito dai primi e ultimi quattro byte (qub:<8 hex>…<8 hex>).
Un verificatore conforme può completare ogni controllo in §9.4 senza contattare l'API qub, senza alcuna rete oltre l'archivio permanente e drand, e senza alcuna lookup lato server. La risoluzione dell'attestazione è un passaggio separato best-effort eseguito solo dopo che la verifica della firma è riuscita.
9.6 Impatto sulla dimensione
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Firma | 64 byte | 3.309 byte |
| Chiave pubblica | 32 byte | 1.952 byte |
| Totale per qub | 96 byte | 5.261 byte |
| Delta costo di archiviazione (a ~$5/MB) | ~$0,0005 | ~$0,026 |
Per un qub testuale di 500–2.000 byte, ML-DSA-65 triplica all'incirca la dimensione memorizzata. Il costo assoluto è trascurabile.
9.7 Verifica del cofirmatario (accordi bilaterali di patto)
Per accordi bilaterali (content_type = 0x03), un secondo livello di firma dimostra che entrambe le parti hanno acconsentito agli stessi termini.
Campi dell'envelope:
cosigner_pubkey: chiave pubblica ML-DSA-65 del cofirmatario (Party B).cosigner_signature: firma sullo stessosig_inputdell'autore (§9.3).
Entrambi i campi DEVONO essere presenti insieme o entrambi assenti. Se è presente esattamente uno, i visualizzatori DEVONO segnalare un errore di integrità.
Procedura di verifica:
1. Se cosigner_pubkey assente e cosigner_signature assente → nessun cofirmatario. Fatto.
2. Se è presente esattamente uno → errore di integrità.
3. Verificare cosigner_pubkey != author_pubkey (impedire l'auto-cofirma).
Fallimento → mostrare "la pubkey del cofirmatario deve differire dall'autore."
4. Ricostruire sig_input utilizzando la stessa formula di §9.3 (solo V2 — il
fallback V1 legacy è ritirato; tutti i client di patto producono firme V2).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Successo → mostrare "cofirmato da [fingerprint cofirmatario]."
7. Fallimento → mostrare "verifica della cofirma fallita."
Proprietà:
- Il cofirmatario firma lo stesso identico
sig_inputdell'autore — entrambe le parti si impegnano sullo stessoqub_id,body_hasheunlock_at(e, sotto V2, lo stessosender_label_hashereply_to_or_zero). - Per permettere al cofirmatario di ricostruire il preimage V2 senza accesso ai byte grezzi dell'envelope, il servizio di staging impone al momento dello staging che il
sender_labeldi un envelope di patto sia uguale apact_terms.party_a.labele chereply_tosia assente. Entrambe le condizioni valgono per ogni patto del client di riferimento; gli envelope che le violano vengono rifiutati in staging. - La derivazione di
qub_id(§4.1) NON include i campi del cofirmatario. Aggiungere un cofirmatario a un envelope esistente non cambia ilqub_id. - Un patto può essere firmato solo dall'autore (impegno unilaterale), solo dal cofirmatario (insolito) o entrambi (piena prova bilaterale).
Gate di binding email (operativo). Quando un patto in staging porta un contatto email della Party B (§6.1), il servizio di upload del qub DEVE rifiutare la richiesta di cofirma a meno che non esista un marker di verifica email di breve durata corrispondente sia all'id di staging sia all'hash dell'email normalizzata di quel contatto. Il marker viene scritto da /api/v1/auth/verify quando il token magic-link porta uno staging_id e l'indirizzo verificato corrisponde a SHA-256(normalise_email(party_b.contact)) — dove normalise_email(addr) preserva il case della local-part e mette in minuscolo solo la parte di dominio (secondo RFC 5321 §2.3.11), e SHA-256 qui è l'hash NIST FIPS 180-4 (distinto dallo SHA3-256 utilizzato nelle derivazioni §4) — e scade 900 secondi (15 minuti) dopo l'emissione. Questo è un gate operativo anti-impersonificazione, NON parte della prova on-chain del qub — un verificatore di terze parti che replica §11 ha bisogno solo dell'archivio permanente e di drand, senza alcuna lookup lato server. Il marker esiste solo lato server e non fa mai parte del corpo firmato.
Impatto sulla dimensione (autore ML-DSA-65 + cofirmatario):
| Componente | Dimensione |
|---|---|
| Firma dell'autore | 3.309 byte |
| Chiave pubblica dell'autore | 1.952 byte |
| Firma del cofirmatario | 3.309 byte |
| Chiave pubblica del cofirmatario | 1.952 byte |
| Overhead crittografico totale | 10.522 byte |
| Delta costo di archiviazione | ~$0,05 |
10. Rendering e sanitizzazione Markdown
Questa sezione è critica per la sicurezza. Il visualizzatore renderizza i qub testuali (content_type = 0x01) utilizzando un sottoinsieme ristretto di Markdown.
10.1 Elementi consentiti
- Intestazioni: da
#a####(no#####o######) - Enfasi: grassetto (
**), corsivo (*), barrato (~~) - Liste: ordinate (
1.) e non ordinate (-,*) - Citazioni (
>) - Codice: span inline (```) e blocchi delimitati (`````)
- Righe orizzontali (
---) - Interruzioni di riga (due spazi finali o riga vuota)
- Paragrafi
10.2 Elementi vietati
| Elemento | Gestione |
|---|---|
HTML grezzo (<div>, <script>, ecc.) |
Rimosso completamente. Nessun HTML passa. |
Immagini () |
Rimosse. La sintassi di immagine è eliminata dall'output. |
Link ([text](url)) |
URL renderizzato come testo semplice visibile. Non auto-linkato. Non cliccabile senza azione esplicita dell'utente. |
| Schemi URL pericolosi | javascript:, data:, vbscript:, file: — rimossi. |
| Iframe, embed, object | Rimossi. |
| Entità HTML | Decodificate in caratteri visualizzati solo se sicuri. |
10.3 Implementazione
Le implementazioni DEVONO utilizzare un parser con allowlist rigorosa, non una blocklist. L'approccio raccomandato:
- Parsare Markdown utilizzando
pulldown-cmark(o equivalente). - Attraversare l'AST e scartare qualsiasi nodo non presente nell'allowlist (§10.1).
- Per i nodi link: emettere l'URL come testo visibile, non come elemento
<a>cliccabile. - Convertire l'AST filtrato in una rappresentazione intermedia tipizzata (ad es. un enum
MarkdownNodecon solo varianti sicure). L'HTML grezzo è strutturalmente non rappresentabile in questa IR. - Renderizzare dalla IR tipizzata al livello di view target (ad es. componenti di view reattivi, nodi DOM). Nessuna concatenazione di stringhe HTML o
innerHTMLin alcun punto.
Gli approcci a blocklist sono fragili perché nuove estensioni Markdown o stranezze del parser possono introdurre elementi non filtrati. L'approccio AST tipizzato rende XSS strutturalmente impossibile — non c'è alcuna variante che possa trasportare HTML arbitrario.
10.4 Limiti di dimensione e struttura
- Profondità massima delle intestazioni renderizzate:
####(H4).#####e più profondi sono renderizzati come testo in grassetto. - Nessun limite sul numero di paragrafi (i limiti di dimensione del corpo in §6 sono il vincolo).
- Blocchi di codice delimitati: nessun syntax highlighting in MVP. Renderizzati come testo preformattato monospazio.
11. Verifica di terze parti
Qualsiasi terza parte può verificare un qub pubblico senza cooperazione di qub. La procedura di verifica:
1. Ottenere arweave_tx_id (dal link di consegna o per conoscenza diretta).
2. Recuperare SealedQubCbor da qualsiasi gateway di archiviazione.
3. Confermare l'inclusione nel blocco di archiviazione (altezza del blocco, timestamp del blocco).
4. Parsare SealedQubCbor → SealedQub.
5. Recuperare la firma del drand round per SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → byte CBOR del QubEnvelope.
7. Parsare → QubEnvelope.
8. Verificare SHA3-256(body) == body_hash.
9. Verificare QubEnvelope.qub_id == SealedQub.qub_id.
10. Verificare QubEnvelope.unlock_at == SealedQub.unlock_at.
11. Se sig_alg != 0x00: verificare author_signature (vedi §9.4).
12. Tutti i controlli passano → qub è verificato.
Cosa dimostra la verifica:
| Prova | Cosa stabilisce |
|---|---|
| Impegno | Il ciphertext esisteva entro il timestamp del blocco dell'archivio. |
| Integrità | Il corpo in plaintext corrisponde all'hash impegnato e non è stato alterato. |
| Tempistica | Il contenuto era illeggibile fino al drand round, che corrisponde all'unlock time scelto (soggetto alle ipotesi di sicurezza di tlock e drand). |
Cosa la verifica NON dimostra:
| Non-prova | Perché |
|---|---|
| Autorialità | Il sender_label è decorativo. Senza sig_alg ≥ 0x01, chiunque potrebbe aver sigillato questo contenuto. |
| Intento | Il qub dimostra il contenuto e la tempistica, non ciò che il creatore intendeva soggettivamente. |
| Tempistica pre-evento | L'inclusione nel blocco dell'archivio può ritardare l'upload effettivo di minuti. Il timestamp di impegno è il tempo del blocco, non il momento in cui l'utente ha premuto "sigilla". |
12. Versionamento
12.1 Versione del protocollo
Il campo version (u8) sia in SealedQub sia in QubEnvelope identifica la versione major del protocollo.
- I visualizzatori DEVONO rifiutare versioni major sconosciute con un errore chiaro.
- All'interno di una versione major nota, i decoder DEVONO rifiutare le chiavi di mappa sconosciute (§3.1) — l'evoluzione dello schema avviene introducendo una nuova
version, non aggiungendo chiavi che i decoder esistenti salterebbero. (Le revisioni precedenti di questa specifica permettevano di tollerare campi opzionali sconosciuti; quella clausola è stata ritirata — rendevaencode(decode(x))non iniettiva e apriva un vettore di contenuto firmato nascosto sui payload dei patti.) - I tipi di contenuto (
content_type) e gli schemi di firma (sig_alg) sono soggetti a gate di versione: nuovi valori possono essere introdotti solo insieme a una nuova versione del protocollo o un aggiornamento esplicito del registro.
12.2 Cronologia delle versioni
| Versione | Valore | Descrizione |
|---|---|---|
| v1 | 0x01 |
qub testuali pubblici (content_type 0x01), accordi bilaterali di patto (0x03, schema structured/v1, autore + cofirmatario ML-DSA-65), tlock, SHA3-256 |
12.3 Compatibilità in avanti
Un visualizzatore v1 che incontra un QubEnvelope con chiavi di mappa CBOR sconosciute (chiavi non nell'ordine canonico §3.2) DEVE rifiutarlo con un errore di decodifica (§3.1). La compatibilità in avanti poggia sul campo version, non sulla tolleranza delle chiavi: le aggiunte future — anche metadati minori — arrivano con un nuovo valore di version, che un visualizzatore v1 rifiuta con un chiaro errore "protocollo più nuovo" invece di scartare silenziosamente contenuto su cui le firme si impegnano.
Un visualizzatore v1 che incontra sig_alg = 0x01 (ML-DSA-65) ma manca del supporto di verifica ML-DSA-65 DOVREBBE mostrare il contenuto del qub con un avviso "firma presente ma non verificabile", non rifiutare interamente il qub. L'implementazione di riferimento oggi rifiuta ogni valore di sig_alg diverso da 0x00 e 0x01 perché il registro v1 non contiene altro algoritmo valido — il rifiuto rigoroso e il soft-fail sono osservazionalmente identici finché non viene registrato un terzo algoritmo. Il comportamento di soft-fail sopra diventa load-bearing una volta che §9.2 ammette una nuova voce, e il visualizzatore di riferimento sarà aggiornato per fare soft-fail a quel punto.
12.4 Versione del wrapper esterno
L'OuterWrapper descritto in §13 porta il proprio byte version, indipendente da SealedQub.version e QubEnvelope.version. I due spazi di versione evolvono separatamente: una futura sostituzione simmetrica post-quantistica sicura aumenta il byte del wrapper senza toccare la versione del protocollo interno, e una futura aggiunta a livello di protocollo (ad es. un nuovo campo dell'envelope) aumenta la versione interna senza toccare il byte del wrapper.
OUTER_WRAPPER_VERSION_* |
Valore | Algoritmo | Stato |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM con nonce di 12 byte, tag di autenticazione di 16 byte, AAD legato a qub_id |
default v1 |
| — | 0x02–0xFF |
Riservato | Futuro |
I visualizzatori DEVONO rifiutare versioni del wrapper sconosciute con un errore chiaro. Il protocollo mantiene intenzionalmente stretto lo spazio della versione del wrapper finché non appare un driver di migrazione concreto (ad es. linee guida NIST a favore di un AEAD diverso); uno slot 0x02 sarà allocato nella stessa revisione che introduce l'algoritmo.
13. Wrapper di cifratura esterno
13.1 Razionale
I livelli del protocollo (QubEnvelope → tlock → SealedQub) rendono un qub sigillato bloccato nel tempo: il corpo è illeggibile finché unlock_at e la firma del drand round non sono pubblicati. Dopo lo sblocco, tuttavia, la firma del round è pubblica e la forma CBOR canonica di SealedQub è riconoscibile, quindi un harvester che ha indicizzato le transazioni dell'archivio permanente potrebbe decifrare in massa l'intero corpus dei qub.
Il wrapper di cifratura esterno chiude quel canale interponendo un ulteriore livello AEAD simmetrico tra il SealedQubCbor canonico e i byte scritti nell'archivio permanente. La chiave a 256 bit K vive solo nel frammento URL del link di consegna e sui dispositivi degli utenti; i browser non trasmettono i frammenti URL ai server, quindi qub.social, ogni gateway di archiviazione e ogni CDN davanti a uno qualsiasi di essi sono osservazionalmente ciechi a K. Ogni qub nell'archivio permanente è quindi un ciphertext opaco il cui plaintext è irrecuperabile senza l'URL che il creatore ha scelto di condividere.
Effetto netto:
- Immunità all'enumerazione per default. I byte avvolti nell'archivio permanente sono byte-indistinguibili da ciphertext arbitrari. Una strategia di harvester di "fare query GraphQL per upload a forma di qub, decifrare in massa con firme drand pubbliche" non termina con plaintext.
- Postura di privacy crypto-shredding. qub.social letteralmente non può decifrare il proprio corpus. I subpoena raggiungono ciphertext, non plaintext.
- Scala di riservatezza a due livelli. Default = accesso controllato dal link (questa sezione). qub privati cifrati per destinatario (una funzionalità riservata della Fase 2, non ancora specificata) si stratificano sopra come secondo livello.
13.2 Stratificazione
corpo plaintext ← QubEnvelope.body (§2.2)
↓ CBOR canonico (§3)
CBOR dell'envelope
↓ tlock encrypt al drand round (§7 passo 10)
tlock_ciphertext (dentro SealedQub) (§2.3)
↓ CBOR canonico (§3)
byte SealedQubCbor ← artefatto wire interno
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 passo 12a, questa sezione)
byte CBOR OuterWrapper ← caricati su archivio permanente (§7 passo 15)
Il sigillo e lo sblocco a livello di protocollo (§7, §8) sono invariati al di sotto del confine del wrapper; il wrapper si attacca al call site di seal() e si stacca al call site di unlock().
13.3 Struttura dati OuterWrapper
struct OuterWrapper {
version: u8, // 0x01, vedi §12.4
qub_id: [u8; 32], // copiato dal SealedQub interno; AAD AEAD
nonce: [u8; 12], // nonce AEAD a 96 bit
ciphertext: Vec<u8>, // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || tag di 16 byte
}
Invarianti dei campi.
versionDEVE essere uguale a0x01per i byte del wrapper v1.0.qub_idDEVE essere uguale al campoqub_iddel SealedQub recuperato dopo lo unwrapping. Il passo di unwrap non lo impone direttamente (il binding AAD AEAD rende impossibile la manomissione a livello di byte), ma il livello di sblocco controlla la relazione transitivamente: se un creatore avvolge unSealedQubCboril cuiqub_idinterno non corrisponde alqub_iddel wrapper, il passo 11 di §8 fallisce.nonceDEVE essere a 96 bit (12 byte), generato fresco da un CSPRNG per ogni operazione di wrap. Riutilizzare un nonce con la stessa chiave consente attacchi di nonce-reuse AEAD che recuperano il plaintext; i produttori DEVONO trattare le coppie (chiave,nonce) come monouso.ciphertextè l'output AES-256-GCM: byte del ciphertext concatenati con il tag di autenticazione di 16 byte.ciphertext.len() == SealedQubCbor.len() + 16esattamente.
Codifica CBOR. CBOR canonico secondo §3, con la stessa regola di ordinamento delle chiavi (ordinato per lunghezza in byte codificati crescente, poi lessicograficamente). Le quattro chiavi sono:
| Chiave | Byte codificati | Ordine |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
Il primo byte del CBOR OuterWrapper è quindi l'header di mappa a lunghezza definita per una mappa di 4 voci (0xA4).
13.4 Binding AAD a qub_id
Il wrapper lega qub_id come additional authenticated data AEAD. Questa è la difesa strutturale load-bearing contro tre classi di attacco:
| Attacco | Difesa |
|---|---|
Spostare il ciphertext sotto un campo qub_id diverso nel wrapper |
AAD non corrisponde → l'autenticazione AEAD fallisce |
| Mescolare il frammento URL del qub A con i byte dell'archivio del qub B | AAD non corrisponde → l'autenticazione AEAD fallisce |
Manomettere il campo qub_id del wrapper dopo l'upload |
AAD non corrisponde → l'autenticazione AEAD fallisce |
Trasportare qub_id nel plaintext del wrapper non indebolisce significativamente l'immunità all'enumerazione — qub_id è esso stesso un hash SHA3-256 del preimage §4.1 senza preimage recuperabile dal digest, e un enumeratore che ha già raccolto i byte del wrapper non apprende nulla dal qub_id visibile che non potrebbe dedurre dall'esistenza stessa dell'upload.
13.5 Algoritmi di wrap e unwrap
wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
richiedere K.len() == 32 e N.len() == 12 e Q.len() == 32
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C include il tag di autenticazione di 16 byte alla fine
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
richiedere K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
richiedere O.version == 0x01 // §12.4
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// qualsiasi fallimento AEAD → DECRYPT_FAILED, indistinguibile per il chiamante
return P // P è il SealedQubCbor interno
Collasso della modalità di fallimento. K errata, nonce errato, mancata corrispondenza AAD e ciphertext manomesso producono tutti lo stesso errore DECRYPT_FAILED. Questa è una proprietà AEAD deliberata: distinguere la modalità di fallimento creerebbe un canale laterale che un attaccante remoto potrebbe sondare inviando wrapper malformati e cronometrando la risposta. Le implementazioni di riferimento DEVONO collassare tutti i fallimenti AEAD in un'unica forma di errore.
13.6 Materiale chiave e distribuzione
La chiave di wrapping K è un valore casuale uniforme di 256 bit generato per-qub da un CSPRNG. Le implementazioni di riferimento la traggono da:
- Creatore WASM:
getrandom(WebCrypto sotto il backendwasm_js). - Chiamante dell'API di sigillo lato server: il proprio CSPRNG locale; il chiamante fornisce e conserva
Kcomewrapper_key_b64url. Il Worker usaKin memoria per il wrapper ma NON DEVE persisterla. Questo consente a un nuovo tentativo idempotente di recuperare una risposta oscurata usando la capability conservata dal chiamante invece di dipendere da un segreto monouso generato dal server.
Distribuzione: K DEVE essere codificata come base64 URL-safe (RFC 4648 §5, senza padding) e aggiunta al link di consegna come componente di frammento:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
Il frammento non viene mai trasmesso ad alcun server da un browser conforme. I canali di recupero (indice di cronologia lato server, auto-invio email opt-in) che persistono il link di consegna completo — incluso il frammento — oltre il dispositivo dell'utente sono un trade-off esplicito contro la postura di crypto-shredding predefinita e DEVONO essere gestiti su consenso esplicito dell'utente.
Perdita del frammento. Se un utente perde il frammento URL e non ha alcun canale di recupero, il qub è illeggibile. Questo è il trade-off load-bearing del design e DEVE essere divulgato all'utente al momento del sigillo. L'MVP rafforza la disclosure al momento del sigillo con copy esplicito "salva questo URL" e un canale di recupero email verificata per gli utenti che si scelgono di aderire.
13.7 Fuori ambito per questa sezione
- La firma di autorialità (§9) è invariata: le firme sono calcolate all'interno del
QubEnvelopeinterno e sono recuperate dopo unwrap → tlock decrypt → CBOR parse. - I qub privati cifrati per destinatario (una funzionalità riservata della Fase 2, non ancora specificata) si compongono sopra questo wrapper come secondo livello di riservatezza; entrambi i livelli possono essere attivi simultaneamente.
- I patti (§6, content_type
0x03) sono avvolti esattamente come i qub testuali; il wrapper è byte-blind rispetto al tipo di contenuto interno.
13.8 qub pubblici (omissione del wrapper)
Il wrapper esterno è opzionale a livello di consegna. Un creatore può sigillare un qub come pubblico, nel qual caso il SealedQubCbor canonico viene scritto direttamente nell'archivio permanente, senza livello OuterWrapper e senza chiave K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
Un qub pubblico è bloccato nel tempo ma non protetto dal link: resta illeggibile finché il suo drand round non viene pubblicato (il livello tlock è invariato), ma dopo lo sblocco chiunque possieda l'arweave_tx_id può decifrarlo — non è richiesto alcun frammento URL, perché non esiste alcuna K. Questo è il trade-off deliberato per le superfici che il server deve pilotare: le email di notifica di rivelazione, gli embed di terze parti e una SEO post-rivelazione più ricca hanno tutti bisogno di un link che funzioni senza un segreto che il server non detiene mai (§13.6).
Conseguenze di cui un produttore DEVE tenere conto:
- Nessuna immunità all'enumerazione. I qub pubblici rinunciano per costruzione alla proprietà di immunità all'enumerazione di §13.1. Il servizio di upload di riferimento appone su di essi (e solo su di essi) un tag
Visibility: publicnell'archivio permanente, così da renderli intenzionalmente individuabili; i qub privati non portano tale tag e mantengono la loro byte-indistinguibilità. - Titolo in plaintext esposto al momento del sigillo. Il campo
titledi §3.2 è in plaintext all'interno delSealedQubCbor. Sotto il wrapper resta nascosto finché un visualizzatore non fornisceK; senza il wrapper è leggibile da chiunque nell'archivio permanente dal momento dell'upload, prima dello sblocco. Le app conformi lato creatore DEVONO divulgarlo al momento del sigillo. - Il rilevamento è strutturale. Un visualizzatore/embed conforme distingue le due forme tramite il parsing: i byte che si interpretano come
OuterWrapperseguono il percorso di unwrap conK; i byte che si interpretano come unSealedQubCbornudo vengono accettati direttamente. Non è richiesto alcun flag sul wire, equb_idnon lega la visibilità — lo stesso contenuto è byte-identico a livello diSealedQubsia che venga sigillato come pubblico sia come privato.
Privato (avvolto) resta il default; pubblico è una scelta esplicita del creatore per singolo qub.
14. Vettori di test
14.1 Derivazione di 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 = assente
drand_round = 4695445 (= (1736294400 - 1595431050) / 30, parametri drand mainnet §14.2)
body = "Hello, future." (UTF-8, 14 byte)
title = assente
Intermedio:
body_hash = SHA3-256("Hello, future.")
= 76ab8b3f843c6ed4f2d0fd75b9f457b4
ad49dd4450f9c22723ae430e3af3211d
title_hash = [0u8; 32] (title assente — sentinella §4.2.1)
Separatore di dominio (10 byte):
[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]
Preimage (108 byte — V1.2):
domain_separator || // 10 byte
0x01 || // version
0x01 || // content_type
0x0000000067748580 || // created_at come i64 big-endian (1735689600)
0x00000000677DC000 || // unlock_at come i64 big-endian (1736294400)
0x0000000000000000 || // outcome_at_or_zero (outcome_at assente)
0x000000000047A595 || // drand_round come u64 big-endian (4695445)
body_hash || // 32 byte
title_hash // 32 byte (sentinella tutta-zeri; title assente)
Output atteso:
qub_id = SHA3-256(preimage)
= 3a9fcb31b750d985c262fada6d4f777f
d6a28be831d941d85c131f5a4bbaf8a4
Le implementazioni DEVONO produrre valori di body_hash e qub_id identici per questo input. Questo vettore di test DOVREBBE essere il primo unit test scritto. I valori canonici sopra sono stati calcolati dall'implementazione di riferimento e DEVONO corrispondere bit per bit. Layout storici del preimage (pre-lancio — nessun qub attivo dipendeva da questi): il qub_id V1.0 da 92 byte era 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; il qub_id V1.1 da 100 byte (dopo l'integrazione di outcome_at_or_zero) era b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 integra drand_round e porta il separatore di dominio a QUB_ID_V2.
14.2 Mapping del round di sblocco
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calcolo:
(1735689600 - 1595431050) / 30 = 4675285.0
ceil(4675285.0) = 4675285
drand_round = 4675285
14.3 Round-trip CBOR canonico
Le implementazioni DEVONO verificare che serialize(parse(serialize(qub))) == serialize(qub) per tutti gli input validi. Questo è un property test, non un singolo vettore.
14.4 CBOR di PactTerms (content_type 0x03)
Input:
pact_version = 1
title = "Scooter deposit"
terms = [
{ key: "Item", value: "Honda Metropolitan scooter" },
{ key: "Price", value: "$100" },
{ key: "Deposit", value: "$10" }
]
party_a = { label: "Alice" }
party_b = { label: "Bob", contact: "bob@example.com" }
notes = assente
Ordine canonico delle chiavi CBOR (PactTerms):
"notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)
Ordine canonico delle chiavi CBOR (PactTerm):
"key"(4) < "value"(6)
Ordine canonico delle chiavi CBOR (PartyIdentifier):
"label"(6) < "contact"(8)
I byte CBOR canonici e il body_hash SHA3-256 sono calcolati dall'implementazione di riferimento. Le implementazioni DEVONO produrre CBOR byte-identico per questo input.
Le implementazioni DEVONO inoltre verificare che serialize(parse(serialize(pact))) == serialize(pact) per tutti gli input PactTerms validi (property test).
14.5 Vettori cross-linguaggio del wrapper esterno
Il wrapper esterno (§13) ha una fixture canonica separata in crates/qub-core/tests/vectors/wrapper_v1.json. Ogni caso fissa una tupla (key, nonce, qub_id, sealed_cbor) come input hex opachi e asserisce uno specifico output expected_wrapper_hex. Entrambe le implementazioni di riferimento consumano lo stesso file 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).
La fixture attualmente fissa tre casi:
| Caso | Copertura |
|---|---|
basic-text-public |
Forma SealedQub realistica più piccola; nessun campo opzionale. Stabilisce la forma canonica del wrapper per un qub tipico di v1.0. |
with-recipient-pubkey |
SealedQub con recipient_pubkey impostato (path di Phase 2). Diverso insieme di chiavi CBOR interno, diverso qub_id. |
longer-body |
Corpo ~4 KiB — esercita prefissi di lunghezza CBOR multibyte sia all'interno dell'envelope interno sia del ciphertext esterno. |
Le implementazioni DEVONO produrre expected_wrapper_hex byte-identico per gli input registrati. Rigenerare la fixture richiede QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors ed è riservato a modifiche di formato deliberate.
15. Governance del profilo crittografico (futuro)
Questa sezione è informativa per v1 e diventa normativa la prima volta che un secondo algoritmo entra in una delle primitive crittografiche di qub.
15.1 Postura corrente
Il protocollo v1 lega esattamente un algoritmo per primitiva:
- Firma: ML-DSA-65 (
sig_alg = 0x01; chiave pubblica di 1952 byte, firma di 3309 byte) e non firmato (sig_alg = 0x00). Il registro §9.2 non definisce altri valori; un verificatore v1 DEVE rifiutare ognisig_algal di fuori di{0x00, 0x01}. Una futura voce Ed25519 è prevista (§15.3) ma non è allocata in v1. - Timelock: solo drand quicknet — l'hash della catena, la chiave pubblica, il tempo di genesi e il periodo sono parametri di rete fissi portati dal riferimento
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) e daconfig/drand-endpoints.json. - Wrapper esterno: solo AES-256-GCM v1 (§13).
I verificatori attualmente hard-codano lunghezze di chiave e firma per primitiva. Nessuna superficie di agility è esposta dal formato wire.
15.2 Forma prevista
Quando un secondo algoritmo entra nel protocollo, il verificatore sarà configurato per un CryptoProfile nominato (ad es. ExqubV1) che elenca l'insieme esatto di valori permessi per primitiva — sig_alg, catene drand, versioni del wrapper, tipi di contenuto. Il profilo è fissato al momento della verifica, mai negoziato in-band. Qualsiasi valore al di fuori del profilo attivo viene rifiutato.
Ciò garantisce che aggiungere ML-DSA-87 o attivare Ed25519 non possa indebolire retroattivamente le configurazioni esistenti dei verificatori: un verificatore v1 rimane un verificatore v1 anche dopo che un profilo v2 è pubblicato.
15.3 Condizioni di trigger
Promuovere §15 a stato normativo quando è proposto uno dei seguenti:
- Un secondo byte
sig_alg(attivazione di Ed25519, ML-DSA-87 o qualsiasi nuova voce nel registro §9). - Una seconda catena drand in uso di produzione.
- Una seconda versione del wrapper esterno.
Fino ad allora §15 è un segnaposto che fissa la forma della migrazione in modo che future PR atterrino contro un target noto piuttosto che ri-litigare la superficie di negoziazione da zero.
16. Log di trasparenza e livelli di durabilità (Design — revisione completata)
Stato. Questa sezione è una specifica di design. I formati wire, l'hashing e il modello di fiducia descritti di seguito sono normativi per l'implementazione, ma nessun codice del log di trasparenza è ancora stato rilasciato. La revisione esterna W5 è completata: §16.15 registra le decisioni risolte e i vincoli vincolanti di lancio che ne sono emersi. L'implementazione può procedere sotto quei vincoli. §16 resta orientata al futuro nello stesso senso di §15 — fissa il target così che l'implementazione atterri contro un design assestato piuttosto che ri-derivare il modello di fiducia in revisione del codice. È strettamente additiva — ogni qub esistente mantiene la propria transazione Arweave individuale e non c'è alcun cambiamento al formato wire
SealedQub/QubEnvelope.
16.1 Razionale e livelli di durabilità
Oggi la durabilità di un qub e il suo impegno temporale poggiano entrambi su una singola transazione Arweave per-qub (§11). Ciò accoppia la latenza di sigillo alla finalità di Arweave, rende l'upload per-qub un tetto di costo di prodotto (ARWEAVE_DAILY_CEILING) e non offre alcun ordinamento a prova di manomissione tra i qub. Il log di trasparenza aggiunge due livelli sotto e attorno a quell'unico livello:
| Livello | Nome | Garanzia | Quando |
|---|---|---|---|
| T1 | Ack sincrono R2-first | Soglia minima di durabilità — i byte sigillati sono scritti su archiviazione durevole prima che il sigillo ritorni (< 300 ms p95). |
Ogni qub, in modo sincrono (§16.10). |
| T2 | Inclusione batch nel log di trasparenza | Impegno universale append-only, a prova di manomissione + ordinamento totale, ancorato ad Arweave. | Ogni qub, differito + batch (§16.5–16.7). |
| T3 | Permanenza Arweave per-qub | Una transazione Arweave individuale per il qub. | Upsell a pagamento, e fallback in caso di indisponibilità di Arweave (§16.8). |
T2 rende l'Arweave per-qub una scelta (T3) anziché l'unico percorso di durabilità. ARWEAVE_DAILY_CEILING è ritirato come tetto di prodotto e declassato a circuit breaker sul solo wallet di ancoraggio dedicato (§16.7); i sigilli degli utenti non vengono mai rifiutati per averlo superato.
Onestà sulla durabilità (risolto — §16.15 Q6). La durabilità non regredisce: la scrittura T1 su R2 è sincrona e write-once, quindi un qub di fascia gratuita che non ha acquistato T3 è pienamente durevole nell'istante in cui il sigillo ritorna. Ciò che si fa più grossolano è il tempo di impegno con limite superiore dimostrabile: per un qub gratuito diventa il tempo di blocco dell'ancoraggio anziché il tempo di blocco di una transazione per-qub. A basso volume — lo stato realistico di lancio iniziale e fuori picco — la piena cadenza giornaliera è la soglia tipica, non un caso limite raro. La cornice di prodotto è quindi un limite superiore con nessuna latenza numerica impegnata — "sigillato e durevole ora; un timestamp pubblico indipendente è aggiunto al prossimo ancoraggio del log (tipicamente giornaliero)" — e la prova dell'impegno con precisione all'ora è una proprietà T3 a pagamento, divulgata alla superficie di confronto dei livelli e nelle condizioni (§16.11, §16.15 Q6). Qualsiasi limite temporale è solo uno SLO interno, mai uno SLA pubblicizzato.
16.2 Struttura LogLeaf (due forme impegnate)
Una voce del log è un LogLeaf, codificato come CBOR canonico scritto a mano sotto il profilo §3.1 (lunghezza definita, nessun tag, nessun float, interi in forma più corta, testo NFC, campi opzionali omessi quando assenti, chiavi ordinate per lunghezza in byte codificati crescente poi byte per byte). La guardia canonica §3.1 parse → re-encode → compare è applicata sul percorso di codifica prima dell'hashing (non solo in decodifica), così due implementazioni non possono divergere sui byte della foglia attraverso una differenza di larghezza intera o di ordine delle chiavi. Tutti gli interi sono u8 / u64 / i64; tutti i digest sono stringhe di byte di 32 byte (bstr[32]). Un id di transazione Arweave memorizzato è un digest SHA-256 grezzo di 32 byte trasportato come bstr[32], mai una stringa di testo base64url (coerente con §3.3).
La foglia ha due forme selezionate da un byte kind, perché sul percorso di upload predefinito il Worker è cieco ai byte: POST /api/v1/upload riceve solo qub_id e unlock_at come asserzioni del client non fidate — body_hash, drand_round, created_at e drand_chain_version sono tutti sigillati all'interno del wrapper esterno §13, la cui chiave il Worker non possiede mai. Solo il percorso di sigillo lato server (POST /api/v1/seal) deriva body_hash / drand_round dal plaintext. Una singola forma di foglia che trasporti body_hash + drand_round impegnerebbe quindi valori che l'operatore non ha mai verificato per la maggioranza dei qub reali. Lo split mantiene onesto ogni valore impegnato:
| Chiave | Lung. enc. | Tipo | Presenza | Significato |
|---|---|---|---|---|
seq |
4 | u64 |
obbligatorio | Indice globale di foglia a base 0; la posizione a cui si impegna la prova di inclusione. |
kind |
5 | u8 |
obbligatorio | 0x01 attestato (sigillo lato server) o 0x02 asserito (sigillo lato client / upload cieco ai byte). |
ref |
4 | bstr[32] |
obbligatorio | Id di riferimento della foglia. Attestato → qub_id grezzo. Asserito → l'id blinded SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
6 | bstr[32] |
obbligatorio | Indirizzo di contenuto SHA3-256(stored_bytes) — l'unico legame al contenuto che il Worker può sempre calcolare onestamente, su entrambi i percorsi. |
unlock_at |
10 | i64 |
obbligatorio | Copiato (attestato) o asserito (asserito); validato > 0 prima di entrare nella foglia. |
received_at |
12 | i64 |
obbligatorio | Orologio di sistema del Worker all'ack R2. Non probatorio (asserito dall'operatore; §16.6). Presente per auto-descrizione, mai una prova. Validato > 0. |
body_hash |
10 | bstr[32] |
solo kind=0x01 |
Omesso su 0x02 — il Worker non lo possiede sotto §13. |
drand_round |
12 | u64 |
solo kind=0x01 |
Omesso su 0x02. |
Una foglia kind=0x02 deliberatamente non impegna né body_hash né drand_round: attesta l'impegno e l'ordinamento di un ciphertext opaco all'indirizzo di contenuto chash, rivendicando qub_id e unlock_at — non il suo plaintext o round. Le componenti di plaintext/round per un qub asserito provengono dalla verifica esistente del bundle .qub §11, non dal log (§16.11). drand_chain_version non è nella foglia (è dentro il wrapper sul percorso predefinito); la granularità della catena vive sull'ancoraggio (§16.7). Disciplina dell'encoder: rifiutare un ref o chash tutto a zero, e rifiutare unlock_at / received_at non positivi, rispecchiando la guardia sentinella outcome_at > 0 in cbor.rs.
16.2.1 Blinding dei qub privati
Il log non deve diventare l'oracolo di enumerazione che il wrapper esterno §13 esiste per prevenire (§13.1). Per un qub privato (avvolto) la foglia asserted impegna l'identificatore blinded SHA3-256(qub_id ‖ log_blind_secret), dove log_blind_secret è un segreto detenuto dal server, e omette body_hash. Una terza parte non può legare tale foglia a uno specifico qub_id; il detentore del qub, che ha l'URL di consegna e quindi qub_id, può ricalcolare il blind per confermare la propria inclusione. Un qub pubblico (già enumerabile, già portatore del tag Arweave Visibility: public per §13.8) impegna il qub_id grezzo. Questo è l'unico punto in cui la verificabilità autonoma cede deliberatamente a un invariante di privacy load-bearing; il legame autonomo per i qub privati è chash (§16.9).
Custodia di log_blind_secret (risolto — §16.15 Q4). Il blind protegge la non collegabilità delle foglie, non la riservatezza del plaintext (il wrapper §13 garantisce quella indipendentemente). In caso di compromissione di log_blind_secret, per ogni qub_id che l'avversario già detiene o può ricostruire (ogni qub di cui ha il bundle/URL, più qualsiasi qub_id a bassa entropia o pubblico) ricalcola il ref della foglia in un hash e lo collega — questo è collegamento diretto di una popolazione nota, non un attacco a forza bruta su uno spazio sconosciuto. Classificare log_blind_secret come un segreto di grado correlazione/Sybil nello stesso livello di custodia degli altri segreti del server, e ruotare solo in avanti (una rotazione ri-blinda le foglie future; non può retroattivamente scollegare quelle già ancorate).
16.3 Hashing di foglie e nodi
Hashing con separazione di dominio RFC 6962 §2.1 con SHA-256 sostituito da SHA3-256:
leaf_hash = SHA3-256(0x00 || canonical_cbor(LogLeaf))
node_hash(l,r) = SHA3-256(0x01 || l || r)
empty tree = SHA3-256("") // definito ma mai ancorato
I byte di prefisso di dominio 0x02 (catena di voci, §16.4) e 0x03 (hash STH, §16.6) sono riservati e disgiunti da questi. Sono byte singoli e quindi non possono collidere con i separatori di dominio ASCII esistenti di 10 byte (QUB_ID_V2, ecc.). L'albero è l'albero RFC 6962 sbilanciato left-full (ogni split interno alla maggiore potenza di due strettamente inferiore al conteggio delle foglie del sottoalbero), che permette alle prove di inclusione e di consistenza di condividere un unico algoritmo di percorso di audit. La specifica di riferimento porta pseudocodice esplicito di derivazione sinistra/destra e fissa un vettore di test non potenza di due (5 foglie) così che il caso di promozione del bordo destro — che un vettore a 4 foglie nasconde — sia esercitato.
16.4 Concatenamento di hash (interno)
Il LogDO mantiene una catena interna di voci solo per la consistenza in caso di crash. Non è mai pubblicata e mai esposta al verificatore:
entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1] = SHA3-256("QUB_TLOG_GENESIS_V1")
L'autorità append-only pubblicata è la radice Merkle cumulativa + il suo ancoraggio (§16.5–16.6), mai l'ordine grezzo in cui l'operatore capita di servire le foglie: la catena si ricalcola per qualsiasi ordine servito, quindi solo la radice ancorata fissa la posizione canonica.
16.5 Albero Merkle cumulativo e batching
C'è un unico albero RFC 6962 in continua crescita su tutte le foglie in ordine seq — non alberi isolati per-batch. (Una costruzione con catena di foglie-riporto per-batch è stata respinta: non è una vera relazione di prefisso, quindi le sue "prove di consistenza" sono infondate.) L'albero cumulativo dà genuine prove di consistenza RFC 9162 e permette a un singolo ancoraggio recente di dimostrare l'inclusione per qualsiasi qub più vecchio.
Il Durable Object LogDO è il singolo scrittore (blockConcurrencyWhile, rispecchiando QuotaDO / EntitlementDO) — l'aggiunta a un log condiviso è read-modify-write su stato condiviso e quindi DEVE passare per un DO, mai per KV. Mette in cache la frontiera del bordo destro dell'albero (O(log n) hash) così che chiudere un batch è O(batch). Un batch è l'insieme delle foglie ancorate insieme; i suoi trigger sono configurabili, non congelati nel protocollo: un avanzamento di tree_size di almeno LOG_BATCH_MAX_LEAVES (default 4096), o l'età che raggiunge la cadenza di ancoraggio, o un flush forzato quando atterra un sigillo T3 a pagamento. root_i è il Merkle Tree Hash cumulativo sulle foglie 0 .. tree_size_i.
16.6 Signed Tree Head tramite ancoraggio Arweave
La transazione di ancoraggio Arweave è il Signed Tree Head e sostituisce una firma dell'operatore per la testa dell'albero stessa: l'ancoraggio giornaliero non necessita di alcuna chiave qub perché l'owner della tx Arweave è la firma. La tesi del moat regge — il substrato immutabile, non un segreto detenuto da qub, è load-bearing per la radice ancorata.
C'è esattamente una chiave di firma qub calda nel design, ed è pinnata: la chiave di ricevuta per-sigillo (§16.10). La sua chiave pubblica è impegnata in LogProfile (distribuito con il verificatore) e controfirmata da anchor_owner, così un verificatore valida una ricevuta contro la stessa radice pinnata dell'ancoraggio. Questa è la risoluzione di §16.15 Q2 — una chiave di ricevuta non pinnata, ruotabile dall'operatore, sarebbe ripudiabile (l'operatore potrebbe negare che la chiave fosse sua), il che vanificherebbe il valore di responsabilità della ricevuta contro l'avversario a livello di operatore che la ricevuta esiste per scoraggiare. Quindi: qub non detiene alcuna chiave di firma del log non pinnata; la chiave di ricevuta è pinnata e controfirmata da anchor_owner.
Il SignedTreeHead è CBOR canonico (chiavi per lunghezza codificata): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (sth_hash precedente; genesi = 32 byte a zero), log_id:bstr[32], first_seq:u64, anchored_at:i64. Il suo hash è sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).
Radice di fiducia pinnata. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Un verificatore conforme DEVE richiedere anchor_tx.owner == LogProfile.anchor_owner, dove anchor_owner (e la chiave pubblica della chiave di ricevuta) è incorporato in qub_core come LogProfile — accanto alle costanti quicknet già in DrandTimelockProvider::quicknet() — e distribuito con il binario del verificatore. Il verificatore DEVE anche verificare localmente il binding dati della tx Arweave → tx_id anziché fidarsi di una risposta /raw/ di un gateway. Ciò chiude il buco dell'equivocazione del wallet canaglia: "ancorato su Arweave" è privo di significato finché il verificatore non pinna quale wallet.
La rotazione è un'estensione della governance §15, non un riuso (risolto — §16.15 Q3). La superficie di profilo di §15.2 attualmente enumera solo sig_algs / catene drand / versioni del wrapper / tipi di contenuto, e i trigger di §15.3 non ne elencano nessuno — LogProfile / anchor_owner non è ancora nella superficie di §15. La governance della rotazione deve quindi essere costruita: §15.3 è esteso (sotto) per aggiungere il trigger LogProfile, e una rotazione è un bump firmato di LogProfile spedito in un aggiornamento del verificatore. Una rotazione pianificata porta una controfirma uscente → entrante; una rotazione guidata da compromissione non può (la chiave uscente è non fidata/non disponibile proprio in quel momento) e ricade sul bump governato da §15, con il controllo del fork dell'ancoraggio precedente (sotto) che limita il danno nel frattempo.
Finestra di equivocazione (parametro di fiducia di prima classe). Una foglia è resistente all'equivocazione solo una volta che il suo ancoraggio di copertura è confermato su Arweave. La finestra è received_at → conferma dell'ancoraggio (≤ cadenza + finalità di Arweave). Al suo interno le uniche garanzie sono la ricevuta di sigillo pinnata (§16.10) e l'integrità operativa di qub. Tre artefatti di responsabilità rendono questo onesto anziché vagheggiato (il modello di witness è la risoluzione di §16.15 Q2):
- Ricevuta di sigillo firmata e pinnata — l'analogo dell'SCT restituito nella risposta di upload (§16.10), firmato dalla chiave di ricevuta pinnata e controfirmata da
anchor_owner. Una foglia eliminata prima del suo ancoraggio lascia alla vittima una ricevuta non ripudiabile da pubblicare, chiudendo il buco dell'omissione silenziosa. - Metodologia di monitoraggio pubblicata + percorrimento della catena prev — la catena
prevdell'ancoraggio è percorsa testa→genesi; un fork (due ancoraggi a unasizeconrootdiversa, o unprevrotto) è prova pubblicabile di cattiva condotta. Il rilevamento dell'equivocazione è un impegno operativo dichiarato, non un'assunzione silenziosa. - Teste duali auto-pubblicate — ogni nuova testa
{sth_hash, tree_size}è pubblicata su un repository GitHub pubblico, append-only, di proprietà di qub dedicato (la componente load-bearing di auto-pubblicazione a prova di manomissione), con un post sui social come corroborazione best-effort soltanto. Una pubblicazione fallita DEVE generare un alert (non fallire in silenzio).
Limite di onestà (vincolo vincolante). Poiché qub controlla entrambe le superfici di pubblicazione, questo è auto-pubblicato, non testimoniato in modo indipendente. Nessuna superficie di prodotto, marketing o legale può affermare che il log sia "testimoniato in modo indipendente"; l'affermazione consentita è che l'equivocazione è rilevabile e lascia una ricevuta non ripudiabile. Un vero witness terzo indipendente è rinviato a un futuro bump della governance §15.
received_at è asserito dall'operatore e nessuna affermazione può poggiarvi — non è mai esposto come prova o come corroborazione di disputa su alcuna superficie di prodotto / legale / API / rendering di prova. Il tempo di blocco dell'ancoraggio Arweave T è l'unico timestamp privo di fiducia (un limite superiore su "registrato entro"). Qualsiasi controllo di sanità del monitor su received_at DEVE confrontare contro T, non contro il campo STH anchored_at controllato dall'operatore; tale controllo è una guardia contro il bug dell'orologio di un operatore onesto soltanto, non un controllo di responsabilità contro un operatore malevolo (§16.15 Q5).
16.7 Formato e cadenza della transazione di ancoraggio
L'AnchorBundle è il corpo della transazione Arweave in CBOR canonico, scritto tramite il bundler §16.8: ver:u8, sth:bstr (byte SignedTreeHead canonici), prev_anchor:bstr (byte grezzi dell'id della tx di ancoraggio precedente; omesso alla genesi), chain_hash:tstr (la catena drand in vigore — quicknet), e lo stream leaf-CBOR del batch in ordine seq così che l'ancoraggio sia auto-contenuto: un monitor ri-deriva root dal corpo con zero dipendenza da qub. (Se lo stream delle foglie diventa grande ad alto volume, una futura revisione potrebbe impegnare solo un intervallo di foglie per riferimento; annotato, non adottato in v1.)
I tag Arweave sono intenzionalmente enumerabili — il log è pensato per essere trovato, a differenza dei qub privati: App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. I tag sono suggerimenti non fidati; il corpo CBOR è l'unica autorità.
Cadenza: giornaliera per default, rivista con il volume (il trigger di dimensione accorcia automaticamente la cadenza effettiva sotto carico); un sigillo T3 a pagamento forza un ancoraggio così che i clienti paganti non aspettino mai un giorno. Il wallet di ancoraggio è dedicato e a bassa velocità, separato dal wallet di upload — DEVE essere il proprio JWK (una chiave distinta, non un ruolo logico sul wallet di upload) così che una compromissione del wallet di upload non possa falsificare ancoraggi — con un budget rigido di transazioni di ancoraggio per giorno (il declassato ARWEAVE_DAILY_CEILING). La postura di custodia è dichiarata chiaramente: una chiave calda a ambito ristretto con un circuit breaker stretto e basso saldo, non "fredda" — un wallet che firma automaticamente ogni giorno non può essere freddo, e la specifica non finge il contrario.
16.8 Bundler ANS-104
Un encoder DataItem ANS-104 interno e un firmatario deep-hash, all'incirca 300 righe, solo Web Crypto, zero dipendenze npm (entrambi gli SDK Turbo falliscono il gate di supply-chain npm ci --ignore-scripts). Layout dei byte del 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
La firma è il deepHash di Arweave — un digest SHA-384 ricorsivo (requisito wire di Arweave, crypto.subtle.digest("SHA-384")) su ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — poi RSA-PSS sul deep hash con il JWK del wallet tramite crypto.subtle; id = base64url(SHA-256(signature)). Lo SHA-384 qui è messo in quarantena come primitiva esclusivamente wire-Arweave, mai una primitiva di fiducia qub (§15 registra la barriera; l'hashing di fiducia di qub è SHA3-256 dappertutto).
Un unico percorso di codice serve tre consumatori: la permanenza per-qub T3 a pagamento, il fallback in caso di indisponibilità di Arweave (accodare il DataItem, restituire comunque l'ack R2-first — questo chiude l'attuale vicolo cieco 503 ARWEAVE_UNAVAILABLE), e la scrittura dell'AnchorBundle. Schema di firma (risolto — §16.15 Q8): v1 firma con RSA-PSS (signature type 1) riutilizzando il meccanismo JWK del wallet Arweave esistente (zero nuova custodia di chiavi a lunga vita, servendo la tesi "un segreto in meno"); Ed25519 è rinviato al percorso di migrazione PQ §15.
Il deep hash scritto a mano è il codice a più alto rischio e a minor copertura naturale in W5, quindi il suo gating è non negoziabile (§16.15 Q8):
- La fixture cross-linguaggio
tlog_v1.json(Rust + TS, il patternwrapper_v1.json§14.5) copre deep-hash, byte + id del DataItem, hash di foglia, una radice a 5 foglie + percorso di audit, un hash STH, una prova di inclusione e una prova di consistenza — in entrambe le direzioni di firma e verifica (la direzione di verifica conta perché il controllo locale tx → tx_id di §16.6 tira il deep hash in ogni verificatore autonomo, non solo nello scrittore). - Un round-trip di interoperabilità una tantum attraverso un bundler ANS-104 di riferimento, consumato come dati di test statici soltanto — mai una dipendenza runtime npm (la postura solo-Web-Crypto / niente install-script regge).
- Il percorso deep-hash + RSA-PSS deve fare round-trip attraverso le stesse primitive
crypto.subtleusate in produzione, così che l'encoder interno sia byte-compatibile. - Un monitor di accettazione post-bundle continuo conferma che ogni DataItem di ancoraggio / fallback raggiunga effettivamente l'accettazione su Arweave, con un alarm + circuit breaker — perché il deep hash serve anche la coda di fallback in caso di indisponibilità di Arweave, quindi una regressione silenziosa riempirebbe quella coda di elementi rifiutati dalla rete durante l'esatto outage che essa esiste per coprire.
16.9 Prove di inclusione e di consistenza
Entrambe sono RFC 9162, SHA3-256, servite come CBOR canonico.
InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (il CBOR esatto della foglia — il verificatore ricalcola leaf_hash da sé e non si fida mai di un hash fornito), 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. Un'unica lista di chiavi non ambigua, fissata da vettore di test.
Verifica autonoma (nessun server qub, estende §11):
1. Parsare il bundle .qub → SealedQub; ricalcolare qub_id (§4.1).
2. Leggere leaf.kind.
3a. kind=0x01 (attestato):
assert leaf.ref == qub_id
assert leaf.body_hash == SHA3-256(body)
assert leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserito):
assert leaf.ref == SHA3-256(qub_id || blind) // il detentore fornisce blind
OR trattare ref come opaco e legare tramite leaf.chash == SHA3-256(stored_bytes)
4. Ricalcolare leaf_hash = SHA3-256(0x00 || leaf); piegare `audit` per RFC 6962
usando index/size; richiedere derived root == proof.root.
5. Recuperare anchor.txid da qualsiasi gateway; verificare il binding tx data → tx_id
(non fidarsi di una risposta /raw/ di un gateway); RICHIEDERE anchor_tx.owner ==
LogProfile.anchor_owner.
6. Parsare AnchorBundle; richiedere committed root == proof.root e size ==
proof.size; leggere il tempo di blocco Arweave T.
7. Emettere l'affermazione con ambito definito da leaf.kind (§16.11).
L'archiviazione che serve le prove DEVE essere indicizzata per coordinate (risolto — §16.15 Q7, precondizione bloccante). La generazione di prove per foglie fredde è neutra rispetto alla correttezza solo se il materiale di audit su R2 è un archivio persistente di nodi Merkle indicizzato per coordinata assoluta dell'albero (level, index) — non delta di nodi per-batch. Con un archivio indicizzato per coordinate, qualsiasi percorso di audit (leaf i, size N) è un insieme di O(log N) GET R2 diretti con nessun ricalcolo attraverso i confini di batch; con un archivio indicizzato per batch non lo è, ed è questa la lacuna di layout di archiviazione che questa risoluzione chiude. I corpi delle foglie sono parimenti indirizzabili per contenuto tramite seq. Un vettore di test W5 DEVE dimostrare una foglia fredda dell'era genesi contro una radice molto più tarda usando solo R2 + Arweave con l'archiviazione del LogDO cancellata, così che l'affermazione di sicurezza della reclamazione in §16.13 sia supportata anziché asserita. Gli O(log N) GET R2 sequenziali appartengono solo all'endpoint di prova asincrono — mai al percorso caldo di sigillo (§16.10) o a un cron per-tick.
16.10 Ordinamento dell'ack R2-first
La sequenza di POST /api/v1/upload diventa:
- Gate della prima metà (auth, validazione, chiave di shard di idempotenza) — invariati.
- In modo sincrono
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— la soglia minima di durabilità; chiude anche la race di precache di W1 (in precedenza unctx.waitUntildopo il submit Arweave). - In modo sincrono
await LogDO.append(leaf)— un'unica RPC DO in-colo; il singolo scrittore assegnaseq, estende la catena di voci e aggiorna la frontiera. (RMW su stato condiviso → DO, mai KV.) La RPCappendfa solo quello — il lavoro di chiusura batch MerkleO(batch)gira fuori da questa RPC sull'alarm del LogDO, altrimenti il p95 diappendha un picco a ogniLOG_BATCH_MAX_LEAVES-esimo sigillo. - Restituire l'ack ora — con la ricevuta di sigillo (firmata dalla chiave di ricevuta pinnata, §16.6) e
{ tx_id, log_seq, anchor_status: "pending" }. Il fan-out Arweave di più secondi è rimosso dal percorso critico. - Un solo
ctx.waitUntilaccoda il lavoro differito: il submit Arweave per-qub (ora best-effort / a pagamento; in caso di fallimento instrada alla coda di fallback del bundler anziché restituire 503 all'utente) più le scritture di meta provvisorie esistenti. La chiusura del batch e l'ancoraggio girano indipendentemente dall'alarm del LogDO e dal cron di ancoraggio giornaliero. Nessunctx.waitUntildentro un loop; la chiave di shard di idempotenza esistente è preservata.
Budget di latenza (risolto — §16.15 Q7). Il target < 300 ms p95 è un gate di lancio misurato, non un'assunzione. Il percorso critico onesto è la prima metà di letture KV + un PUT R2 + due Durable Object serializzati — il debito di quota di sigillo QuotaDO esistente e il nuovo append su LogDO — quindi il budget deve tenere conto di due round-trip DO in-colo, non uno. Spedire un alarm di latenza per LogDO che rispecchia quello di QuotaDO e trattare una regressione del p95 come un bloccante di rilascio.
16.11 Modello di fiducia — l'affermazione precisa, con ambito definito dal kind della foglia
Per kind=0x01 (attestato): "Questo contenuto — corpo corrispondente a body_hash, identificato da qub_id — è stato impegnato al log append-only di qub alla posizione seq ed esisteva non più tardi del tempo di blocco Arweave T; era crittograficamente illeggibile fino al drand round R = unlock_round(unlock_at)." Questa è la tripletta completa {binding del round tlock + inclusione Merkle + radice ancorata}.
Per kind=0x02 (asserito, il default): "Un ciphertext opaco con indirizzo di contenuto chash, che rivendica qub_id e unlock_at, è stato impegnato al log append-only alla posizione seq ed esisteva non più tardi del tempo di blocco Arweave T." Le componenti di round e corpo sono fornite dalla verifica esistente del bundle .qub §11 (qub_core::unlock), non dal log; ciò che il log aggiunge rispetto a una nuda transazione per-qub è un ordinamento a prova di manomissione, un tempo di impegno con limite superiore privo di fiducia e la resistenza all'equivocazione.
Entrambe le affermazioni escludono, per §11: l'autorialità senza sig_alg ≥ 0x01, l'intento e la tempistica a granularità sub-ancoraggio. Nessuna delle due lascia che alcuna affermazione poggi su received_at.
Tetto dell'affermazione (vincolo vincolante di lancio — risolto §16.15 Q1). Per i qub gratuiti / di default (kind=0x02), l'affermazione con ambito kind=0x02 sopra è il tetto di ciò che qualsiasi superficie di prodotto, marketing, condizioni o rendering di prova può asserire. Nessuna superficie può affermare o implicare che il log dimostri il contenuto o il round di sblocco di un qub di default — il log dimostra l'ordinamento + un tempo di impegno con limite superiore privo di fiducia di un ciphertext opaco. La prova del contenuto e del round provengono esclusivamente dalla verifica esistente del bundle .qub §11, che è indipendente dal log. Questo è un bloccante rigido di lancio sui testi, non una preferenza stilistica; è la risoluzione che mantiene onesto il percorso predefinito cieco ai byte.
16.12 Versionamento e coordinamento W3
Non c'è alcun bump del wire SealedQub e quindi nessun bump della versione del protocollo (§12.1): il log è un sidecar che si impegna a campi e byte esistenti, quindi non entra nella cronologia delle versioni del protocollo §12.2. Il drand_chain_version opzionale di W3 resta intatto e rimane l'unico campo SealedQub opzionale. Il log introduce invece i propri spazi di versione indipendenti — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — rispecchiando l'indipendenza della versione del wrapper §12.4 (il wrapper porta un byte di versione indipendente dalla versione del protocollo, e le versioni del log seguono la stessa separazione).
La consegna della prova è recuperata per default, con un'opzionale ride-along. Una prova non può esistere al momento del sigillo (l'ancoraggio non è ancora stato scritto), quindi il bundle .qub al momento del sigillo resta privo di prova. Il verificatore di W7 recupera GET …/proof una volta, oppure in modalità completamente offline ricostruisce la prova dall'AnchorBundle pubblico tramite una query Arweave su Log-Id. Il bundle .qub (W7) riserva un membro inclusion_proof opzionale — assente al sigillo, popolato da una ri-esportazione post-ancoraggio per l'archiviazione fredda — seguendo lo stesso pattern "opzionale, omesso per default, additivo" del drand_chain_version di W3.
16.13 Retention
Le finestre di retention per la coda aperta del LogDO, il substrato R2 che serve le prove, i contatori del circuit breaker di ancoraggio e la coda di fallback del bundler sono specificate in docs/DATA-RETENTION.md. Principio: l'archiviazione calda per-voce del log (LogDO) è reclamabile post-ancoraggio; il suo materiale di audit — l'archivio di nodi Merkle indicizzato per coordinate (level, index) + i corpi di foglia indirizzati per seq (§16.9) + gli ancoraggi Arweave — è permanente. Reclamare una foglia fredda dal DO non invalida mai una prova emessa, perché una prova si risolve contro quell'archivio di nodi R2 permanente e l'ancoraggio Arweave, non contro il DO (e il vettore di test a DO cancellato di §16.9 lo dimostra).
16.14 Vettori di test
W5 spedisce la fixture cross-linguaggio tlog_v1.json (§16.8) più vettori elaborati: una foglia kind=0x01 e una kind=0x02 → leaf_hash; la radice cumulativa a 5 foglie; una prova di inclusione; una prova di consistenza; un AnchorBundle; e un id di DataItem. Questi vivono accanto ai vettori del wrapper esterno §14.5 e sono esercitati sia dall'implementazione Rust (qub-core) sia da quella TypeScript (Worker).
16.15 Decisioni di revisione (W5 — risolte)
La revisione esterna W5 (un passaggio di design avversariale + sign-off del proprietario) è completata. Ogni decisione sotto è assestata e riflessa nel testo §16 sopra; i vincoli vincolanti di lancio sono riaffermati alla fine. L'implementazione può procedere sotto di essi.
- Onestà della foglia del percorso predefinito (
kind=0x02) — RISOLTO. Spedire lo split a due kind di foglia come specificato:kind=0x02non impegna nébody_hashnédrand_round. Nessun campo*_body_hashsul percorso cieco ai byte (sarebbe il più leggibile falso segnale "verificato" per gli integratori ed è una comodità che §11 già fornisce dal bundle). Non richiedere il sigillo lato server per i qub attestati nel log (ciò forzerebbe il plaintext attraverso il Worker e distruggerebbe il moat del crypto-shredding). Qualsiasi scorciatoia auto-descrittiva appartiene al bundle.qub/ envelope di prova come campo ricalcolato dal verificatore, mai un campo della foglia. Tetto dell'affermazione confermato dal proprietario: §16.11. - Responsabilità di equivocazione / omissione — RISOLTO. La chiave di ricevuta di sigillo è pinnata in
LogProfile+ controfirmata daanchor_owner(chiudendo la precedente contraddizione "nessuna chiave di firma"; §16.6). Modello di witness di lancio: ricevuta pinnata + metodologia di monitoraggio + percorrimento della catena prev + teste duali auto-pubblicate (repository GitHub pubblico di proprietà di qub, social best-effort), pubblicizzato come rilevabile + con ricevuta, mai testimoniato in modo indipendente. Un vero witness terzo è rinviato a un bump della governance §15. - Radice di fiducia anchor-owner pinnata + rotazione — RISOLTO. Adottare il pin
LogProfile(§16.6); il verificatore controllaanchor_tx.owner == anchor_ownere verifica localmente il binding tx data → tx_id. La governance della rotazione è un'estensione di §15 da costruire (trigger §15.3 aggiunto), non un riuso; le rotazioni pianificate controfirmano, le rotazioni guidate da compromissione ricadono sul bump §15 con il controllo del fork che limita il danno. - Blinding della foglia dei qub privati — RISOLTO. Mantenere il blinding per i qub privati (
ref = SHA3-256(qub_id ‖ log_blind_secret)),qub_idgrezzo per i qub pubblici (già §16.2.1),chashcome legame autonomo.log_blind_secretè un segreto di grado correlazione/Sybil, ruotare solo in avanti (§16.2.1). received_at— RISOLTO. Mantenerlo nella foglia, impegnato ma esplicitamente non probatorio; mai esposto come prova o corroborazione di disputa su alcuna superficie. Qualsiasi controllo di sanità del monitor confronta contro il tempo di blocco ArweaveT, non contro l'anchored_atcontrollato dall'operatore (§16.6).- Tempistica dimostrabile di fascia gratuita — RISOLTO (sign-off del proprietario). La durabilità non regredisce; solo il tempo di impegno con limite superiore dimostrabile si fa più grossolano fino al tempo di blocco dell'ancoraggio. I testi di fascia gratuita non usano alcuno SLA numerico ("…aggiunto al prossimo ancoraggio del log, tipicamente giornaliero"); la prova con precisione all'ora è una proprietà T3 a pagamento, divulgata alla superficie di confronto dei livelli + nelle condizioni (§16.1).
- Albero cumulativo su Workers — RISOLTO. Singolo albero cumulativo RFC 9162 + LogDO a singolo scrittore con frontiera in cache (comodo margine rispetto al tetto DO di ~1k scritture/sec; rinviare lo sharding Merkle-di-radici-di-shard fin quasi a esso). Precondizione bloccante: archivio di nodi R2 indicizzato per coordinate
(level, index)+ il vettore di test a foglia fredda con DO cancellato (§16.9);< 300 msè un gate di lancio misurato su due DO serializzati (§16.10). - Schema di firma ANS-104 + deep-hash — RISOLTO. RSA-PSS (sig type 1, riutilizzando il JWK del wallet di ancoraggio dedicato); Ed25519 rinviato al percorso PQ §15. Il deep hash SHA-384 scritto a mano è gated sulla fixture cross-impl in entrambe le direzioni, un controllo di interoperabilità solo-statico con bundler di riferimento, il round-trip su
crypto.subtlecondiviso e il monitor di accettazione Arweave post-bundle (§16.8).
Vincoli vincolanti di lancio (da portare nell'implementazione + revisione di prodotto/legale):
- Tetto dell'affermazione (Q1/Q6). Nessuna superficie può dire che il log dimostri il contenuto o il round di sblocco di un qub di default; l'affermazione consentita è ordinato, in modo a prova di manomissione, con un tempo di impegno con limite superiore privo di fiducia. I testi del timestamp di fascia gratuita non portano alcuna latenza numerica; la prova con precisione all'ora è solo T3 a pagamento.
- Onestà sul witness (Q2). Pubblicizzare l'equivocazione come rilevabile + con ricevuta, mai testimoniata in modo indipendente.
- Chiavi di ricevuta + ancoraggio (Q2/Q8). La chiave di ricevuta è pinnata + controfirmata; il wallet di ancoraggio è il proprio JWK distinto dal wallet di upload.
- Gate del deep-hash (Q8). Nessun ancoraggio o tx T3 viene spedito finché la fixture in entrambe le direzioni + il controllo di interoperabilità non passano; il monitor di accettazione genera un alert in caso di fallimento.
- Precondizione di archiviazione (Q7). L'archivio di nodi indicizzato per coordinate + il vettore a foglia fredda con DO cancellato sono prerequisiti per la garanzia "la reclamazione non invalida mai una prova".