Specifica del protocollo qub

qub è un protocollo per impegni temporali crittografici: un sistema per sigillare parole a una data futura e verificare in seguito esattamente cosa è stato sigillato, quale drand round ne ha regolato il rilascio e, quando è disponibile una transazione di archiviazione o una prova del log di trasparenza, un limite superiore con marcatura temporale indipendente del momento in cui il ciphertext è stato impegnato.

Tre primitive lo rendono possibile. drand è un beacon di casualità decentralizzato: la data di rivelazione è imposta crittograficamente anziché dalla buona volontà di qub. Un archivio durevole insieme a un log di trasparenza append-only conserva i byte sigillati e ancora impegni in batch nell'archivio pubblico permanente; il percorso T3 a pagamento scrive inoltre una singola transazione nell'archivio permanente. ML-DSA-65 è una firma post-quantistica: quando l'autorialità è abilitata, il qub è legato a una coppia di chiavi il cui segreto non lascia mai il dispositivo dell'autore.

Insieme queste primitive producono una dichiarazione bloccata nel tempo e a prova di manomissione, facoltativamente attribuibile e marcabile temporalmente in modo indipendente: 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
Rilascio del documento 1.0.0 (protocol-v1.0.0)
Protocollo wire 0x01
Wrapper esterno 0x01
Data di efficacia 2026-09-23
Stato Corrente
Revisionato fino a 2026-09-23

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],        // Random, generated locally
    created_at:     i64,             // Unix seconds UTC
    unlock_at:      Option<i64>,     // Unix seconds UTC; None while composing
    visibility:     u8,              // 0x00 = private; 0x01 = public
    content_type:   u8,              // 0x01 text; 0x03 pact; 0x04 verdict
    plaintext:      Vec<u8>,         // Raw body bytes (UTF-8 for text)
    sender_label:   Option<String>,  // Display name; V2-signed when authorship is enabled
    title:          Option<String>,  // Plaintext countdown title; bound via title_hash
    reply_to:       Option<[u8; 32]>,// Parent qub_id; V2-signed when authorship is enabled
    outcome_at:     Option<i64>,     // Optional future judgment time; bound to qub_id
    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,              // Protocol major version (0x01 for v1)
    qub_id:              [u8; 32],        // Derived (see §4.1)
    content_type:        u8,              // Content type registry (see §6)
    created_at:          i64,             // Unix seconds UTC
    unlock_at:           i64,             // Unix seconds UTC
    outcome_at:          Option<i64>,     // When reality renders judgment; bound to qub_id
    sender_label:        Option<String>,  // Not in qub_id; V2-signed when authorship is enabled
    reply_to:            Option<[u8; 32]>,// Parent qub_id; not in qub_id; V2-signed when present
    body:                Vec<u8>,         // UTF-8 text or canonical CBOR pact/verdict body
    body_hash:           [u8; 32],        // SHA3-256(body) (see §4.2)
    sig_alg:             u8,              // Signature algorithm (see §9.2)
    author_signature:    Option<Vec<u8>>, // Set when sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Set when sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
    cosigner_signature:  Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}

Baseline (qub testuale non firmato): version = 0x01, content_type = 0x01, sig_alg = 0x00; i campi di firma e cofirmatario sono assenti. Possono essere presenti altri campi di metadati facoltativi.

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). Questo è l'artefatto wire interno: la consegna pubblica memorizza questi byte nudi, mentre la consegna privata li avvolge in OuterWrapper prima dell'archiviazione (§13).

SealedQub {
    version:           u8,              // Protocol major version (0x01 for v1)
    qub_id:            [u8; 32],        // Same as QubEnvelope.qub_id
    visibility:        u8,              // 0x00 = private/wrapped; 0x01 = public/bare
    unlock_at:         i64,             // Unix seconds UTC
    outcome_at:        Option<i64>,     // Surfaced on the verdict-watch CTA
                                        //   before reveal; mirrors QubEnvelope.outcome_at;
                                        //   bound to qub_id via the §4.1 preimage.
    drand_chain_id:    String,          // drand chain hash (hex string)
    drand_round:       u64,             // Target drand round number
    drand_chain_version: Option<u8>,    // W3 — chain-migration version. Absent / 0 = quicknet
                                        //   (the only chain today). Lets a future chain swap
                                        //   be expressed on the wire without a breaking format
                                        //   change. NOT part of the §4.1 qub_id preimage, so its
                                        //   addition never alters an existing qub's identity.
    tlock_ciphertext:  Vec<u8>,         // tlock-encrypted QubEnvelope CBOR bytes
    recipient_pubkey:  Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
                                        //   but not interpreted by the v1 reference viewer
    title:             Option<String>,  // Plaintext title surfaced on the viewer
                                        //   countdown before reveal. Bound to qub_id
                                        //   via title_hash (§4.1). 1..=100 NFC code
                                        //   points, no hostile/control code points.
}

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>,       // Carried from both wire layers; drives the verdict-watch block
    drand_chain_id:      String,
    drand_round:         u64,
    sender_label:        Option<String>,
    title:               Option<String>,    // Carried forward from 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 encoded bytes)
"qub_id"              (7 encoded bytes)
"sig_alg"             (8 encoded bytes)
"version"             (8 encoded bytes)
"reply_to"            (9 encoded bytes)   ← only if present (reply chains)
"body_hash"           (10 encoded bytes)
"unlock_at"           (10 encoded bytes)
"created_at"          (11 encoded bytes)
"outcome_at"          (11 encoded bytes)  ← only if present (verdict mechanic)
"content_type"        (13 encoded bytes)
"sender_label"        (13 encoded bytes)  ← only if present
"author_pubkey"       (14 encoded bytes)  ← only if present
"cosigner_pubkey"     (16 encoded bytes)  ← only if present (pact cosign)
"author_signature"    (17 encoded bytes)  ← only if present
"cosigner_signature"  (19 encoded bytes)  ← only if present (pact cosign)

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 encoded bytes)   ← only if present
"qub_id"            (7 encoded bytes)
"version"           (8 encoded bytes)
"unlock_at"         (10 encoded bytes)
"outcome_at"        (11 encoded bytes)  ← only if present (verdict mechanic)
"visibility"        (11 encoded bytes)
"drand_round"       (12 encoded bytes)
"drand_chain_id"    (15 encoded bytes)
"recipient_pubkey"  (17 encoded bytes)  ← only if present
"tlock_ciphertext"  (17 encoded bytes)
"drand_chain_version" (20 encoded bytes) ← only if present (W3; absent = quicknet)

PactTerms (corpo del patto, content_type 0x03):

"notes"         (6 encoded bytes)  ← only if present
"terms"         (6 encoded bytes)
"title"         (6 encoded bytes)
"party_a"       (8 encoded bytes)
"party_b"       (8 encoded bytes)
"pact_version"  (13 encoded bytes)

PactTerm (riga dell'array terms):

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

PartyIdentifier (mappa party_a / party_b):

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

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"          ||  // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
    version              ||  // u8 (1 byte)
    content_type         ||  // u8 (1 byte)
    created_at           ||  // i64 big-endian (8 bytes)
    unlock_at            ||  // i64 big-endian (8 bytes)
    outcome_at_or_zero   ||  // i64 big-endian (8 bytes; 0 when outcome_at is absent)
    drand_round          ||  // u64 big-endian (8 bytes)
    body_hash            ||  // [u8; 32] (32 bytes)
    title_hash               // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output

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: una revisione dell'implementazione precedente al rilascio ha esteso il preimage da 92 a 100 byte per integrare il campo facoltativo 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: una successiva revisione dell'implementazione precedente al rilascio 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'ora di sblocco mostrata corrisponde in modo dimostrabile al 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)   if title is present
title_hash = [0u8; 32]                         if title is absent

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 = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
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

Questa è la mappatura tlock di riferimento (CurrentRound di drand). drand pubblica il round N in chain_genesis_time + (N - 1) * chain_period_seconds, quindi la formula seleziona il round corrente a unlock_at, ossia il round la cui firma è la prima che può usare un visualizzatore che arriva a unlock_at.

Proprietà di allineamento (il caso rilevante in pratica): quando (unlock_at - chain_genesis_time) è esattamente divisibile per chain_period_seconds, la firma del round selezionato viene pubblicata esattamente a unlock_at, mai prima. Questo vale sempre per il deployment di riferimento: il tempo di genesi di quicknet (1692803367) è divisibile per il suo periodo di 3 secondi e le app di riferimento fissano gli orari di sblocco a minuti interi. Per un unlock_at non allineato, la firma del round selezionato viene pubblicata rigorosamente meno di un periodo prima di unlock_at: la precisione temporale dell'impegno è pari a un periodo del beacon.

Mappatura legacy precedente al rilascio e tolleranza lato sblocco: la mappatura originale era ceil((unlock_at - chain_genesis_time) / chain_period_seconds), che, nel caso allineato al periodo descritto sopra, selezionava il round pubblicato un intero periodo prima di unlock_at, rendendo il ciphertext decifrabile in anticipo di esattamente un periodo. Le due mappature differiscono esattamente di +1 quando il delta è divisibile per il periodo e coincidono negli altri casi. Poiché drand_round è incluso nel preimage immutabile di qub_id (§4.1), gli artefatti sigillati con la mappatura legacy non possono essere ri-derivati; i verificatori che eseguono il controllo incrociato del round al passo 6a di §8 DEVONO quindi accettare un drand_round memorizzato uguale o al round derivato o al round derivato meno uno (e DEVONO richiedere che il round della stanza tlock sia esattamente uguale al round memorizzato). La tolleranza anticipa la prima firma utilizzabile al massimo di un periodo. Il servizio di staging dei patti applica la stessa tolleranza quando ri-deriva il qub_id di un patto in staging (sia allo staging sia alla cofirma): se la mappatura corrente non riproduce il qub_id impegnato e il delta è divisibile per il periodo, riprova con il round meno uno e sigilla il patto finalizzato al round effettivamente vincolato da qub_id, senza usare ciecamente il round ricalcolato, cosa che renderebbe l'artefatto permanentemente non derivabile.

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() Artefatto wire interno; memorizzato nudo per la consegna pubblica o avvolto per quella privata, quindi recuperato dal visualizzatore
QubEnvelopeCbor CBOR canonico di QubEnvelope serialize_qub_envelope() Input cifratura tlock, output decifratura tlock

5.1 Regole di costruzione

// Production code — only through CBOR serialisers:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);

// There is deliberately NO From<Vec<u8>> implementation.
// You cannot accidentally wrap arbitrary bytes in a wire format type.

// Accessing raw bytes:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();

5.2 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 for structured/v1
    title:         String,                // ≤ 200 bytes, NFC
    terms:         Vec<PactTerm>,         // ≤ 20 rows
    party_a:       PartyIdentifier,       // initiator
    party_b:       PartyIdentifier,       // counter-signer
    notes:         Option<String>,        // ≤ 5,000 bytes, NFC; absent key if none
}

