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à:

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:

  1. Solo HTTPS. La stringa DEVE iniziare con la sequenza di byte https://. Qualsiasi altro schema — http, ftp, javascript, data, file, ecc. — è rifiutato.
  2. Limite di lunghezza. ≤ 2.048 byte (limite pratico degli URL del browser).
  3. Controllo NFC + codepoint ostili. Stessa regola di title e reflection — i codepoint bidi-override / a larghezza zero / del blocco tag / BOM / C0 / C1 sono rifiutati. La definizione coincide con crate::handle::contains_hostile_text_codepoint in Rust e con workers/api/src/utils/unicode.ts::isHostileCodepoint in TS (da tenere allineati).
  4. Nessuno spazio bianco, nessun carattere di controllo ASCII. Spazi bianchi / DEL / byte sotto 0x20 ovunque nell'URL sono rifiutati — chiude il vettore di iniezione \n/\t che la regola bidi non copre.
  5. 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.

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:

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à:

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

10.2 Elementi vietati

Elemento Gestione
HTML grezzo (<div>, <script>, ecc.) Rimosso completamente. Nessun HTML passa.
Immagini (![alt](url)) 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:

  1. Parsare Markdown utilizzando pulldown-cmark (o equivalente).
  2. Attraversare l'AST e scartare qualsiasi nodo non presente nell'allowlist (§10.1).
  3. Per i nodi link: emettere l'URL come testo visibile, non come elemento <a> cliccabile.
  4. Convertire l'AST filtrato in una rappresentazione intermedia tipizzata (ad es. un enum MarkdownNode con solo varianti sicure). L'HTML grezzo è strutturalmente non rappresentabile in questa IR.
  5. Renderizzare dalla IR tipizzata al livello di view target (ad es. componenti di view reattivi, nodi DOM). Nessuna concatenazione di stringhe HTML o innerHTML in 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


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_alg0x01, 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.

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
0x020xFF 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:

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.

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:

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

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:

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:

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:

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:

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 fidatebody_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_hashdrand_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):

  1. 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.
  2. Metodologia di monitoraggio pubblicata + percorrimento della catena prev — la catena prev dell'ancoraggio è percorsa testa→genesi; un fork (due ancoraggi a una size con root diversa, o un prev rotto) è prova pubblicabile di cattiva condotta. Il rilevamento dell'equivocazione è un impegno operativo dichiarato, non un'assunzione silenziosa.
  3. 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):

  1. La fixture cross-linguaggio tlog_v1.json (Rust + TS, il pattern wrapper_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).
  2. 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).
  3. Il percorso deep-hash + RSA-PSS deve fare round-trip attraverso le stesse primitive crypto.subtle usate in produzione, così che l'encoder interno sia byte-compatibile.
  4. 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.

InclusionProofGET /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 }.

ConsistencyProofGET /api/v1/log/consistency?first=<size_a>&second=<size_b>: ver:u8, first_size:u64, second_size:u64, first_root:bstr[32], second_root:bstr[32], nodes:[bstr[32]], first_anchor, second_anchor. 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:

  1. Gate della prima metà (auth, validazione, chiave di shard di idempotenza) — invariati.
  2. 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 un ctx.waitUntil dopo il submit Arweave).
  3. In modo sincrono await LogDO.append(leaf) — un'unica RPC DO in-colo; il singolo scrittore assegna seq, estende la catena di voci e aggiorna la frontiera. (RMW su stato condiviso → DO, mai KV.) La RPC append fa solo quello — il lavoro di chiusura batch Merkle O(batch) gira fuori da questa RPC sull'alarm del LogDO, altrimenti il p95 di append ha un picco a ogni LOG_BATCH_MAX_LEAVES-esimo sigillo.
  4. 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.
  5. Un solo ctx.waitUntil accoda 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. Nessun ctx.waitUntil dentro 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=0x02leaf_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.

  1. Onestà della foglia del percorso predefinito (kind=0x02) — RISOLTO. Spedire lo split a due kind di foglia come specificato: kind=0x02 non impegna né body_hashdrand_round. Nessun campo *_body_hash sul 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.
  2. Responsabilità di equivocazione / omissione — RISOLTO. La chiave di ricevuta di sigillo è pinnata in LogProfile + controfirmata da anchor_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.
  3. Radice di fiducia anchor-owner pinnata + rotazione — RISOLTO. Adottare il pin LogProfile (§16.6); il verificatore controlla anchor_tx.owner == anchor_owner e 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.
  4. Blinding della foglia dei qub privati — RISOLTO. Mantenere il blinding per i qub privati (ref = SHA3-256(qub_id ‖ log_blind_secret)), qub_id grezzo per i qub pubblici (già §16.2.1), chash come legame autonomo. log_blind_secret è un segreto di grado correlazione/Sybil, ruotare solo in avanti (§16.2.1).
  5. 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 Arweave T, non contro l'anchored_at controllato dall'operatore (§16.6).
  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).
  7. 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).
  8. 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.subtle condiviso e il monitor di accettazione Arweave post-bundle (§16.8).

Vincoli vincolanti di lancio (da portare nell'implementazione + revisione di prodotto/legale):