PactTerm       { key: String (≤ 100 bytes), value: String (≤ 2,000 bytes) } // NFC
PartyIdentifier{ label: String (≤ 100 bytes), contact: Option<String (≤ 320 bytes)> }

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 for structured/v1
    outcome:         u8,                  // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
    reflection:      Option<String>,      // ≤ 2,000 bytes NFC; "what changed, what did you learn"
    evidence_url:    Option<String>,      // ≤ 2,048 bytes; HTTPS only; absent key when omitted
}

Ordine canonico delle chiavi CBOR:

"outcome"          (8 encoded bytes)
"reflection"       (11 encoded bytes)  ← only if present
"evidence_url"     (13 encoded bytes)  ← only if present
"verdict_version"  (16 encoded bytes)

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 ammette 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. User composes plaintext and metadata in ComposeQub.
 2. Validate:
    a. body is non-empty.
    b. body size ≤ max for content_type and user tier (see §6).
    c. unlock_at is in the future.
    d. unlock_at ≤ created_at + 10 years.
    e. content_type is a known, supported value.
    f. visibility is 0x00 (private) or 0x01 (public).
 3. Compute body_hash = SHA3-256(body).
 4. Set created_at = current Unix seconds UTC.
 5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
    compute drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
    (§4.3). (Computed here, before qub_id, because drand_round is bound into the
    qub_id preimage—§4.1.)
 6. Compute qub_id (see §4.1), folding in drand_round from step 5.
 7. Construct QubEnvelope with all fields.
 8. Serialise QubEnvelope using canonical CBOR → bytes B.
    Assert: serialised output matches canonical profile (§3).
 9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C and matching qub_id, version,
    visibility, unlock_at, drand_chain_id, and drand_round.
11. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12. Select the delivery shape from visibility:
    a. Private (0x00): generate K = 32 random bytes and N = 12 random bytes
       using a CSPRNG. Compute W = wrap_sealed_qub(SealedQubCbor,
       qub_id=qub_id, key=K, nonce=N) per §13. Upload payload = W.
    b. Public (0x01): upload payload = bare SealedQubCbor; do not generate K.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit the selected upload payload to the qub upload service. For a private
    browser seal, the service is byte-blind to the inner SealedQubCbor and never
    receives K. The Builder `/api/v1/seal` route is an explicit exception: it
    receives plaintext and caller-supplied K in memory, then persists neither.
16. Receive arweave_tx_id from the service. For private delivery, construct
    `<origin>/c/<arweave_tx_id>#<base64url(K)>` (or the equivalent short-code
    path). For public delivery, omit the fragment. Browsers do not transmit URL
    fragments to servers, so K from the browser-seal path is not observed by
    qub.social or any storage gateway.

Livello dei tag dell'archivio (fuori banda). Il servizio di upload di qub allega un insieme deliberatamente piccolo di tag della transazione di archiviazione accanto al payload di upload selezionato. Content-Type=application/octet-stream è normativamente richiesto. Il servizio di riferimento allega inoltre tre tag facoltativi quando il creatore sceglie di esporli: Intent (intento di composizione validato contro l'allowlist: announcement, thesis, prediction, letter, secret, commitment, proof oppure verdict emesso dal sistema), Author (fingerprint della chiave pubblica del creatore del §9.3 come esadecimale minuscolo di 64 caratteri) e Parent-Tx-Id (id della transazione di archiviazione del qub padre per le catene di risposta, base64url di 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. Per la consegna privata, il wrapper esterno (§13) cifra il riconoscibile artefatto SealedQub interno, quindi raccogliere i wrapper archiviati e ottenere le firme drand pubbliche non basta comunque a recuperare il corpo senza K; i tag di archiviazione restano deliberatamente metadati pubblici.

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. Viewer opens delivery URL. Extract arweave_tx_id from the path and retain
    the optional URL fragment. Do not assume a missing fragment is an error:
    public/bare delivery intentionally has no K.
 2. Check denylist. If tx_id is denylisted → display block message. Stop.
 3. Fetch the stored bytes (with multi-gateway fallback).
 3a. Resolve the delivery shape structurally:
    a. If the bytes parse as OuterWrapper, require a well-formed 32-byte K in
       the URL fragment, require wrapper version 0x01, and unwrap per §13.
       Any missing/malformed K or AEAD failure is a terminal error.
    b. Otherwise require the bytes to parse as bare SealedQubCbor; no K is
       required. If neither shape parses, report an integrity error.
 4. Parse SealedQubCbor → SealedQub.
 5. Validate: SealedQub.version is known (0x01), visibility is known, and the
    delivery shape matches it (wrapped = 0x00 private; bare = 0x01 public).
    Reject any mismatch or unknown value.
 6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
 6a. Round-binding check. Recompute expected_round from
    SealedQub.unlock_at per §4.3. Reject unless SealedQub.drand_round ==
    expected_round OR SealedQub.drand_round == expected_round - 1 (the
    legacy pre-release mapping—see §4.3), AND the round baked into the tlock
    ciphertext stanza (read via the age/tlock header, no signature required)
    == SealedQub.drand_round exactly. The stanza round is the one that
    actually gates decryption; without this check a malicious creator could
    bind the ciphertext to an already-past round while displaying a future
    countdown, so anyone reading the stored bytes could decrypt before
    unlock_at. Implementations with no chain identity (test mocks) skip this
    check.
 7. Once current time ≥ SealedQub.unlock_at:
    a. Fetch drand round signature for SealedQub.drand_round from drand network.
    b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
 8. Parse B → QubEnvelope.
 9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
    Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
    Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
    Fail → integrity error.
12a. Verify: QubEnvelope.outcome_at == SealedQub.outcome_at (both absent, or
    both present and equal). Fail → integrity error.
12b. Content re-derivation. Recompute qub_id per §4.1 from the decrypted
    fields — (QubEnvelope.version, content_type, created_at, unlock_at,
    outcome_at, SealedQub.drand_round, QubEnvelope.body_hash,
    title_hash(SealedQub.title)) — and verify it equals SealedQub.qub_id.
    Fail → integrity error. The pairwise checks in steps 10-12a only prove
    the two layers agree with EACH OTHER; a forger who rewrites a bound
    field consistently on both surfaces (a pre-reveal title swap, or a
    post-round body swap with a recomputed body_hash re-encrypted to the
    same round under the same qub_id) passes them all. Only re-deriving
    the identity from content closes this.
13. Verify: QubEnvelope.content_type is known and renderable.
    Known values: 0x01 (text), 0x03 (pact), 0x04 (verdict).
    Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using the appropriate renderer (see §10 for text and §6 for pact/verdict).
17. Construct RevealedQub for display.

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 Stato
0x00 Nessuna firma (non firmato) — — Attivo
0x01 ML-DSA-65 (FIPS 204) 1.952 byte 3.309 byte Attivo
0x02 Ed25519 32 byte 64 byte Costante riservata; non supportata nel protocollo v1

I visualizzatori del protocollo v1 DEVONO rifiutare ogni valore al di fuori di {0x00, 0x01}, incluso il valore riservato 0x02. La riserva impedisce il riutilizzo accidentale; non costituisce attivazione. La sua attivazione richiede la modifica governata descritta nel §15.

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

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

signature = Sign(author_secret_key, sig_input)

sender_label_hash 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"  ||    // domain separator (17 bytes)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 bytes)
    body_hash            ||    // [u8; 32] (32 bytes)
    unlock_at            ||    // i64 big-endian (8 bytes)
    0x00                       // u8 (1 byte): MUST be 0x00 in v1.0
)

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

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
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. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
   only accepted form — the legacy V1 fallback is retired (§9.3), so a
   signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."

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. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
   Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3 (V2 only — the
   legacy V1 fallback is retired; all pact clients produce V2 signatures).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."

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 che detenga i byte memorizzati (e K per un qub privato/avvolto) può verificare l'artefatto crittografico senza la cooperazione di qub. Un'affermazione di esistenza con marcatura temporale indipendente richiede inoltre l'inclusione verificata in una singola transazione dell'archivio permanente oppure una prova verificata del log di trasparenza §16.

1. Obtain the stored bytes. For a private delivery, also obtain K from the
   delivery-link fragment.
2. Resolve the delivery shape (§8 step 3a): unwrap OuterWrapper with K, or
   accept bare SealedQubCbor. Require shape/visibility agreement.
3. Parse SealedQubCbor → SealedQub; validate protocol version, visibility,
   content type, chain identity, and structural bounds.
4. Recompute expected_round from unlock_at (§4.3); require the stored round
   (allowing the documented legacy minus-one case) and ciphertext-stanza round
   to agree exactly as §8 step 6a specifies.
5. Obtain the drand signature for SealedQub.drand_round and verify its BLS
   signature against the pinned chain public key.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify envelope/sealed equality for qub_id, unlock_at, and outcome_at.
10. Recompute qub_id from the decrypted fields, sealed drand_round, and title;
    require it to equal the carried qub_id (§4.1).
11. If sig_alg != 0x00, verify the V2 author signature (§9.4). If cosigner
    fields are present, verify their pairing, key separation, and signature
    (§9.7).
12. For an existence-time claim, independently verify either:
    a. the permanent-storage transaction's data-to-id binding, owner, block
       inclusion, and block timestamp; or
    b. a §16 inclusion proof through its pinned anchor and anchor block time.
13. Report each verified claim separately; do not collapse an absent storage
    or anchor proof into a successful timing verdict.

Cosa dimostra la verifica:

Input di prova Cosa stabilisce
Bundle/artefatto sigillato valido + firma drand Il corpo recuperato corrisponde a body_hash; i metadati vincolati in qub_id sono intatti; il ciphertext è legato al drand round dichiarato; e tale round è trascorso. Questo non stabilisce quando è stato creato il ciphertext.
Firma V2 valida dell'autore/cofirmatario Il detentore o i detentori delle corrispondenti chiavi segrete hanno autenticato la superficie firmata nel §9.3.
Singola transazione di archiviazione del qub verificata in modo indipendente L'esatto ciphertext memorizzato esisteva non oltre la relativa marcatura temporale di blocco.
Prova valida del log di trasparenza ancorato L'affermazione specifica per tipo di foglia del §16.11, incluso un limite superiore sul tempo dell'impegno dato dal blocco di ancoraggio.

Cosa la verifica NON dimostra:

Non-prova Perché
Autorialità Il sender_label è decorativo. Senza sig_alg ≥ 0x01, chiunque potrebbe aver sigillato questo contenuto.
Intento L'artefatto dimostra byte e relazioni crittografiche, non ciò che il creatore intendeva soggettivamente.
Impegno preesistente dal solo .qub Un creatore può assemblare un bundle valido dopo che il round vincolato è trascorso. La firma drand incorporata dimostra che il round è trascorso, non che il ciphertext esisteva prima.
Ora esatta del clic su Sigilla La marcatura temporale del blocco di archiviazione o ancoraggio è un limite superiore verificabile in modo indipendente e può essere successiva all'azione locale dell'utente. Le asserzioni sealed_at / received_at non hanno valore probatorio.

Il log di trasparenza implementato (§16) estende la verifica tra più qub con un ordinamento a prova di manomissione e un limite superiore sul tempo dell'impegno privo di fiducia (il tempo del blocco di ancoraggio), con ambito definito dal tipo di foglia (§16.11). Non aggiunge autorialità né intento; per il percorso di upload predefinito, cieco ai byte, non dimostra da solo body_hash o drand_round, che continuano a provenire dai controlli dell'artefatto.


12. Versionamento e controllo dei rilasci

I rilasci del documento, il protocollo wire interno e il wrapper esterno sono spazi di versione separati. Un chiarimento relativo soltanto al documento non modifica quindi silenziosamente i byte, e una futura migrazione wire non può mascherarsi da revisione editoriale.

12.1 Versione del rilascio del documento

Questa specifica usa rilasci semantici del documento (MAJOR.MINOR.PATCH) e un tag Git immutabile denominato protocol-v<release>.

Lo stato del rilascio è Bozza (non ancora normativo), Corrente (l'unico obiettivo di implementazione raccomandato) oppure Superato (conservato per la verifica storica). La route non versionata /protocollo mostra il rilascio Corrente; il tag di rilascio conserva la sorgente esatta e ogni locale pubblicato insieme a essa. La modifica dello stato o del numero di rilascio richiede l'aggiornamento di questa tabella e della cronologia dei rilasci nella stessa modifica revisionata.

Rilascio del documento Data di efficacia Stato Protocollo wire Wrapper Sorgente
1.0.0 2026-09-23 Corrente 0x01 0x01 protocol-v1.0.0

12.2 Versione del protocollo

Il campo version (u8) sia in SealedQub sia in QubEnvelope identifica la versione major del protocollo.

12.3 Cronologia delle versioni del protocollo

Versione Valore Descrizione
v1 0x01 Consegna privata/avvolta e pubblica/nuda; corpi di testo (0x01), patto (0x03) e verdetto (0x04); firma V2 ML-DSA-65 dell'autore/cofirmatario; tlock quicknet drand; SHA3-256.

12.4 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.5 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 Attivo per la consegna privata
— 0x02–0xFF Riservato Futuro

I visualizzatori DEVONO rifiutare versioni del wrapper sconosciute con un errore chiaro. Il protocollo mantiene intenzionalmente stretto lo spazio della versione del wrapper finché non appare un driver di migrazione concreto (ad es. linee guida NIST a favore di un AEAD diverso); uno slot 0x02 sarà allocato nella stessa revisione che introduce l'algoritmo.


13. Wrapper di cifratura esterno

13.1 Razionale

I livelli del protocollo (QubEnvelope → tlock → SealedQub) rendono un qub sigillato bloccato nel tempo: il corpo è illeggibile finché unlock_at e la firma del drand round non sono pubblicati. Dopo lo sblocco, tuttavia, la firma del round è pubblica e la forma CBOR canonica di SealedQub è riconoscibile, quindi un harvester che ha indicizzato le transazioni dell'archivio permanente potrebbe decifrare in massa l'intero corpus dei qub.

Per la consegna privata, il wrapper di cifratura esterno chiude quel canale interponendo un ulteriore livello AEAD simmetrico tra il SealedQubCbor canonico e i byte memorizzati. Nel percorso di sigillatura del browser, 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. La rappresentazione memorizzata di un qub privato è quindi un ciphertext opaco il cui plaintext è irrecuperabile senza l'URL che il creatore ha scelto di condividere. La consegna pubblica omette deliberatamente questo livello (§13.8).

Effetto netto:

13.2 Stratificazione

plaintext body                       ← QubEnvelope.body (§2.2)
  ↓ canonical CBOR (§3)
envelope CBOR
  ↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
  ↓ canonical CBOR (§3)
SealedQubCbor bytes                  ← inner wire artifact
  ├─ public (visibility=0x01) ───────────────▶ stored directly (§13.8)
  └─ private (visibility=0x00)
       ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12, this section)
     OuterWrapper CBOR bytes         ← stored private payload

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, see §12.5
    qub_id:     [u8; 32],     // copied from inner SealedQub; AEAD AAD
    nonce:      [u8; 12],     // 96-bit AEAD nonce
    ciphertext: Vec<u8>,      // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}

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 archiviati del qub B Chiave errata (e AAD vincolato indipendentemente) → 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):
    require K.len() == 32 and N.len() == 12 and Q.len() == 32
    I := canonical_cbor_decode(S) as SealedQub
    require I.qub_id == Q               // reject mismatched caller AAD
    C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
    // C includes the 16-byte authentication tag at the end
    return canonical_cbor_encode(OuterWrapper{
        version:    0x01,
        qub_id:     Q,
        nonce:      N,
        ciphertext: C,
    })

unwrap_sealed_qub(OuterWrapper bytes W, key K):
    require K.len() == 32
    O := canonical_cbor_decode(W) as OuterWrapper
    require O.version == 0x01           // §12.5
    P := AES_256_GCM_decrypt(
            key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
         )
    // any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
    S := canonical_cbor_decode(P) as SealedQub
    require S.qub_id == O.qub_id       // explicit inner/outer cross-check
    return P                            // P is the validated SealedQubCbor

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 entra direttamente nella pipeline di archiviazione, senza livello OuterWrapper e senza chiave K:

SealedQubCbor bytes  ──(public)──▶  stored as-is
SealedQubCbor bytes  ──(private)─▶  AES-256-GCM(K, …) ▶ OuterWrapper ▶ stored

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'ID della transazione di archiviazione 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 della rivelazione, i link oEmbed/auto-embed senza frammento e una SEO post-rivelazione più ricca richiedono un link che funzioni senza un segreto che il server non detiene mai (§13.6). Un qub privato può continuare a usare la forma esplicita <qub-embed src="full_delivery_url"> quando chi pubblica fornisce la funzionalità completa comprensiva del frammento.

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   = absent
  drand_round  = 4695446  (= floor((1736294400 - 1595431050) / 30) + 1, §4.3 mapping, drand mainnet params §14.2)
  body         = "Hello, future."  (UTF-8, 14 bytes)
  title        = absent

Intermediate:
  body_hash  = SHA3-256("Hello, future.")
             = 76ab8b3f843c6ed4f2d0fd75b9f457b4
               ad49dd4450f9c22723ae430e3af3211d
  title_hash = [0u8; 32]   (title absent — §4.2.1 sentinel)

Domain separator (10 bytes):
  [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]

Preimage (108 bytes—current protocol v1):
  domain_separator    ||  // 10 bytes
  0x01                ||  // version
  0x01                ||  // content_type
  0x0000000067748580  ||  // created_at as i64 big-endian (1735689600)
  0x00000000677DC000  ||  // unlock_at as i64 big-endian (1736294400)
  0x0000000000000000  ||  // outcome_at_or_zero (outcome_at absent)
  0x000000000047A596  ||  // drand_round as u64 big-endian (4695446)
  body_hash           ||  // 32 bytes
  title_hash              // 32 bytes (all-zeros sentinel; title absent)

Expected output:
  qub_id = SHA3-256(preimage)
         = 4a84e3dfaec32954949c30073f8e6506
           fd3204c1bb97f9162b81c7587afe412e

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. I layout storici dei prototipi precedenti al lancio (nessun qub attivo dipendeva dai primi due) usavano 92 byte prima di outcome_at (3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0) e 100 byte dopo l'aggiunta di outcome_at_or_zero (b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed). L'attuale layout da 108 byte ha poi aggiunto drand_round e il separatore di dominio QUB_ID_V2. Un primo vettore da 108 byte usava la mappatura legacy ceil del round (drand_round = 4695445) e produceva 3a9fcb31b750d985c262fada6d4f777fd6a28be831d941d85c131f5a4bbaf8a4 — tuttora un qub_id valido per quell'input di round, mentre l'esempio sopra segue la mappatura corrente del round del §4.3.

14.2 Mapping del round di sblocco

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

Calculation:
  (1735689600 - 1595431050) / 30 = 4675285.0
  floor(4675285.0) + 1 = 4675286

drand_round = 4675286

Il round 4675286 viene pubblicato a 1595431050 + (4675286 - 1) * 30 = 1735689600 — esattamente a unlock_at, mai prima. (La mappatura legacy ceil precedente al rilascio dava 4675285, pubblicato a 1735689570 — 30 secondi in anticipo; i verificatori accettano quel round legacy secondo §4.3.)

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        = absent

Canonical CBOR key order (PactTerms):
  "notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)

Canonical CBOR key order (PactTerm):
  "key"(4) < "value"(6)

Canonical CBOR key order (PartyIdentifier):
  "label"(6) < "contact"(8)

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 di wrapper di basso livello. Verificano la codifica deterministica di OuterWrapper e l'interoperabilità AEAD indipendentemente dall'invariante della forma di consegna del §13.8; in particolare, il nome storico basic-text-public e il suo visibility = 0x01 interno non rendono i byte avvolti risultanti una consegna pubblica conforme. Un produttore DEVE comunque memorizzare nudi i byte interni pubblici e avvolgere soltanto quelli privati (0x00).

Caso Copertura
basic-text-public Nome storico della fixture di basso livello. La più piccola forma realistica di SealedQub, senza campi opzionali; verifica soltanto i byte del wrapper e non è una consegna memorizzata conforme al §13.8.
with-recipient-pubkey SealedQub con recipient_pubkey impostato (percorso futuro riservato). Esercita un diverso insieme di chiavi CBOR interne; il contenuto distinto della fixture produce indipendentemente un qub_id diverso (recipient_pubkey stesso non è nel preimage del §4.1).
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 fissano nel codice le lunghezze di chiave e firma per ogni primitiva attiva. I byte di versione di sig_alg e del wrapper sono selettori espliciti, ma v1 non esegue negoziazione in-band e ammette soltanto i valori attivi sopra indicati.

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. Registro di Trasparenza e Livelli di Durabilità (Implementato — revisione completata)

Stato. Questa sezione è implementata (W5/UP-B1, Fasi 1–8), con il produttore e l'ambito della radice di fiducia indicati qui. I formati wire, l'hashing e i percorsi del verificatore sono attivi: il Merkle core + tipi canonical-CBOR (qub-core), lo specchio TypeScript + bundler ANS-104 (workers/api/src/crypto/), lo LogDO a scrittore singolo + store R2 a chiave coordinata, il tentativo di log-append /upload, i cron giornalieri di anchor + svuotamento del bundler, gli endpoint di prova GET /api/v1/qub/:tx_id/proof (inclusione) e GET /api/v1/log/consistency (RFC 9162), la prova di inclusione tipizzata trasportata nel bundle .qub (§17.5), il verificatore nativo ANS-104 per anchor (tools/qub-verify), e il hook a doppia testa auto-pubblicata (§16.6). Un /upload riuscito è sempre durevole R2 ma è coperto dal log solo quando LOG_DO è configurato e l'append inline riesce; solo allora la sua risposta porta log_seq, receipt e anchor_status. Se RECEIPT_SK è assente o non valido, il sig_b64url di quella ricevuta è vuoto e non fornisce non-ripudio. Gli attuali percorsi di pubblicazione /seal e di patto pianificano singole transazioni Arweave ma non aggiungono una foglia al log. Nessun codice attualmente esegue la riconciliazione successiva proposta dal commento /upload dopo un fallimento di append. La revisione esterna W5 è completa: §16.15 registra decisioni progettuali e vincoli di lancio, ma tali vincoli non espandono la copertura del produttore appena indicata. Rimangono tre elementi di fiducia/distribuzione vincolati: (a) il wallet dedicato per anchor (ANCHOR_JWK; LogProfile.anchor_owner è ancora il placeholder [0xAB; 32]); (b) la chiave di firma della ricevuta e il pin a chiave pubblica corrispondente (RECEIPT_SK è opzionale e LogProfile.receipt_pubkey è attualmente vuoto); e (c) il repository GitHub self-pubblicato + token (§16.6). Finché i pin per anchor/profilo non sono forniti, un verificatore autonomo riporta lo stato della prova onestamente anziché dichiarare una verifica completamente ancorata e pinnata. Il design è strettamente aggiuntivo e non ci sono modifiche al formato wire SealedQub / QubEnvelope.

16.1 Motivazione e Livelli di Durabilità

Gli attuali percorsi di pubblicazione separano il riconoscimento dalla conferma Arweave: derivano e firmano una transazione individuale, persistono l'artefatto e lo stato esatto di invio in R2, quindi pubblicano asincronamente. Il registro di trasparenza aggiunge un layer di ordinamento ancorato indipendentemente per il sottoinsieme delle richieste generali /upload il cui append LogDO riesce:

Livello Nome Garanzia Quando
T1 R2-primo ack sincrono Livello di durabilità — i byte sigillati e lo stato esatto della pubblicazione vengono scritti nello storage durevole prima che venga restituito il successo. Implementato attraverso i percorsi di pubblicazione attuali.
T2 Inclusione del registro di trasparenza in batch Impegno solo-append, evidenza di manomissione + ordinamento totale una volta incluso e ancorato. Produttore attuale: aggiunte riuscite da /upload a LogDO; la risposta contiene la tupla di ricevuta. Non universale.
T3 Permanenza Per-qub su Arweave Una singola transazione Arweave per il qub. Attualmente preparata per ogni pubblicazione accettata e inviata in modo asincrono; la transazione firmata esatta rimane nella casella in uscita drenabile fino alla consegna.

I livelli descrivono proprietà distinte di evidenza e durabilità, non il piano commerciale attuale. Il codice attuale programma comunque una transazione Arweave individuale per ogni pubblicazione accettata; non espone T3 solo come un upsell a pagamento. I limiti di quota tramite chiave API/account rimangono controlli separati dell'applicazione.

Onestà della durabilità. La scrittura T1 è sincrona, quindi una risposta riuscita stabilisce la durabilità a livello applicativo senza aspettare un gateway Arweave. Non stabilisce di per sé un timestamp indipendente. Una transazione individuale confermata fornisce il suo limite superiore di tempo blocco. Per una risposta contenente il tuple completo della ricevuta T2, il prossimo anchor confermato può fornire la prova del log descritta sotto. Se il tuple è assente, nessuna superficie può implicare che questo qub sia già nel registro di trasparenza. La latenza dell'anchor e della pubblicazione non ha SLA numerico a livello di protocollo.

16.2 Struttura LogLeaf (due forme impegnate)

Una voce di log è un LogLeaf, codificata come CBOR canonico scritto a mano sotto il profilo §3.1 (lunghezza definita, senza tag, senza float, interi nella forma più corta, testo NFC, campi opzionali omessi quando assenti, chiavi ordinate per lunghezza dei byte codificati in ascendente e poi in ordine di byte). La guardia canonica §3.1 parse → re-encode → compare è applicata sul percorso di codifica prima dell'hash (non solo sulla decodifica), quindi due implementazioni non possono discordare sui byte della foglia a causa di differenze di larghezza interi o 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 portato come bstr[32], mai una stringa di testo base64url (corrisponde a §3.3).

La foglia ha due forme selezionate da un byte kind, perché il percorso di caricamento generale è cieco ai byte: POST /api/v1/upload tratta deliberatamente entrambe le forme di payload accettate come opache e riceve qub_id e unlock_at solo come asserzioni cliente non fidate. Sul percorso privato predefinito, body_hash, drand_round, created_at e drand_chain_version sono ulteriormente nascosti all'interno del wrapper esterno §13, la cui chiave il Worker non possiede mai. Il sistema di tipi definisce anche una forma attestata per un produttore che deriva body_hash / drand_round da sé. L'attuale percorso /seal ha quei valori ma non chiama LogDO, quindi la produzione emette attualmente solo foglie asserite (0x02) da appendici di caricamento generale riuscite. La divisione mantiene ogni valore impegnato onesto senza fingere che il produttore attestato sia cablato:

Chiave Lunghezza enc. Tipo Presenza Significato
seq 4 u64 richiesto Indice foglia globale basato su 0; la posizione a cui la prova di inclusione si riferisce.
kind 5 u8 richiesto 0x01 abilitato a certificare (definito, non attualmente emesso) o 0x02 affermato (sigillo del cliente / caricamento byte-cieco).
ref 4 bstr[32] richiesto ID di riferimento della foglia. Attestato → grezzo qub_id. Affermato → l'ID offuscato SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1).
chash 6 bstr[32] richiesto Indirizzo del contenuto SHA3-256(stored_bytes) — l'unico collegamento di contenuto che il Lavoratore può sempre calcolare onestamente, su entrambi i percorsi.
unlock_at 10 i64 richiesto Copiato (attestato) o dichiarato (dichiarato); verificato > 0 prima che entri nella foglia.
received_at 12 i64 richiesto Orologio a parete del lavoratore su R2-ack. Non probatorio (asserito dall'operatore; §16.6). Presente per autodescrizione, mai una prova. Validato > 0.
body_hash 10 bstr[32] Solo kind=0x01 Omesso su 0x02 — il Lavoratore non lo possiede ai sensi del §13.
drand_round 12 u64 Solo kind=0x01 Omissi su 0x02.

Una foglia kind=0x02 non compromette deliberatamente né body_hash né drand_round: attesta l'impegno e l'ordinamento di un testo cifrato opaco all'indirizzo di contenuto chash, rivendicando qub_id e unlock_at — non il suo testo in chiaro o il suo round. Le gambe in chiaro/rotonde per un qub affermato provengono dalla verifica esistente del fibrato .qub §11, non dal log (§16.11). drand_chain_version non è nella foglia (è all'interno dell'involucro sul percorso predefinito); la granularità della catena risiede sull'ancora (§16.7). Disciplina codificatore: rifiuta un ref o chash tutto zero, e rifiuta unlock_at non positiva / received_at, rispecchiando la guardia sentinella outcome_at > 0 in cbor.rs.

16.2.1 Accecamento privato con qub

Il log non deve diventare l'oracolo di enumerazione che il wrapper esterno §13 esiste per impedire (§13.1). Per un qub privato (wrapped) la foglia asserted commette il SHA3-256(qub_id ‖ log_blind_secret) identificativo blinded, dove log_blind_secret è un segreto detenuto dal server e omette body_hash. Una terza parte non può collegare tale foglia a un qub_id specifico; 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 Visibility: public Arweave secondo §13.8) commit il qub_id grezzo. Questo è l'unico punto in cui la verificabilità autonoma cede deliberatamente a un invariante della privacy portante; il legame autonomo per i qubs privati è chash (§16.9).

log_blind_secret custodia (risolta — §16.15 Q4). Il blind protegge la non collegabilità della foglia, non la riservatezza del testo in chiaro (il wrapper §13 vale independevolmente). In caso di compromesso log_blind_secret, per ogni qub_id l'avversario già detiene o può ricostruire (ogni qub il cui bundle/URL possiede, più qualsiasi qub_id a bassa entropia o pubblico) ricalcola il ref foglia in un hash e lo collega — questo è un collegamento diretto di una popolazione nota, non una forza bruta su uno spazio sconosciuto. Classifica log_blind_secret come segreto di correlazione/Sybil-grade nello stesso livello di custodia degli altri segreti server, e ruota solo in avanti (una rotazione riacceca le future leave; non può discollegare retroattivamente quelle già ancorate).

16.3 Hashing di foglie e nodi

RFC 6962 §2.1 hashing separato dal dominio 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("")          // defined but never anchored

I byte prefissi di dominio 0x02 (catena di ingresso, §16.4) e 0x03 (hash STH, §16.6) sono riservati e disgiunti da questi. Sono byte singoli e quindi non possono collizionare con i separatori di dominio ASCII esistenti da 10 byte (QUB_ID_V2, ecc.). L'albero è l'albero RFC 6962 sinistra-piena sbilanciata (ogni split interno al massimo potenza di due strettamente inferiore al conteggio delle foglie del sottoalbero), che permette a dimostrazioni di inclusione e consistenza di condividere un algoritmo di percorso di audit. La specifica di riferimento trasporta pseudocodice esplicito di derivazione sinistra/destra e fissa un vettore di test non-power-of-two (5-foglia) per esercitare il caso di promozione del bordo destro — che un vettore a 4 foglie nasconde —

16.4 Hash Chaining (interno)

Il LogDO mantiene una catena di ingresso interna solo per la coerenza dei crash. Non è mai pubblicato e mai rivolto a verificatori:

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

L'autorità pubblicata solo appendici è la radice di Merkle cumulativa + il suo ancora (§16.5–16.6), mai l'ordine grezzo in cui l'operatore serve lascia: la catena ricalcola per qualsiasi ordine servito, quindi solo la radice ancorata è canonica in posizione.

16.5 Albero Merkle cumulativo e lottamento

Esiste un albero RFC 6962 in continua crescita su tutte le foglie in seq ordine — non isolato per batch. (Una costruzione carry-leaf-chained per batch è stata respinta: non è una vera relazione prefisso, quindi le sue "dimostrazioni di coerenza" non sono valide.) L'albero cumulativo fornisce vere dimostrazioni di consistenza RFC 9162 e lascia che un singolo ancoraggio recente dimostri l'inclusione per qualsiasi qub più vecchio.

L'LogDO Durable Object è il singolo scrittore (blockConcurrencyWhile, mirroring QuotaDO / EntitlementDO) — aggiungendo a un log condiviso c'è lettura-modifica-scrittura nello stato condiviso e quindi DEVE passare attraverso un DO, mai KV. Memorizza nella cache la frontiera del bordo destro dell'albero (O(log n) hash), quindi chiudere un batch è O(batch). Un batch è l'insieme di foglie ancorate insieme; i suoi trigger implementati sono un avanzo tree_size di almeno LOG_BATCH_MAX_LEAVES (default 4096), età che raggiunge la cadenza dell'ancoraggio, o una chiusura forzata esplicita amministrativa/cron. root_i è l'accumulo dell'Hash dell'Albero di Merkle su foglie 0 .. tree_size_i.

16.6 Segnalato Tree Head tramite Arweave Anchor

La transazione dell'ancoraggio Arweave è la Testa dell'Albero Segnato e sostituisce una firma operatore per la testa dell'albero stessa: l'ancora giornaliera non necessita di chiave qub perché la owner di Arweave è la firma. La tesi del fossato vale — il substrato immutabile, non un segreto custodito da qub, è portante per la radice ancorata.

Il design del log richiede una chiave calda di aggiunta successa ricevutezza (§16.10), fissata in LogProfile e firmata incrociato da anchor_owner. L'implementazione attuale non ha completato quella provisioning trust-root: RECEIPT_SK è opzionale, una chiave assente/invalida produce sig_b64url: "" e la LogProfile.receipt_pubkey compilata è vuota. Una tale ricevuta può descrivere la foglia aggiunta ma non una ricevuta firmata non ripudiabile. La rivendicazione di design più forte si applica solo dopo che un rilascio del verificatore fissa la chiave pubblica corrispondente e il proprietario dell'ancora la firma incrociata. Una risposta alla pubblicazione senza la tupla completa di ricevuta non fa alcuna richiesta di accettazione log; una con firma vuota fa una richiesta append-position ma nessuna richiesta di verifica della firma.

Il SignedTreeHead è il CBOR canonico (chiavi per lunghezza codificata): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (prior sth_hash; genesis = 32 zero byte), log_id:bstr[32], first_seq:u64, anchored_at:i64. Il suo hash è sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).

Root di fiducia fissata. 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) è integrata nel qub_core come LogProfile — insieme alle costanti quicknet già presenti in DrandTimelockProvider::quicknet() — e distribuita con il binario del verificatore. Il verificatore DEVE anche verificare il data → tx_id binding localmente di Arweave tx invece di fidarsi di una risposta /raw/ gateway. Questo chiude la falla di ambiguità del portafoglio rogue: "ancorato su Arweave" non ha significato finché il verificatore non pina quale wallet.

La rotazione è un'estensione di governance §15, non un riutilizzo (risolto — §16.15 Q3). La superficie profilo di §15.2 attualmente enumera solo sig_algs / chain drand / versioni di wrapper / tipi di contenuto, e i trigger di §15.3 non elencano nessuno di questi — LogProfile / anchor_owner non è ancora nella superficie di §15. La governance della rotazione deve quindi essere costruita: §15.3 viene estesa (sotto) per aggiungere il trigger LogProfile, e una rotazione è un bump LogProfile firmato inviato in un aggiornamento del verificatore. Una rotazione pianificata porta una firma incrociatato in uscita → in arrivo; Una rotazione guidata dal compromesso non può (la chiave uscente è non affidabile/non disponibile proprio in quel momento) e ricade sul bump governato dalla §15, con il controllo della fork prev-anchor (sotto) che limita danni 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 da Arweave. La finestra è received_at → anchor confirmation (cadenza + finalità di Arweave, senza garanzia di latenza del protocollo). Prima della fornitura della radice di fiducia, l'implementazione attuale fornisce l'integrità operativa di qub più eventuali metadati di append non firmati presenti; non fornisce la garanzia di non ripudio pianificata. Tre artefatti di responsabilità definiscono il design completato (il modello dei testimoni è la risoluzione §16.15 Q2):

  1. Sigillo di ricevuta (dipendente dal provisioning) — l'analogo SCT restituito quando l'appendenza del registro di un upload ha successo (§16.10). Diventa non ripudiabile solo quando sig_b64url non è vuoto e la relazione corrispondente tra chiave pubblica e proprietario dell'ancora è fissata nel verificatore. Il pin del profilo di produzione attualmente vuoto non può supportare tale verdetto. Questo controllo non si applica a una tupla di ricevuta omessa o a una ricevuta non firmata.
  2. Metodologia del monitor pubblicata + camminata prev-chain — l'ancoraggio prev catena è head→genesis camminata; una bifurcazione (due ancoraggi in un size con root diversi, o un prev rotto) è una prova pubblicabile di cattiva condotta. Il rilevamento dell'equivocazione è un impegno operativo dichiarato, non un'assunzione silenziosa.
  3. Doppie teste auto-pubblicate — ogni nuova {sth_hash, tree_size} di head viene pubblicata su un repository GitHub pubblico e solo append-own di proprietà qub (la fase auto-pubblicazione a prova di manomissione e a prova di manomissione), con un post social come conferma solo per il miglior sforzo. Una pagina MUST per pubblicazione fallita (non fail silent). Implementata (Fase 8) come hook publishHead sull'ancoraggio cron (workers/api/src/utils/heads-publish.ts): una PUT all'API dei contenuti senza sha è solo append-up (una 422 significa che la testa è già pubblicata, mai una sovrascrittura); opt-in / deploy-gated su PUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO} e inerte fino a quando il repository non viene provisionato. Un fallimento hard su GitHub invia pagine tramite il canale health_alert e fa sbattere una metrica di fallimento duratura (m:tlog_publish_head_fail); l'ancora Arweave stesso non torna mai indietro su un fallimento di pubblicazione. "Non fallimento silenzioso" è garantito da quella metrica duratura — su cui le operazioni DEVONO avvisare dashboard — anche se la pagina email del miglior sforzo non può essere consegnata. Due limitazioni oneste derivano dall'"anchor-on-advance" (il cron pubblica solo quando la dimensione aumenta): un fallimento transitorio di GitHub lascia un vuoto nella sequenza delle teste pubblicate per quella dimensione — limitato, non silenzioso (it pages), e poiché ogni testa commit un albero superset, una prova di coerenza §16.9 colma il divario; fondamentalemente, quella dimostrazione di coerenza viene calcolata dall'albero autorevole ancorato all'Arweave, non dalla superficie di GitHub, quindi un gap in GitHub non indebolisce mai la verificabilità. Un backfill di recupero che colma le lacune di head-head pubblicate è un miglioramento differito.

Onestà vincolata (vincolo vincolante). Poiché qub controlla entrambe le superfici di pubblicazione pianificate, questo è autopubblicato, non testimoniato indipendentemente. Nessun prodotto, marketing o superficie legale può affermare che il registro sia "testimoniato indipendentemente". Dopo che le porte di ricevuta/profilo/testa sono state predisposte, l'affermazione consentita è che l'equivocazione è rilevabile e un appendice firmata con successo lascia una ricevuta non ripudiabile. Prima di allora, tale affermazione non è disponibile. Un vero testimone indipendente di terze parti è rimandato a un futuro aggiornamento di governance §15.

received_at è dichiarato dall'operatore e nessuna affermazione può basarsi su di esso — non viene mai mostrato come prova o come conferma di disputa su qualsiasi superficie prodotto / legale / API / rendering della prova. Il tempo del blocco di ancoraggio Arweave T è l'unico timestamp senza fiducia (un limite superiore su "registrato da"). Qualsiasi controllo di sanità del monitor su received_at DEVE confrontare con T, non con il campo STH controllato dall'operatore anchored_at; tale controllo è solo una guardia contro un errore dell'orologio di un operatore onesto, non un controllo di responsabilità contro un operatore maligno (§16.15 Q5).

16.7 Formato e Cadenza della Transazione di Ancoraggio

Lo AnchorBundle è il corpo canonico-CBOR della transazione Arweave, scritto tramite il bundler §16.8: ver:u8, sth:bstr (byte canonici SignedTreeHead), prev_anchor:bstr (id tx di ancoraggio precedente in byte grezzi; omesso al genesis), chain_hash:tstr (la catena drand in vigore — quicknet), e lo stream-CBOR delle foglie del batch in ordine seq in modo che l'ancora sia autosufficiente: un monitor ricalcola root dal corpo senza dipendenza da qub. (Se lo stream delle foglie diventa grande ad alto volume, una futura revisione potrebbe impegnarsi solo su un intervallo di foglie per riferimento; annotato, non adottato in v1.)

I tag Arweave sono intenzionalmente enumerabili — il registro è destinato a 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 affidabili; il corpo CBOR è l'unica autorità.

Cadence: giornaliero di default, rivisitato con il volume (il trigger di dimensione accorcia automaticamente la cadenza effettiva sotto carico). L'attuale produttore non implementa un gancio a pagamento per sigillare forzato. Il portafoglio ancora è dedicato e a bassa velocità, separato dal portafoglio upload — DEVE essere un JWK (una chiave distinta, non un ruolo logico sul portafoglio di upload) quindi un compromesso tra portafoglio upload non può falsificare ancoraggi — con un budget giornaliero rigido per transazioni ancora. La posizione di custodia è chiara: un tasto rapido a scopo ristretto con interruttore elettrico stretto e saldo basso, non "freddo" — un wallet che firma automaticamente ogni giorno non può essere freddo, e la specifica non pretende il contrario.

16.8 ANS-104 Bundler

Un codificatore interno ANS-104 DataItem e deep-hash signer, circa 300 righe, solo Web Crypto, zero dipendenze npm (entrambi gli SDK Turbo falliscono il gate npm ci --ignore-scripts della supply chain). Layout dei byte 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 è Arweave deepHash — un digest ricorsivo SHA-384 (requisito wire di Arweave, crypto.subtle.digest("SHA-384")) su ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — poi RSA-PSS su deep hash con il wallet JWK tramite crypto.subtle; id = base64url(SHA-256(signature)). Qui SHA-384 è quarantenato come primitivo solo per fili Arweave, mai primitivo qub trust (§15 registra la recinzione; l'hashing qub trust è SHA3-256 per tutto il tempo).

Il percorso del codice ANS-104 serve la macchina di fallback/drenaggio differito e scrive AnchorBundle DataItems. Il percorso di pubblicazione ordinario crea prima una transazione Arweave firmata esatta e mantiene il JSON in una casella di uscita durevole; il posting diretto è un'ottimizzazione della latenza, e il percorso di drenaggio riprova la stessa transazione prima di applicare il suo fallback bundler. Schema signature (risolto — §16.15 Q8): v1 firma con RSA-PSS (tipo firma 1) riutilizzando il meccanismo JWK esistente del wallet Arweave (zero nuova custodia di chiavi di lunga durata, al servizio della tesi "un segreto in meno"); Ed25519 viene rimandato al percorso di migrazione PQ §15.

L'hash profondo rollato manualmente è il codice a rischio più alto e a copertura naturale più bassa in W5, quindi il suo gating è non negoziabile (§16.15 Q8):

  1. Il fixture cross-lingua tlog_v1.json (Rust + TS, il modello §14.5 wrapper_v1.json) copre deep-hash, byte + id di DataItem, hash delle foglie, una radice a 5 foglie + percorso di audit, un hash STH, una prova di inclusione e una prova di consistenza — in entrambe le direzioni firma e verifica (la direzione di verifica è importante perché il controllo locale tx → tx_id di §16.6 porta il deep hash in ogni verificatore autonomo, non solo nello scrittore).
  2. Un round-trip di interoperabilità una-tantum attraverso un bundler di riferimento ANS-104, consumato solo come dati di test statici — mai una dipendenza runtime npm (la postura Web-Crypto-only / senza script di installazione resta).
  3. Il percorso deep-hash + RSA-PSS deve fare il round-trip attraverso le stesse primitive crypto.subtle utilizzate in produzione, quindi l'encoder interno è compatibile a livello di byte.
  4. Un monitor di accettazione post-bundle continuo conferma che ogni DataItem di ancoraggio / fallback ottiene effettivamente l'accettazione Arweave, con allarme + interruttore di sicurezza — perché il deep hash serve anche alla coda di fallback per indisponibilità di Arweave, quindi una regressione silenziosa riempirebbe quella coda con elementi rifiutati dalla rete durante esattamente il blackout che serve a coprire.

16.9 Prove di Inclusione e Consistenza

Entrambe sono RFC 9162, SHA3-256, servite come CBOR canonico.

InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (il CBOR esatto della foglia — il verificatore ricalcola leaf_hash da solo e non si fida di un hash fornito), index:u64, size:u64, audit:[bstr[32]], root:bstr[32], anchor:{ txid:bstr[32], batch:u64, sth:bstr[32], log_id:bstr[32], block_height?:u64, anchored_at?:i64 }.

ConsistencyProof — GET /api/v1/log/consistency?first=<size_a>&second=<size_b>: ver:u8, first_size:u64, second_size:u64, first_root:bstr[32], second_root:bstr[32], nodes:[bstr[32]], first_anchor, second_anchor. Una lista di chiavi univoca, fissata dal vettore di test.

Verifica autonoma (nessun server qub, estende §11):

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

L'archiviazione proof-serving DEVE essere codificata a chiave di coordinate (risolta — §16.15 Q7, precondizione di blocco). La generazione di proof a freddo è neutra rispetto alla correttezza solo se il materiale di audit R2 è un persistente nodo di Merkle keyed tramite (level, index) di coordinate assolute a albero — non per batch delta di nodo. Con uno store a chiave di coordinate, qualsiasi percorso di audit (leaf i, size N) è un insieme di O(log N) GET R2 diretti senza nessun calcolo tra confini batch; con uno store batch-key non lo è, che è il divario di storage layout che questa risoluzione colma. Anche i corpi foglia sono indirizzabili per il contenuto da seq. Un vettore di test W5 DEVE dimostrare una leaf fredda dell'era genesi contro una radice molto più avanzata usando solo R2 + Arweave con lo storage LogDO cancellato, quindi la pretesa di sicurezza di recupero in §16.13 è supportata piuttosto che affermata. I O(log N) GET R2 sequenziali appartengono solo all'endpoint di prova asincrona — mai sul percorso caldo del sigillo (§16.10) o su un cron per tick.

16.10 R2 - Ordine del primo ack

La sequenza POST /api/v1/upload implementata è:

  1. Porte della metà anteriore (autenticazione, validazione, chiave shard di idempotenza) — invariate.
  2. Creare, etichettare e firmare la singola transazione Arweave esatta. Questo deriva tx_id localmente, anche se la creazione di transazioni può ottenere metadati di ricompensa/ancora da un gateway. Un fallimento di preparazione fallisce comunque la richiesta prima della conferma.
  3. Sincronamente scrivi l'artefatto selezionato al qub-cache/<tx_id> e persistono i record stabili di creazione-operazione/outbox. Questi sono il livello di durata e di ritento; i fallimenti prima del liquido ritornano 503.
  4. Quando LOG_DO è configurato, tentativo sincrono LogDO.append(leaf). Il singolo scrittore assegna seq, estende la catena di ingresso e aggiorna la frontiera. L'append RPC fa solo questo; la chiusura batch esce fuori dal percorso sull'allarme. Un errore di trasporto append/applicazione è attualmente fail-soft: la risposta può ancora avere successo senza log_seq, receipt o anchor_status. Nonostante un commento sull'implementazione, oggi non è cablato alcun riconcilio automatico dei log successivo.
  5. Restituire la conferma. Includere { log_seq, anchor_status: "pending", receipt } solo quando l'append ha restituito la completa tupla di successo. receipt.sig_b64url è vuota quando il firmatario della ricevuta non è disponibile; i clienti NON DEVONO chiamare quel valore firmato o non ripudiabile. L'assenza della tupla significa solo pubblicazione duratura, non accettazione del registro di trasparenza.
  6. Utilizzare un compito differito per pubblicare la transazione firmata esatta. Il successo rimuove la casella di uscita; il fallimento lascia il Dren Cron limitato e non deve modificare il tx_id già riconosciato. I metadati provvisori e altri sidecar di miglior sforzo sono anch'essi differiti.

Confine di latenza. Il percorso della richiesta include lavoro di autorità/quota nella prima metà, preparazione/firma delle transazioni, scritture R2 durature e (quando configurato) il tentativo di LogDO. < 300 ms appare nella revisione di progettazione come obiettivo operativo, non come garanzia di protocollo; il passaggio attuale di preparazione della transazione può eseguire una richiesta di metadati gateway. Gli allarmi di latenza e i cancelli di lancio sono controlli operativi, non prove disponibili per un verificatore.

16.11 Modello di Fiducia — la rivendicazione precisa, definita per tipo di foglia

Per kind=0x01 (attestato): *"Questo contenuto — corrispondenza del corpo body_hash, identificato da qub_id — è stato messo nel registro solo appendici di qub alla posizione seq ed è esistito non oltre il tempo di blocco di Arweave T; era crittograficamente illeggibile fino al round di drand R = unlock_round(unlock_at)." * Questo è il triplo {tlock round binding + Merkle inclusion + anchored root} completo.

Per kind=0x02 (affermato, il predefinito): *"Un testo cifrato opaco con indirizzo di contenuto chash, che rivendica qub_id e unlock_at, è stato inviato nel log solo appendibile alla posizione seq ed esisteva non oltre il tempo di blocco di Arweave T." * Le gambe round e body sono fornite dalla verifica esistente del fascio .qub §11 (qub_core::unlock), non dal log; ciò che il log aggiunge su una transazione per qub semplice è un ordine evidente per manomissione, un tempo di impegno di limite superiore senza fiducia e resistenza all'equivocazione.

Entrambe le affermazioni escludono, secondo il §11: paternità senza sig_alg ≥ 0x01, intento e tempistica di sotto-ancoraggio. Nessuna delle due permette a nessuna affermazione di appoggiarsi a received_at.

Limite di richiesta (vincolo di lancio vincolante — risolto §16.15 Q1). Per una pagina (kind=0x02) affermata, la rivendicazione con ambito sopra è il tetto su ciò che qualsiasi prodotto, marketing, termini o superficie di rendering delle prove può affermare. Nessuna superficie può affermare o implicare che il log dimostri il contenuto o il round di sblocco di un upload a byte-blind — il log dimostra l'ordine + un tempo di impegno di un testo cifrato opaco senza fidanza. Contenuto e round proof provengono esclusivamente dalla verifica esistente del .qub-bundle §11, che è indipendente dal log. Una pubblicazione senza aggiunta o ricezione riuscita non ha alcuna rivendicazione log.

16.12 Versioning e coordinamento W3

Non c'è nessun bump SealedQub filo e quindi nessun bump nella versione del protocollo (§12.2): il log è un sidecar che effettua commit ai campi e byte esistenti, quindi non entra nella cronologia delle versioni del protocollo §12.3. La drand_chain_version opzionale di W3 rimane intatta 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 — che rispecchiano l'indipendenza dalla versione del wrapper da §12.5 (lo wrapper trasporta un byte di versione indipendente dalla versione del protocollo, e le versioni del log seguono la stessa separazione).

La consegna della prova viene recuperata di default, con un ride-along opzionale. Una prova non può esistere al momento del sigillo (l'ancora non è ancora stata scritta), quindi il bundle del .qub del sigillo rimane privo di proof. Il verificatore di W7 recupera GET …/proof una volta, oppure in modalità completamente offline ricostruisce la prova dal 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 riesportazione post-ancoraggio per l'archiviazione a freddo — seguendo lo stesso schema "opzionale, omesso di default, additivo" del drand_chain_version di W3.

16.13 Mantenimento

Le finestre di mantenimento per la coda aperta LogDO, il substrato di servizio di prova R2, i contatori di interruttori di ancoraggio e la coda di fallback del bundler sono specificate in docs/DATA-RETENTION.md. Principio: lo storage hot per entry del log (LogDO) è recuperabile dopo l'ancoraggio; il suo materiale di audit — il database (level, index) nodo Merkle con chiave coordinata + i corpi foglia indirizzati seq (§16.9) + gli ancoraggi Arweave — è permanente. Il recupero di una foglia fredda dal DO non invalida mai una prova emessa in corso, perché una prova si risolve contro quel nodo permanente R2 e l'ancora Arweave, non contro il DO (e il vettore di test DO cancellato §16.9 lo dimostra).

16.14 Vettori di Test

W5 include il tlog_v1.json fixture cross-language (§16.8) più vettori lavorati: un → leaf_hash foglia kind=0x01 e uno kind=0x02; la radice cumulativa a 5 foglie; una dimostrazione di inclusione; una prova di coerenza; una AnchorBundle; e un ID DataItem. Questi convivono accanto ai vettori esterni del §14.5 e sono esercitati sia dalle implementazioni Rust (qub-core) che TypeScript (Worker).

16.15 Decisioni di revisione (W5 — risolto)

La revisione esterna del W5 (un passaggio di progettazione avversario + approvazione del proprietario) è completa. Ogni decisione qui sotto è decisa e riflessa nel testo §16 sopra; i vincoli di lancio sono riproposti alla fine. L'implementazione può procedere sotto di essi.

  1. Onestà foglia del percorso predefinito (kind=0x02) — RISOLTO. Inviare la divisione a due foglie come specificato: kind=0x02 non compromette né body_hash né drand_round. Nessun campo *_body_hash sul percorso byte-blind (sarebbe il segnale falso "verificato" più leggibile per gli integratori ed è una comodità che §11 già fornisce dal bundle). Non richiedono sigillo server per i qubs attestati nel log (che forzerebbe il testo in chiaro attraverso il Worker e distruggerebbe il fossato di crypto-shredding). Qualsiasi cortocircuito auto-descrittivo appartiene al bundle .qub / inviluppo di prova come campo ricalcolato dal verificatore, mai come campo foglia. Tetto di richiesta confermato dal proprietario: §16.11.

  2. Responsabilità per equivocazione / omissione — PROGETTAZIONE RISOLTA, PROVISIONING INCOMPLETO. Il design richiede che la chiave di ricevuta del sigillo venga fissata in LogProfile e firmata incrociata da anchor_owner, oltre alla metodologia del monitor, al percorso prev-chain e a due testi auto-pubblicati. Il profilo compilato e gli hook di distribuzione sono ancora segnaposto/opzionali come dettagliato nel §16.6, quindi la dichiarazione più forte rilevabile + ricevuto non è attuale fino alla chiusura di quei portal. Non deve mai essere commercializzato come testimoniato indipendentemente. Un vero testimone di terze parti è rinviato a un aumento di governance secondo il §15.

  3. Root fiduciaria ancora-proprietario fissata + rotazione — RISOLTO. Adottare il pin LogProfile (§16.6); il verificatore controlla anchor_tx.owner == anchor_owner e verifica localmente i dati TX → tx_id legando. La governance della rotazione è una §15 estensione della build (§15.3 trigger aggiunto), non un riutilizzo; rotazioni pianificate segnano incrociato e le rotazioni guidate dal compromesso tornano al bump §15 con il danno delimitato dal controllo fork.

  4. Accecamento fogliare qub privato — RISOLTO. Continua accecando per qub privati (ref = SHA3-256(qub_id ‖ log_blind_secret)), qub_id grezzo per qubs pubblici (già §16.2.1), chash come pareggio autonomo. log_blind_secret è un segreto di grado di correlazione/Sybil, solo per rotazione in avanti (§16.2.1).

  5. received_at — RISOLTO. Mantenerlo nella foglia, compromettenuto ma esplicitamente non probatorio; mai ricomparso come prova o come contestazione su nessuna superficie. Qualsiasi controllo di sanità mentale del monitor si confronta con il T di tempo di blocco di Arweave, non con il anchored_at controllato dall'operatore (§16.6).

  6. Tempistica dimostrabile a livelli — RISOLUZIONE DI PROGETTO, NON ROUTING CORRENTE. Il progetto revisionato assegna il timing del blocco di ancoraggio al tier batched e la prova a ore esatte al T3 pagato, senza alcun SLA numerico per il primo. Le rotte attuali non hanno fatto questa distinzione commerciale: programmano una singola transazione per ogni pubblicazione accettata, e la copertura dei log rimane condizionata come indicato nelle §16.1/§16.10. Il testo del prodotto deve descrivere l'implementazione, non questa divisione di tier futuro.

  7. Albero cumulativo sui Workers — RISOLTO. Singolo albero cumulativo RFC 9162 + LogDO a scrittura singola con frontiera memorizzata nella cache (margine confortevole rispetto al plafonamento DO di ~1k scritture/sec; differire lo sharding Merkle-of-shard-roots fino a vicino a tale soglia). Lo store per nodi R2 (level, index) con chiave di coordinamento + vettore di test a foglia fredda DO cancellato è implementato (§16.9). < 300 ms rimane un obiettivo di progettazione/operativo, non una promessa di protocollo (§16.10).

  8. Schema di firma ANS-104 + deep-hash — RISOLTO. RSA-PSS (tipo di firma 1, riutilizzando il JWK dell'anchor-wallet dedicato); Ed25519 differito al percorso PQ del §15. L'hash profondo SHA-384 fatto a mano è vincolato al fixture cross-impl bidirezionale, a un controllo di interoperabilità reference-bundler solo statico, al round-trip condiviso crypto.subtle e al monitor di accettazione Arweave post-bundle (§16.8).

Vincolanti vincoli di lancio (portano all'implementazione + revisione prodotto/legale):


17. Bundle di verifica portatile (.qub)

Stato. Questa sezione è implementata (W7 / UP-C2): qub_core::export produce e analizza il bundle e tools/qub-verify è una CLI pubblica e autonoma che lo verifica offline. I §11 e §16.9 fanno già riferimento al «bundle .qub» come unità consumata da un verificatore autonomo; questa sezione ne specifica i byte e la procedura di verifica. È strettamente additiva: il bundle racchiude gli input esistenti del §11 e non modifica alcun formato wire on-chain.

17.1 Scopo

Il §11 stabilisce che qualsiasi terza parte può verificare l'artefatto crittografico di un qub senza la cooperazione di qub. Il bundle .qub rende tale verifica portatile e offline: racchiude il CBOR sigillato e la firma del round drand che lo sblocca in un unico artefatto autonomo, così un destinatario può verificare l'integrità del contenuto, il binding al round e qualsiasi firma di autorialità senza alcuna chiamata di rete (nessun recupero dall'archivio, nessuna richiesta drand live, nessuna API qub). Un bundle da solo non dimostra quando è stato creato il suo ciphertext; una transazione di archiviazione verificata indipendentemente o una prova del log ancorata fornisce quella distinta affermazione sul tempo di esistenza (§11, §17.5).

17.2 Formato del bundle

Un QubBundle è CBOR canonico scritto a mano secondo il profilo §3.1 (lunghezza definita, nessun tag, nessun float, interi nella forma più breve, testo NFC, campi opzionali omessi quando assenti, chiavi ordinate prima per lunghezza dei byte codificati crescente e poi byte per byte). Le tre chiavi da 15 caratteri sono ordinate d < i < s. Un file .qub grezzo è costituito esattamente da questi byte; per il trasporto via URL o copia-incolla, gli stessi byte sono codificati base64url(senza padding).

Chiave Lung. cod. Tipo Presenza Significato
version 8 u8 obbligatorio Versione del formato del bundle (0x01).
sealed_at 10 i64 facoltativo Tempo di sigillo dichiarato dal creatore (secondi Unix); autodescrittivo, non probatorio.
drand_round 12 u64 obbligatorio Il round al quale è bloccato il qub. Proiezione del qub sigillato incorporato.
arweave_tx_id 14 tstr obbligatorio L'id della transazione sotto il quale sono stati memorizzati i byte sigillati (puntatore di provenienza).
drand_chain_id 15 tstr obbligatorio La catena drand (hex). Proiezione del qub sigillato incorporato.
drand_signature 16 bstr obbligatorio La firma del beacon drand per drand_round: il valore che sblocca il ciphertext.
inclusion_proof 16 bstr facoltativo La prova di inclusione Merkle del log di trasparenza del §16, quando il log è disponibile (§17.5).
sealed_qub_cbor 16 bstr obbligatorio I byte interni SealedQubCbor (dopo la rimozione del wrapper del §13), ossia l'input di verifica del §11.

drand_round e drand_chain_id sono proiezioni di utilità di sealed_qub_cbor, trasportate affinché gli strumenti possano leggerle senza analizzare il CBOR interno. Sono derivate durante la costruzione e ricontrollate in decodifica rispetto al qub sigillato analizzato; un bundle il cui campo di primo livello non concorda con il payload viene rifiutato. La disciplina dell'encoder rispecchia il resto del formato wire: rifiutare drand_signature o arweave_tx_id vuoti e limitare ogni campo a lunghezza variabile.

17.3 Cosa dimostra la firma drand incorporata

Il bundle trasporta la firma drand invece di richiedere al verificatore di recuperarla. La decifratura timelock (tlock sulla catena drand, §8) può riuscire soltanto con la firma beacon autentica del round vincolato: un valore che la catena pubblica solo dopo il trascorrere di quel round e che è una firma BLS valida sotto la chiave pubblica della catena. Una firma contraffatta o errata non supera la verifica BLS o la decifratura IBE/AEAD. Un bundle che si decifra dimostra quindi che il ciphertext è vincolato al round R e che il round R è trascorso. Il verificatore fissa la catena (DrandTimelockProvider::quicknet()) e applica il controllo del binding al round del §11, quindi un bundle non può dichiarare un round al quale il suo ciphertext non è vincolato.

Questa è una prova della condizione di rilascio, non un timestamp di creazione. Dopo che il round R è trascorso, chiunque può creare un nuovo ciphertext per R e racchiudervi la firma ormai pubblica. Il bundle da solo NON DEVE quindi essere descritto come prova che il ciphertext o il contenuto esistessero prima di R, prima di unlock_at o prima di qualsiasi evento.

17.4 Procedura di verifica offline

qub-verify <file.qub> esegue la procedura standard del §11 interamente dal bundle, invocando qub_core::unlock::unlock con un DrandTimelockProvider fissato:

1. Parse the .qub bytes → QubBundle (canonical-CBOR guard; bound every field;
   re-check drand_round / drand_chain_id against the embedded sealed qub).
2. BLS-verify bundle.drand_signature for the pinned chain and round, then
   tlock_decrypt(sealed.tlock_ciphertext, bundle.drand_signature) → QubEnvelope.
3. Verify SHA3-256(body) == body_hash               (§11 step 8).
4. Verify QubEnvelope.qub_id   == SealedQub.qub_id   (§11 step 9).
5. Verify QubEnvelope.unlock_at == SealedQub.unlock_at (§11 step 10).
6. Verify ciphertext round == unlock_round(unlock_at) and the chain binding.
7. If sig_alg != 0x00: verify author_signature (and any cosigner; §9.4).
8. Report integrity, round-elapsed/round-binding, authorship, and cosigner
   verdicts separately, plus the recovered body. Do not report a commitment
   timestamp unless step 9 succeeds.
9. Optional existence-time leg: verify an included §16 proof through its pinned
   anchor, or independently verify the referenced storage transaction. Report
   its block time as an upper bound on ciphertext existence.

La CLI termina con 0 (verificato), 1 (verifica non riuscita: ancora bloccato, body hash non corrispondente, binding round/catena interrotto oppure una firma che non supera la verifica) o 2 (uso errato / bundle malformato). Un report --json presenta gli stessi esiti per l'automazione. Poiché il bundle è autonomo, il crate del verificatore (qub-core) e la CLI (qub-verify) sono gli unici software necessari a una terza parte; entrambi sono pubblici e riutilizzano il percorso di verifica esistente del protocollo, senza crittografia ad hoc.

17.5 Relazione con il log di trasparenza

inclusion_proof è uno slot facoltativo per la prova di inclusione Merkle del §16. La verifica del solo bundle (§17.4) è completa per integrità, binding al round / round trascorso e autorialità facoltativa, ma intenzionalmente non contiene alcuna affermazione sull'esistenza con timestamp indipendente. Un'inclusion_proof popolata e verificata integralmente fino all'ancoraggio aggiunge l'impegno e il tempo limite superiore specifici del tipo di foglia del §16.11 senza modificare la versione del formato del bundle. Una prova assente significa soltanto «nessuna prova inclusa», non «non valido» e non necessariamente «non ancorato».

Nell'implementazione di riferimento lo slot è ora tipizzato: qub_core::export::QubBundle::inclusion_proof_typed() restituisce un Option<InclusionProof> che trasporta la struttura completa del §16.9 (foglia, percorso di audit, radice ancorata e AnchorRef) attraverso lo stesso campo CBOR opaco, senza bump della versione del formato del bundle. La CLI autonoma qub-verify lo consuma tramite il suo ramo --anchor e, finché il wallet di ancoraggio non viene predisposto (§16, Stato), segnala una prova popolata ma con proprietario segnaposto come inclusion-only anziché pienamente anchored-verified.