Especificación del protocolo de qub

qub es un protocolo de compromisos temporales criptográficos: un sistema para sellar palabras hasta una fecha futura y verificar después exactamente qué se selló, qué ronda de drand controló su revelación y —cuando se dispone de una transacción de almacenamiento o una prueba del registro de transparencia— un límite superior con marca temporal independiente del momento en que se consignó el texto cifrado.

Tres primitivas lo hacen posible. drand es una baliza de aleatoriedad descentralizada: la fecha de revelación se hace cumplir criptográficamente, no por la buena voluntad de qub. El almacenamiento duradero junto con un registro de transparencia de solo adición conserva los bytes sellados y ancla compromisos por lotes en un almacenamiento público permanente; la ruta T3 de pago también escribe una transacción individual en el almacenamiento permanente. ML-DSA-65 es una firma digital post-cuántica: cuando se habilita la autoría, el qub queda vinculado a un par de claves cuyo secreto nunca abandona el dispositivo del autor.

Juntas, estas primitivas producen una declaración bloqueada en el tiempo, con manipulación detectable, atribuible de forma opcional y susceptible de recibir una marca temporal independiente: un recibo cuyo valor crece a medida que mejora la capacidad del mundo para fabricar el pasado.

El resto de este documento es la especificación normativa requerida para implementaciones interoperables.


Especificación del protocolo qub

Campo Valor
Versión del documento 1.0.0 (protocol-v1.0.0)
Protocolo de transmisión 0x01
Envoltura externa 0x01
Fecha de entrada en vigor 2026-09-23
Estado Actual
Revisado hasta 2026-09-23

Este documento es la especificación normativa del protocolo para el sistema de compromiso temporal qub. Define las estructuras de datos, las reglas de serialización, las fórmulas de derivación y los procedimientos de verificación requeridos para implementaciones interoperables.

Alcance: la capa del protocolo es intencionalmente neutra respecto al idioma — el cuerpo del qub es plaintext / markdown / bytes de pact opacos, y el renderizado localizado es responsabilidad del visor (aplicación web qub.social, iframe <qub-embed>, clientes MCP, etc.).


1. Notación y convenciones

Notación Significado
u8, u64, i64 Enteros sin signo / con signo del ancho de bits indicado
[u8; N] Arreglo de bytes de longitud fija de N bytes
Vec<u8> Arreglo de bytes de longitud variable
Option<T> Valor de tipo T, o ausente
String Cadena de texto UTF-8, normalizada NFC
`
SHA3-256(x) Hash NIST SHA3-256 de la cadena de bytes x (FIPS 202)
ceil(x) Función techo: el menor entero ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Byte más significativo primero

Todos los enteros en las construcciones de preimagen se codifican como arreglos de bytes big-endian de ancho fijo (i64 → 8 bytes, u8 → 1 byte) salvo que se indique lo contrario.

Todas las marcas de tiempo están en segundos Unix UTC.


2. Estructuras de datos

2.1 ComposeQub (estado en memoria del creador)

No serializado en CBOR. No escrito en el almacenamiento permanente. Local a la aplicación del creador.

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 (carga útil descifrada)

Serializado usando CBOR canónico (§3). Cifrado dentro del SealedQub. Es la estructura que prueba la integridad del contenido tras el descifrado.

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
}

Línea base (qub de texto sin firma): version = 0x01, content_type = 0x01, sig_alg = 0x00; los campos de firma y cofirmante están ausentes. Puede haber otros campos opcionales de metadatos.

Otras configuraciones v1: content_type = 0x03 (cuerpo pact, ver §6.1); sig_alg = 0x01 (ML-DSA-65) con author_signature y author_pubkey presentes (ver §9.3); cosigner_pubkey y cosigner_signature presentes juntos para pacts cofirmados (ver §9.7); reply_to establecido al qub_id del qub padre para qubs de cadena de respuesta (ver §9.3 para las implicaciones del alcance de la firma).

2.3 SealedQub (formato de cable canónico)

Serializado mediante CBOR canónico (§3). Es el artefacto interno de transmisión: la entrega pública almacena estos bytes sin envoltura, mientras que la entrega privada los envuelve en OuterWrapper antes del almacenamiento (§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 (estado de la aplicación visor)

No serializado en CBOR. Local a la aplicación visor. Construido tras un descifrado y verificación exitosos.

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. Perfil CBOR canónico

Toda serialización de SealedQub y QubEnvelope DEBE ajustarse a este perfil. Dos implementaciones, dada la misma estructura lógica, DEBEN producir bytes idénticos.

3.1 Reglas de codificación

Regla Especificación
Estándar RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Orden de claves del map Ordenadas primero por longitud de bytes codificada (las más cortas antes que las más largas), luego lexicográficamente (byte por byte para codificaciones de la misma longitud)
Codificación de enteros Forma más corta: 0–23 en el byte inicial; 24–255 en 2 bytes; 256–65535 en 3 bytes; etc.
Codificación de longitud Solo longitudes definidas. No se permiten arrays, maps, byte strings ni text strings de longitud indefinida (additional info = 31 está prohibido).
Tags Sin tags CBOR (el major type 6 está prohibido).
Punto flotante Sin floats (los valores 0xF9–0xFB del major type 7 están prohibidos).
Text strings Codificados en UTF-8, normalizados NFC (Unicode Normalization Form C).
Byte strings Bytes en bruto. Sin codificación base64 en la capa CBOR.
Claves duplicadas Rechazar con error. Los parsers NO DEBEN aceptar silenciosamente claves duplicadas en el map.
Claves desconocidas Rechazar con error. Los parsers NO DEBEN tolerar claves del map fuera del conjunto canónico de claves del tipo — dos byte strings canónicos distintos nunca deben decodificarse al mismo valor (encode(decode(x)) == x), y en los payloads firmados una clave extra sería contenido oculto al que ambas firmas se comprometen. La evolución del esquema pasa por version, nunca por claves extra.
Valores simples Solo true (0xF5), false (0xF4) y null (0xF6) están permitidos.
Campos opcionales Los campos opcionales ausentes se omiten del map CBOR por completo (no se codifican como null). Los campos opcionales presentes se incluyen en el orden de claves ordenado.

3.2 Órdenes canónicos verificados de claves

Estos órdenes de claves son normativos. Las implementaciones DEBEN emitir las claves exactamente en este orden. Las aserciones de depuración DEBERÍAN verificar el orden en builds que no sean release.

QubEnvelope (versión 0x01, sin firma, todos los campos opcionales ausentes):

"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)

Derivación del orden de claves de QubEnvelope: cada clave es un text string CBOR. Longitud codificada = 1 byte de cabecera + longitud de la cadena (para cadenas de menos de 24 bytes). Ordenar primero por longitud codificada total, luego lexicográficamente para claves de la misma longitud.

SealedQub (versión 0x01, público, sin recipient):

"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 (cuerpo pact, 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 (fila del array terms):

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

PartyIdentifier (map de party_a / party_b):

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

3.3 Referencia de codificación de bytes

Tipo Codificación CBOR Ejemplo
Hash SHA3-256 (32 bytes) 0x58 0x20 + 32 bytes body_hash, qub_id
Marcas de tiempo (i64) Major type 0 (positivo) o 1 (negativo), codificación más corta Segundos Unix
Versión (u8, valor 1) 0x01 (un solo byte)
Tipo de contenido (u8, valor 1) 0x01 (un solo byte)
sig_alg (u8, valor 0) 0x00 (un solo byte)
Firma ML-DSA-65 (3.309 bytes) 0x59 0x0C 0xED + 3.309 bytes author_signature, cosigner_signature
Clave pública ML-DSA-65 (1.952 bytes) 0x59 0x07 0xA0 + 1.952 bytes author_pubkey, cosigner_pubkey

4. Derivaciones normativas

4.1 qub_id

El qub_id identifica de forma única a un qub y vincula el QubEnvelope con el SealedQub. Se deriva de manera determinista a partir del contenido del 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

Codificación del separador de dominio: la cadena "QUB_ID_V2" son 9 bytes ASCII. Se añade un único byte de relleno 0x00 para alcanzar 10 bytes para alineación. Las implementaciones DEBEN usar exactamente estos 10 bytes: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Codificación de outcome_at: una revisión de la implementación anterior a la publicación amplió la preimagen de 92 a 100 bytes para incorporar el campo opcional outcome_at al vínculo. Un outcome_at ausente se codifica como 8 bytes a cero; los validadores del protocolo rechazan outcome_at <= 0 en todas partes, de modo que este centinela no puede colisionar con un valor legítimo. Consulta §3.2 (formato de transmisión) y el documento del árbol tasks/verdict-uplift-plan.md para la mecánica de veredicto que motiva este campo.

Codificación de drand_round: una revisión posterior de la implementación anterior a la publicación amplió la preimagen de 100 a 108 bytes para incorporar drand_round (la ronda de drand objetivo, §4.3) al vínculo y cambió el separador de dominio a QUB_ID_V2. Esto vincula la ronda timelock con la identidad del qub: un gateway no puede volver a vincular el texto cifrado a una ronda distinta (por ejemplo, una ya pasada) de la que implica el unlock_at mostrado. El procedimiento de desbloqueo (§8) verifica además que la ronda incluida en la stanza del texto cifrado tlock coincide con unlock_round(unlock_at), de modo que la hora de desbloqueo mostrada corresponde de forma demostrable a la ronda que controla el descifrado.

Propiedades:

4.2 body_hash

body_hash = SHA3-256(body)

Donde body es la carga útil de contenido Vec<u8> cruda. Para qubs de texto, este es el cuerpo del qub codificado en 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

Donde title es el título opcional en plaintext mostrado en la cuenta atrás del visor antes de la revelación (ver §3.2). La normalización NFC se aplica en el momento del hash de modo que el digest sea estable a través de secuencias de code points visualmente equivalentes. El centinela de ceros se reserva para el caso ausente; una cadena vacía se rechaza en la frontera CBOR canónica como una codificación no canónica de "ausente" (la codificación canónica omite el campo por completo).

4.3 Mapeo de unlock-round

drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
Parámetro Fuente Ejemplo
unlock_at Segundos Unix UTC elegidos por el usuario 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time Información de cadena drand (genesis_time) 1595431050
chain_period_seconds Información de cadena drand (period) 30

Este es el mapeo de referencia de tlock (el CurrentRound de drand). drand publica la ronda N en chain_genesis_time + (N - 1) * chain_period_seconds, por lo que la fórmula selecciona la ronda vigente en unlock_at: la ronda cuya firma es la primera que puede usar un lector que llegue en unlock_at.

Propiedad de alineación (el caso importante en la práctica): cuando (unlock_at - chain_genesis_time) es divisible exactamente por chain_period_seconds, la firma de la ronda elegida se publica exactamente en unlock_at, nunca antes. Esto siempre se cumple en el despliegue de referencia: la hora de génesis de quicknet (1692803367) es divisible por su período de 3 segundos, y las aplicaciones de referencia fijan las horas de desbloqueo a minutos enteros. Con un unlock_at no alineado, la firma de la ronda elegida se publica estrictamente menos de un período antes de unlock_at: la precisión temporal del compromiso es un período de la baliza.

Mapeo anterior a la publicación y tolerancia al desbloquear: el mapeo original era ceil((unlock_at - chain_genesis_time) / chain_period_seconds), que —en el caso alineado anterior— elegía la ronda publicada un período completo antes de unlock_at, haciendo posible descifrar el texto un período antes. Los dos mapeos difieren exactamente en +1 cuando la diferencia es divisible por el período y coinciden en los demás casos. Como drand_round forma parte de la preimagen inmutable de qub_id (§4.1), los artefactos sellados con el mapeo anterior no pueden volver a derivarse; por ello, los verificadores que efectúan la comprobación de ronda del paso 6a de §8 DEBEN aceptar un drand_round almacenado igual a la ronda derivada o a la ronda derivada menos uno (y DEBEN exigir que la ronda de la stanza de tlock sea exactamente la ronda almacenada). La tolerancia adelanta como máximo un período la primera firma válida. El servicio de preparación de pactos aplica la misma tolerancia al volver a derivar el qub_id de un pacto preparado (tanto al prepararlo como al cofirmarlo): si la ronda del mapeo actual no reproduce el qub_id consignado y la diferencia es divisible por el período, vuelve a intentarlo con la ronda anterior y sella el pacto final con la ronda a la que realmente está vinculado el qub_id; nunca usa a ciegas la ronda recalculada, lo que volvería imposible derivar el artefacto.

Validación: unlock_at DEBE estar en el futuro al momento del sellado. unlock_at NO DEBE superar created_at + 10 años (para limitar el riesgo de dependencia drand a largo plazo; la UI DEBERÍA advertir sobre fechas de unlock más allá de 2 años).


5. Newtypes del formato de cable

Los newtypes del formato de cable proporcionan seguridad en tiempo de compilación contra confundir bytes CBOR con JSON, plaintext crudo u otras codificaciones de bytes.

Tipo Contiene Producido por Consumido por
SealedQubCbor CBOR canónico de SealedQub serialize_sealed_qub() Artefacto interno de transmisión; se almacena sin envoltura para la entrega pública o envuelto para la entrega privada, y después lo recupera el visor
QubEnvelopeCbor CBOR canónico de QubEnvelope serialize_qub_envelope() Entrada de cifrado tlock, salida de descifrado tlock

5.1 Reglas de construcción

// 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 Validación en construcción

from_encoded() DEBERÍA validar que la entrada comience con una cabecera de map CBOR válida. La validación estructural completa ocurre en el momento del parseo, no en el de la construcción, para evitar el doble parseo.


6. Registro de tipos de contenido

Valor Tipo Tamaño máximo del cuerpo Notas
0x00 Reservado (inválido) — NO DEBE usarse
0x01 Texto plano (UTF-8, Markdown restringido) 50 KB de pago / 10 KB gratuito Ver §10 para las reglas de renderizado. La división gratis / pago se aplica en el servicio de carga; el techo absoluto de la capa del protocolo es 50 KB.
0x02 Reservado (futuro) — Asignado para un futuro tipo de contenido; no válido en v1. Los visores DEBEN rechazarlo según la regla de abajo.
0x03 Pact (acuerdo bilateral, cuerpo CBOR) 100 KB El cuerpo es CBOR canónico PactTerms (§6.1). Firma del cosigner según §9.7.
0x04 Verdict (autocalificación del creador, cuerpo CBOR) 8 KB El cuerpo es CBOR canónico VerdictBody (§6.2). Emitido únicamente por el intent del sistema verdict. La relación con el padre va en la etiqueta de Arweave Parent-Tx-Id, no en el cuerpo. Ver verdict-uplift-plan §3.4.

Los visores DEBEN rechazar tipos de contenido desconocidos con un error claro y visible al usuario. Los visores NO DEBEN intentar renderizar tipos desconocidos como texto.

6.1 Cuerpo Pact (content_type = 0x03)

Un cuerpo pact es la codificación CBOR canónica de un valor 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)> }

Los órdenes canónicos de claves CBOR para los tres maps se dan en §3.2. El CBOR pact serializado total NO DEBE exceder los 100 KB (coincide con §6).

Discriminador de esquema. La primera fila en terms para un pact structured/v1 DEBE ser { key: "pact_schema", value: "structured/v1" }. Las filas sin este marcador son pacts "personalizados" y no reciben validación estructurada ni renderizado consciente del esquema.

Slots de reconocimiento congelados. Los pacts structured/v1 portan exactamente cuatro filas de reconocimiento bajo estas claves:

"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"

El value de cada una es una de ocho cadenas en inglés congeladas elegidas por el par (role, kind), donde role ∈ { seller, buyer, provider, client } y kind ∈ { standard, capacity }. Las cadenas mismas son datos normativos del protocolo — las firmas ML-DSA-65 de ambas partes se comprometen a los bytes exactos vía body_hash. NO están localizadas; el cuerpo firmado es neutro en cuanto al idioma. Cualquier cambio de redacción requiere una nueva versión de esquema (structured/v2).

Las ocho cadenas, su búsqueda (acknowledgement_for(role, kind)) y la justificación de cada una están fijadas por la implementación de referencia. Las implementaciones conformes DEBEN emitir valores de reconocimiento idénticos byte a byte; los tests de body-hash SHA3-256 con golden-fixtures que cubren las cuatro combinaciones de roles capturan cualquier deriva.

Orden de visualización en el visor. Las cadenas de reconocimiento contienen frases como "described above", que presuponen que las filas de descripción / alcance se renderizan antes que los reconocimientos. Los visores DEBEN renderizar el array terms en orden CBOR; reordenar rompe la semántica de la prosa.

Contacto de la contraparte. Cuando el contact de Party B es una dirección de email válida, el servicio de carga de qubs envía automáticamente un email de invitación a revisión / cofirma en el momento del staging y vincula la eventual cofirma a la verificación de esa misma dirección (§9.7). Los pacts cuyo contacto de Party B esté ausente todavía se pueden cofirmar, pero solo a través de un canal fuera de banda — el servicio rechaza solicitudes de cofirma que no puedan producir un marcador coincidente de verificación de email de 15 minutos.

6.2 Cuerpo Verdict (content_type = 0x04)

Un cuerpo verdict es la codificación CBOR canónica de un valor 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
}

Orden canónico de claves 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)

El CBOR de verdict serializado total NO DEBE exceder los 8 KB (coincide con la fila de la tabla anterior).

Enum de outcome. El byte de cable es neutro respecto al intent; las cuatro categorías Right / Partial / Wrong / Unfalsifiable cubren el espacio de desenlaces de todo intent portador de veredicto. Las etiquetas por intent ("Acerté" / "Lo cumplí" / "Entregado" / "Confirmada" para Right, etc.) son una decisión de renderizado del lado del lector, resuelta contra el intent del qub padre — el cable se mantiene neutro en cuanto a idioma e intent. Los valores fuera de 1..=4 DEBEN rechazarse al decodificar.

Vínculo con el padre. Un qub verdict NO porta la referencia al padre en su cuerpo. El id de transacción de Arweave del qub padre se emite como la etiqueta de almacenamiento Parent-Tx-Id en el momento de la carga (§7, capa de etiquetas de almacenamiento). Esto mantiene el cuerpo como una declaración firmada autocontenida de autoevaluación; la cadena de auditoría ("¿acerté sobre qué?") se establece mediante la búsqueda por etiqueta de Arweave.

Seguridad de la URL de evidencia (normativo). Cuando evidence_url está presente, los validadores (lado de composición, lado de cable, edge del Worker) DEBEN aplicar:

  1. Solo HTTPS. La cadena DEBE empezar con la secuencia de bytes https://. Cualquier otro esquema — http, ftp, javascript, data, file, etc. — se rechaza.
  2. Tope de longitud. ≤ 2.048 bytes (límite práctico de URL en navegadores).
  3. Verificación NFC + de codepoints hostiles. Misma regla que para title y reflection — los codepoints bidi-override / zero-width / tag-block / BOM / C0 / C1 se rechazan. La definición coincide con la Rust crate::handle::contains_hostile_text_codepoint y la TS workers/api/src/utils/unicode.ts::isHostileCodepoint (mantenlas sincronizadas).
  4. Sin espacios en blanco, sin controles ASCII. Cualquier espacio en blanco / DEL / byte sub-0x20 en cualquier parte de la URL se rechaza — cierra el vector de inyección \n/\t que la regla bidi no cubre.
  5. Segmento de host no vacío. Todo lo que va entre https:// y la primera /, ? o # DEBE ser no vacío.

Sin fetching del lado del servidor. El Worker NO DEBE proxiar, buscar ni previsualizar la URL. El protocolo almacena una cadena; el renderizado ocurre del lado del lector con rel="nofollow noopener noreferrer" target="_blank" y un host visible mostrado junto al texto del enlace.

Reflexión. Texto opcional de reflexión escrito por el creador ("qué cambió, qué aprendiste"). Misma validación NFC + de codepoints hostiles que title. La entrada vacía o de solo espacios en blanco se colapsa a ausente en el momento de la construcción.

Versión del esquema. v1 admite únicamente verdict_version = 0x01. Futuras revisiones del esquema incrementan este byte y aterrizan junto a una nueva versión del protocolo según §12.


7. Protocolo de sellado

La secuencia completa de sellado. Cada paso es 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.

Capa de tags de almacenamiento (fuera de banda). El servicio de carga de qubs adjunta un conjunto deliberadamente pequeño de tags de transacción de almacenamiento junto con la carga útil de subida elegida. Content-Type=application/octet-stream es normativamente obligatorio. El servicio de referencia adjunta además tres tags opcionales cuando el creador decide exponerlos: Intent (intención de composición validada contra la lista permitida: announcement, thesis, prediction, letter, secret, commitment, proof o el valor verdict emitido por el sistema), Author (fingerprint de la clave pública de §9.3 del creador como 64 caracteres hexadecimales en minúsculas) y Parent-Tx-Id (identificador de la transacción de almacenamiento del qub padre para cadenas de respuesta, 43 caracteres base64url).

El tag Author es opt-in por qub: la aplicación creadora de referencia lo adjunta solo cuando el usuario habilita explícitamente la atribución pública en el momento del sellado. Cuando el toggle está desactivado —el valor predeterminado— no se escribe ningún tag Author y el qub queda sin atribución en la cadena: nada en el almacenamiento permanente vincula la subida con el handle, el email u otros qubs del creador. Cuando el toggle está activado, el fingerprint Author se resuelve al @handle elegido por el creador a través de la cadena de atestación de §9.5. Las relaciones de cadena de respuesta y Intent no identifican a una persona. En la entrega privada, la envoltura externa (§13) cifra el artefacto interno reconocible SealedQub, por lo que recolectar envolturas almacenadas y obtener firmas públicas de drand sigue siendo insuficiente para recuperar el cuerpo sin K; los tags de almacenamiento continúan siendo metadatos deliberadamente públicos.

El servicio de referencia intencionalmente NO adjunta tags App-Name, App-Version ni Type: cualquier filtro de un solo valor de ese tipo devolvería todo el corpus de qubs a una consulta GraphQL, lo que es inconsistente con el alcance de confidencialidad solo-cuerpo del wrapper.

Un verificador conforme NO DEBE depender de ningún tag de almacenamiento para la verificación tercera de §11; el body hash / qub_id / firma se comprometen únicamente al CBOR interno, nunca al conjunto de tags.


8. Protocolo de unlock

La secuencia completa de unlock. Cada paso es 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 de autoría

9.1 Justificación

Los qubs se almacenan en el almacenamiento permanente. Las firmas de autoría deben permanecer infalsificables indefinidamente, razón por la cual v1.0 utiliza el esquema post-cuántico ML-DSA-65 (FIPS 204) en lugar de un esquema clásico cuya seguridad podría degradarse dentro de la vida permanente del qub.

9.2 Registro de algoritmos

sig_alg Esquema Tamaño de clave Tamaño de firma Estado
0x00 Sin firma — — Activo
0x01 ML-DSA-65 (FIPS 204) 1.952 bytes 3.309 bytes Activo
0x02 Ed25519 32 bytes 64 bytes Constante reservada; no compatible con el protocolo v1

Los visores del protocolo v1 DEBEN rechazar todo valor ajeno a {0x00, 0x01}, incluido el valor reservado 0x02. La reserva evita la reutilización accidental; no supone su activación. Activarlo requiere el cambio gobernado descrito en §15.

9.3 Construcción de la preimagen firmada

Han existido dos versiones de la preimagen. Todas las firmas DEBEN usar V2, y los verificadores DEBEN aceptar únicamente V2. La preimagen V1 heredada (documentada más abajo como referencia histórica) se aceptó como fallback exclusivo de verificación durante la migración a V2; ese fallback se ha retirado y una firma solo-V1 ahora se rechaza.

V2 (actual — producida por toda nueva firma de autor y por ambas firmas del flujo de staging / cofirma de pacts):

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 sigue la misma convención de centinela-ausente que title_hash (§4.2.1): 32 bytes a cero no son una salida SHA3-256 válida, de modo que «ausente» nunca puede colisionar con una etiqueta presente. Todos los campos son de ancho fijo, así que la preimagen es inequívoca sin prefijos de longitud.

V1 (heredada — RETIRADA; ya no se produce ni se acepta en la verificación):

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

La preimagen V1 omitía sender_label y reply_to. Se aceptó como fallback exclusivo de verificación durante la migración a V2; desde entonces ese fallback se ha retirado — los verificadores DEBEN aceptar únicamente la preimagen V2. La definición se conserva aquí como referencia histórica y para explicar el separador de dominio de más abajo. Una firma que solo se verifica contra V1 DEBE tratarse como un fallo de verificación.

Separadores de dominio: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" son 17 bytes ASCII cada uno ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Sin relleno. El separador distinto separa por dominio las dos construcciones, de modo que una firma sobre una preimagen nunca puede verificarse como la otra.

Byte org_id_present: el byte que sigue a unlock_at DEBE ser 0x00. La implementación de referencia lo expone como la constante ORG_ID_PRESENT_INDIVIDUAL = 0x00 en crates/qub-core/src/signing.rs; los visores que reconstruyan sig_input para verificación DEBEN emitir el mismo byte.

Alcance de la firma — qué cubre y qué no. El sig_input V2 se compromete directamente a version, qub_id, body_hash, unlock_at, sender_label y reply_to (más el separador de dominio fijo y el byte org_id_present). qub_id se deriva a su vez de version, content_type, created_at, unlock_at, outcome_at, drand_round y body_hash mediante la preimagen de §4.1, por lo que cualquier cambio en esos campos produce un qub_id diferente e invalida la firma transitivamente. La superficie autenticada es por tanto:

Campo Autenticado por la firma Cómo
version ✓ Entrada directa a sig_input
qub_id ✓ Entrada directa
body_hash ✓ Entrada directa
unlock_at ✓ Entrada directa
sender_label ✓ Entrada directa vía sender_label_hash (preimagen V2 — la única forma aceptada)
reply_to ✓ Entrada directa vía reply_to_or_zero (preimagen V2 — la única forma aceptada)
content_type ✓ Transitivamente, vía preimagen de qub_id
created_at ✓ Transitivamente, vía preimagen de qub_id
outcome_at ✓ Transitivamente, vía preimagen de qub_id
drand_round ✓ Transitivamente, vía preimagen de qub_id
body ✓ Transitivamente, vía body_hash = SHA3-256(body)
author_pubkey — (implícito) La clave que verificó la firma es el autor, por definición
cosigner_pubkey / cosigner_signature — Firmados independientemente sobre el mismo sig_input (ver §9.7)
drand_chain_id, tlock_ciphertext, visibility — Campos externos del SealedQub, no dentro del envelope — cubiertos por sus propias invariantes estructurales (consistencia de round / cadena) pero no por la firma del autor. (drand_round ahora está vinculado transitivamente vía la preimagen de qub_id — ver arriba.)

Por qué V2 es la única preimagen aceptada.

Las implementaciones que muestren sender_label o reply_to a usuarios finales DEBEN exponer la identidad autenticada (fingerprint de la pubkey, attestation) como la señal primaria de identidad, no la etiqueta.

9.4 Procedimiento de verificación

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 verificación de firma es la operación más costosa (especialmente ML-DSA-65). DEBERÍA realizarse después de que todas las comprobaciones más baratas (hash, qub_id, unlock_at) hayan pasado.

9.5 Identity attestations

Las attestations de identidad — el mapeo de author_pubkey a afirmaciones de identidad reconocibles por humanos como un handle de qub, dirección de email, handle social o credencial passkey — son una mejora progresiva del lado del visor y no son requeridas para la verificación de firma. Los visores que resuelvan attestations a una identidad de visualización DEBEN aplicar la precedencia:

handle > email > social > fingerprint

El fallback de fingerprint es el hex en minúsculas de SHA3-256(author_pubkey); siempre está disponible para cualquier qub firmado. Los visores PUEDEN abreviarlo para su visualización — el visor de referencia renderiza qub: seguido de los primeros y los últimos cuatro bytes (qub:<8 hex>…<8 hex>).

Un verificador conforme puede completar todas las comprobaciones de §9.4 sin contactar la API de qub, sin ninguna red más allá del almacenamiento permanente y drand, y sin ninguna búsqueda del lado del servidor. La resolución de attestation es un paso separado de mejor esfuerzo realizado solo después de que la verificación de firma haya tenido éxito.

9.6 Impacto en el tamaño

Ed25519 ML-DSA-65
Firma 64 bytes 3.309 bytes
Clave pública 32 bytes 1.952 bytes
Total por qub 96 bytes 5.261 bytes
Coste delta de almacenamiento (a ~$5/MB) ~$0,0005 ~$0,026

Para un qub de texto de 500–2.000 bytes, ML-DSA-65 aproximadamente triplica el tamaño almacenado. El coste absoluto es despreciable.

9.7 Verificación del cosigner (acuerdos bilaterales pact)

Para acuerdos bilaterales (content_type = 0x03), una segunda capa de firma demuestra que ambas partes consintieron a los mismos términos.

Campos del envelope:

Ambos campos DEBEN estar presentes juntos o ambos ausentes. Si exactamente uno está presente, los visores DEBEN reportar un error de integridad.

Procedimiento de verificación:

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."

Propiedades:

Verificación por email-binding (operacional). Cuando un pact en staging porta un contacto de email de Party B (§6.1), el servicio de carga de qubs DEBE rechazar la solicitud de cofirma a menos que exista un marcador de verificación de email de corta duración que coincida tanto con el id de staging como con el hash del email normalizado de ese contacto. El marcador es escrito por /api/v1/auth/verify cuando el token del magic-link porta un staging_id y la dirección verificada coincide con SHA-256(normalise_email(party_b.contact)) — donde normalise_email(addr) preserva la mayúscula/minúscula de la parte local y solo pasa a minúsculas la parte del dominio (según RFC 5321 §2.3.11), y SHA-256 aquí es el hash NIST FIPS 180-4 (distinto del SHA3-256 usado en las derivaciones de §4) — y expira 900 segundos (15 minutos) después de su emisión. Esta es una verificación operacional anti-suplantación, NO parte de la prueba on-chain del qub — un verificador tercero que reproduce §11 solo necesita el almacenamiento permanente y drand, sin ninguna búsqueda del lado del servidor. El marcador existe solo del lado del servidor y nunca forma parte del cuerpo firmado.

Impacto en el tamaño (autor ML-DSA-65 + cosigner):

Componente Tamaño
Firma del autor 3.309 bytes
Clave pública del autor 1.952 bytes
Firma del cosigner 3.309 bytes
Clave pública del cosigner 1.952 bytes
Total de overhead criptográfico 10.522 bytes
Coste delta de almacenamiento ~$0,05

10. Renderizado y saneado de Markdown

Esta sección es crítica para la seguridad. El visor renderiza qubs de texto (content_type = 0x01) usando un subconjunto restringido de Markdown.

10.1 Elementos permitidos

10.2 Elementos prohibidos

Elemento Tratamiento
HTML crudo (<div>, <script>, etc.) Eliminado por completo. Ningún HTML pasa.
Imágenes (![alt](url)) Eliminadas. La sintaxis de imagen se elimina de la salida.
Enlaces ([text](url)) URL renderizada como texto plano visible. Sin auto-link. No clickable sin acción explícita del usuario.
Esquemas de URL peligrosos javascript:, data:, vbscript:, file: — eliminados.
Iframes, embeds, objects Eliminados.
Entidades HTML Decodificadas a caracteres de visualización solo si son seguras.

10.3 Implementación

Las implementaciones DEBEN usar un parser de allowlist estricto, no de blocklist. El enfoque recomendado:

  1. Parsear Markdown usando pulldown-cmark (o equivalente).
  2. Recorrer el AST y descartar cualquier nodo que no esté en la allowlist (§10.1).
  3. Para nodos de enlace: emitir la URL como texto visible, no como elemento <a> clickable.
  4. Convertir el AST filtrado en una representación intermedia tipada (p. ej., un enum MarkdownNode con solo variantes seguras). El HTML crudo es estructuralmente irrepresentable en este IR.
  5. Renderizar desde el IR tipado a la capa de vista objetivo (p. ej., componentes de vista reactivos, nodos del DOM). Sin concatenación de strings HTML ni innerHTML en ningún punto.

Los enfoques de blocklist son frágiles porque nuevas extensiones de Markdown o peculiaridades del parser pueden introducir elementos no filtrados. El enfoque del AST tipado hace que el XSS sea estructuralmente imposible — no hay variante que pueda llevar HTML arbitrario.

10.4 Límites de tamaño y estructura


11. Verificación por terceros

Cualquier tercero que posea los bytes almacenados (y K para un qub privado envuelto) puede verificar el artefacto criptográfico sin la cooperación de qub. Una afirmación de existencia con marca temporal independiente requiere además una inclusión verificada de la transacción individual en el almacenamiento permanente o una prueba verificada del registro de transparencia de §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.

Qué prueba la verificación:

Entrada de prueba Qué establece
Bundle o artefacto sellado válido + firma de drand El cuerpo recuperado coincide con body_hash; los metadatos vinculados a qub_id están intactos; el texto cifrado está vinculado a la ronda de drand declarada y esa ronda ya transcurrió. Esto no establece cuándo se creó el texto cifrado.
Firma V2 válida del autor o del cofirmante Quien posee la clave secreta correspondiente autenticó la superficie firmada de §9.3.
Transacción individual de almacenamiento del qub verificada de forma independiente El texto cifrado exacto almacenado existía a más tardar en la marca temporal de su bloque.
Prueba válida del registro de transparencia anclado La afirmación específica del tipo de hoja de §16.11, incluido un límite superior del momento del compromiso obtenido del bloque de anclaje.

Qué NO prueba la verificación:

No-prueba Por qué
Autoría El sender_label es decorativo. Sin sig_alg ≥ 0x01, cualquiera podría haber sellado este contenido.
Intención El qub prueba contenido y temporalidad, no lo que el creador subjetivamente quiso decir.
Compromiso preexistente a partir de .qub por sí solo Un creador puede construir un bundle válido después de que haya transcurrido la ronda vinculada. La firma de drand incluida demuestra que transcurrió la ronda, no que el texto cifrado existiera antes.
Hora exacta del botón de sellado La marca temporal de un bloque de almacenamiento o de anclaje es un límite superior verificable de forma independiente y puede ir por detrás de la acción local del usuario. Las afirmaciones sealed_at / received_at no tienen valor probatorio.

El registro de transparencia implementado (§16) amplía la verificación entre qubs con un orden de manipulación detectable y un límite superior del momento del compromiso que no requiere confianza (la hora del bloque de anclaje), limitado según el tipo de hoja (§16.11). No añade autoría ni intención; en la ruta predeterminada de subida ciega a los bytes, tampoco demuestra por sí solo body_hash ni drand_round, que siguen procediendo de las comprobaciones del artefacto.


12. Versionado y control de publicaciones

Las publicaciones del documento, el protocolo interno de transmisión y la envoltura externa son espacios de versiones distintos. Así, una aclaración solo documental no cambia los bytes de forma silenciosa, y una futura migración del formato de transmisión no puede hacerse pasar por una revisión editorial.

12.1 Versión de la publicación del documento

Esta especificación usa versiones semánticas del documento (MAJOR.MINOR.PATCH) y un tag inmutable de Git llamado protocol-v<release>.

El estado de la publicación es Borrador (todavía no normativo), Actual (el único objetivo recomendado de implementación) u Obsoleto (conservado para verificación histórica). La ruta /protocolo sin versión muestra la publicación Actual; el tag de la publicación conserva su fuente exacta y todos los idiomas publicados con ella. Cambiar el estado o el número de publicación exige actualizar esta tabla y el historial de publicaciones en el mismo cambio revisado.

Versión del documento Fecha de entrada en vigor Estado Protocolo de transmisión Envoltura Fuente
1.0.0 2026-09-23 Actual 0x01 0x01 protocol-v1.0.0

12.2 Versión del protocolo

El campo version (u8) tanto en SealedQub como en QubEnvelope identifica la versión mayor del protocolo.

12.3 Historial de versiones del protocolo

Versión Valor Descripción
v1 0x01 Entrega privada envuelta y pública sin envoltura; cuerpos de texto (0x01), pacto (0x03) y veredicto (0x04); firma V2 de autor y cofirmante con ML-DSA-65; tlock de drand quicknet; SHA3-256.

12.4 Compatibilidad hacia adelante

Un visor v1 que se encuentre con un QubEnvelope con claves de map CBOR desconocidas (claves no presentes en el orden canónico §3.2) DEBE rechazarlo con un error de decodificación (§3.1). La compatibilidad hacia adelante descansa en el campo version, no en la tolerancia de claves: las adiciones futuras — incluso metadatos menores — se publican bajo un nuevo valor de version, que un visor v1 rechaza con un error claro de "protocolo más nuevo" en lugar de descartar silenciosamente contenido al que las firmas se comprometen.

Un visor v1 que se encuentre con sig_alg = 0x01 (ML-DSA-65) pero sin soporte de verificación ML-DSA-65 DEBERÍA mostrar el contenido del qub con un aviso de "firma presente pero no verificable", no rechazar el qub por completo. La implementación de referencia hoy rechaza todo valor sig_alg que no sea 0x00 o 0x01 porque el registro v1 no contiene ningún otro algoritmo válido — el rechazo estricto y el soft-fail son observacionalmente idénticos hasta que se registre un tercer algoritmo. El comportamiento soft-fail anterior se vuelve relevante una vez que §9.2 admita una nueva entrada, y el visor de referencia se actualizará para hacer soft-fail en ese momento.

12.5 Versión de la envoltura externa

El OuterWrapper descrito en §13 lleva su propio byte version, independiente de SealedQub.version y QubEnvelope.version. Los dos espacios de versión evolucionan por separado: un futuro reemplazo simétrico post-cuántico-seguro incrementa el byte del wrapper sin tocar la versión interna del protocolo, y una futura adición a la capa del protocolo (p. ej., un nuevo campo del envelope) incrementa la versión interna sin tocar el byte del wrapper.

OUTER_WRAPPER_VERSION_* Valor Algoritmo Estado
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM con nonce de 12 bytes, tag de autenticación de 16 bytes, AAD vinculada a qub_id Activa para la entrega privada
— 0x02–0xFF Reservado Futuro

Los visores DEBEN rechazar versiones de wrapper desconocidas con un error claro. El protocolo intencionalmente mantiene estrecho el espacio de versiones del wrapper hasta que aparezca un motivo concreto de migración (p. ej., una guía NIST que favorezca un AEAD diferente); se asignará un slot 0x02 en la misma revisión que introduzca el algoritmo.


13. Wrapper externo de cifrado

13.1 Justificación

Las capas del protocolo (QubEnvelope → tlock → SealedQub) hacen que un qub sellado esté bloqueado en el tiempo: el cuerpo es ilegible hasta unlock_at y hasta que se haya publicado la firma del round drand. Después del unlock, sin embargo, la firma del round es pública y la forma CBOR canónica del SealedQub es reconocible, por lo que un harvester que haya indexado las transacciones del almacenamiento permanente podría descifrar en masa el corpus completo de qubs.

Para la entrega privada, la envoltura externa de cifrado cierra ese canal interponiendo una capa AEAD simétrica adicional entre el SealedQubCbor canónico y los bytes almacenados. En la ruta de sellado del navegador, la clave de 256 bits K vive únicamente en el fragmento de la URL de entrega y en los dispositivos del usuario; los navegadores no transmiten los fragmentos de URL a los servidores, por lo que qub.social, todos los gateways de almacenamiento y los CDN que estén delante de ellos no ven K. La representación almacenada de un qub privado es, por tanto, texto cifrado opaco cuyo texto plano no puede recuperarse sin la URL que el creador decidió compartir. La entrega pública omite deliberadamente esta capa (§13.8).

Efecto neto:

13.2 Capas

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

El sellado y el unlock en la capa del protocolo (§7, §8) no cambian por debajo de la frontera del wrapper; el wrapper se adjunta en el sitio de llamada de seal() y se desadjunta en el sitio de llamada de unlock().

13.3 Estructura de datos del 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
}

Invariantes de campo.

Codificación CBOR. CBOR canónico según §3, con la misma regla de orden de claves (ordenadas por longitud de byte codificada ascendente, luego lexicográficamente). Las cuatro claves son:

Clave Bytes codificados Orden
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

El primer byte del CBOR del OuterWrapper es por tanto la cabecera de map de longitud definida para un map de 4 entradas (0xA4).

13.4 Vinculación AAD a qub_id

El wrapper vincula qub_id como additional authenticated data del AEAD. Esta es la defensa estructural portante contra tres clases de ataque:

Ataque Defensa
Mover el ciphertext bajo un campo qub_id diferente en el wrapper Discrepancia AAD → la autenticación AEAD falla
Mezclar el fragmento de URL del qub A con los bytes almacenados del qub B Clave incorrecta (y AAD vinculada de forma independiente) → la autenticación AEAD falla
Manipular el campo qub_id del wrapper después de la carga Discrepancia AAD → la autenticación AEAD falla

Llevar qub_id en el plaintext del wrapper no debilita significativamente la inmunidad a la enumeración — qub_id es a su vez un hash SHA3-256 de la preimagen de §4.1 sin preimagen recuperable a partir del digest, y un enumerador que ya haya recopilado los bytes del wrapper no aprende nada del qub_id visible que no pudiera inferir de la mera existencia de la carga.

13.5 Algoritmos de wrap y 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

Colapso del modo de fallo. Una K incorrecta, un nonce incorrecto, una discrepancia AAD y un ciphertext manipulado producen todos el mismo error DECRYPT_FAILED. Esta es una propiedad AEAD deliberada: distinguir el modo de fallo crearía un canal lateral que un atacante remoto podría sondear enviando wrappers malformados y midiendo el tiempo de respuesta. Las implementaciones de referencia DEBEN colapsar todos los fallos AEAD a una única forma de error.

13.6 Material de clave y distribución

La clave de envoltura K es un valor aleatorio uniforme de 256 bits generado por qub mediante un CSPRNG. Las implementaciones de referencia la obtienen de:

Distribución: K DEBE codificarse como base64 URL-safe (RFC 4648 §5, sin padding) y añadirse a la URL de entrega como componente fragment:

delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>

El fragmento nunca es transmitido a ningún servidor por un navegador conforme. Los canales de recuperación (índice de historial del lado del servidor, auto-envío opt-in por email) que persisten la URL de entrega completa — incluido el fragmento — más allá del dispositivo del usuario son una compensación explícita contra la postura por defecto de crypto-shredding y DEBEN estar condicionados al consentimiento explícito del usuario.

Pérdida del fragmento. Si un usuario pierde el fragmento de URL y no tiene canal de recuperación, el qub queda ilegible. Este es el compromiso portante del diseño y DEBE divulgarse al usuario en el momento del sellado. El MVP refuerza la divulgación al sellado con un texto explícito de "guarda esta URL" y un canal de recuperación por email verificado para los usuarios que opten por ello.

13.7 Fuera del alcance de esta sección

13.8 qubs públicos (omisión del wrapper)

El wrapper externo es opcional en la capa de entrega. Un creador puede sellar un qub como público, en cuyo caso el SealedQubCbor canónico entra directamente en el proceso de almacenamiento, sin capa OuterWrapper y sin clave K:

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

Un qub público está bloqueado en el tiempo pero no protegido por enlace: permanece ilegible hasta que se publica su round drand (la capa tlock no cambia), pero después del desbloqueo cualquiera que tenga el identificador de la transacción de almacenamiento puede descifrarlo — no se requiere ningún fragmento de URL, porque no hay K. Esta es la compensación deliberada para las superficies que el servidor debe impulsar: los emails de notificación de revelación, los enlaces oEmbed/embed automático sin fragmento y un SEO post-revelación más rico necesitan un enlace que funcione sin un secreto que el servidor nunca posee (§13.6). Un qub privado puede seguir usando la forma explícita <qub-embed src="full_delivery_url"> cuando quien publica proporciona la capacidad completa que contiene el fragmento.

Consecuencias que un productor DEBE tener en cuenta:

El modo privado (envuelto) sigue siendo el predeterminado; el público es una elección explícita del creador por cada qub.


14. Vectores de prueba

14.1 Derivación de 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

Las implementaciones DEBEN producir valores body_hash y qub_id idénticos para esta entrada. Este vector de prueba DEBERÍA ser el primer test unitario escrito. Los valores canónicos anteriores fueron calculados por la implementación de referencia y DEBEN coincidir bit por bit. Los diseños históricos de prototipos anteriores al lanzamiento (ningún qub activo dependía de los dos primeros) usaban 92 bytes antes de outcome_at (3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0) y 100 bytes después de añadir outcome_at_or_zero (b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed). El diseño actual de 108 bytes añadió después drand_round y el separador de dominio QUB_ID_V2. Un vector temprano de 108 bytes usó el mapeo heredado con ceil (drand_round = 4695445) y produjo 3a9fcb31b750d985c262fada6d4f777fd6a28be831d941d85c131f5a4bbaf8a4: sigue siendo un qub_id válido para esa entrada de ronda, mientras que el ejemplo anterior sigue el mapeo de ronda vigente de §4.3.

14.2 Mapeo de unlock-round

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

La ronda 4675286 se publica en 1595431050 + (4675286 - 1) * 30 = 1735689600, exactamente en unlock_at, nunca antes. (El mapeo anterior a la publicación con ceil daba 4675285, publicada en 1735689570, 30 segundos antes; los verificadores aceptan esa ronda heredada según §4.3.)

14.3 Round-trip CBOR canónico

Las implementaciones DEBEN verificar que serialize(parse(serialize(qub))) == serialize(qub) para todas las entradas válidas. Esto es un test de propiedad, no un solo vector.

14.4 PactTerms CBOR (content_type 0x03)

Input:
  pact_version = 1
  title        = "Scooter deposit"
  terms        = [
    { key: "Item",    value: "Honda Metropolitan scooter" },
    { key: "Price",   value: "$100" },
    { key: "Deposit", value: "$10" }
  ]
  party_a      = { label: "Alice" }
  party_b      = { label: "Bob", contact: "bob@example.com" }
  notes        = absent

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

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

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

Los bytes CBOR canónicos y el body_hash SHA3-256 son computados por la implementación de referencia. Las implementaciones DEBEN producir CBOR idéntico byte a byte para esta entrada.

Las implementaciones DEBEN también verificar que serialize(parse(serialize(pact))) == serialize(pact) para todas las entradas PactTerms válidas (test de propiedad).

14.5 Vectores cross-language del wrapper externo

El wrapper externo (§13) tiene un fixture canónico separado en crates/qub-core/tests/vectors/wrapper_v1.json. Cada caso fija una tupla (key, nonce, qub_id, sealed_cbor) como entradas hex opacas y afirma una salida expected_wrapper_hex específica. Ambas implementaciones de referencia consumen el mismo fichero JSON:

El fixture fija actualmente tres casos de bajo nivel de la envoltura. Comprueban la codificación determinista de OuterWrapper y la interoperabilidad AEAD independientemente del invariante de forma de entrega de §13.8; en particular, el nombre histórico basic-text-public y su visibility = 0x01 interno no convierten los bytes envueltos resultantes en una entrega pública conforme. Un productor DEBE almacenar los bytes internos públicos sin envoltura y envolver únicamente bytes internos privados (0x00).

Caso Cobertura
basic-text-public Nombre histórico del fixture de bajo nivel. La forma realista más pequeña de SealedQub, sin campos opcionales; solo prueba los bytes de la envoltura y no es una entrega almacenada conforme con §13.8.
with-recipient-pubkey SealedQub con recipient_pubkey establecido (ruta futura reservada). Ejercita un conjunto distinto de claves CBOR internas; el contenido distinto del fixture produce por sí mismo un qub_id diferente (recipient_pubkey no forma parte de la preimagen de §4.1).
longer-body Cuerpo de ~4 KiB — ejercita prefijos de longitud CBOR multi-byte tanto dentro del envelope interno como del ciphertext externo.

Las implementaciones DEBEN producir un expected_wrapper_hex idéntico byte a byte para las entradas registradas. La regeneración del fixture requiere QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors y se reserva para cambios de formato deliberados.


15. Gobernanza del perfil criptográfico (Futuro)

Esta sección es informativa para v1 y se vuelve normativa la primera vez que un segundo algoritmo entre en alguna de las primitivas criptográficas de qub.

15.1 Postura actual

El protocolo v1 vincula exactamente un algoritmo por primitiva:

Los verificadores codifican actualmente de forma fija las longitudes de clave y firma por primitiva activa. Los bytes sig_alg y de versión de la envoltura son selectores explícitos, pero v1 no realiza negociación en banda y solo admite los valores activos anteriores.

15.2 Forma prevista

Cuando un segundo algoritmo entre en el protocolo, el visor se configurará para un CryptoProfile con nombre (p. ej., ExqubV1) que enumere el conjunto exacto de valores permitidos por primitiva — sig_algs, chains de drand, versiones de wrapper, tipos de contenido. El perfil se fija en el momento de la verificación, nunca se negocia en banda. Cualquier valor fuera del perfil activo es rechazado.

Esto garantiza que añadir ML-DSA-87 o activar Ed25519 no pueda debilitar retroactivamente las configuraciones de visor existentes: un visor v1 sigue siendo un visor v1 incluso después de que se publique un perfil v2.

15.3 Condiciones de disparo

Promueva §15 a estado normativo cuando se proponga cualquiera de los siguientes:

Hasta entonces, §15 es un marcador de posición que fija la forma de la migración para que los PRs futuros aterricen contra un objetivo conocido en lugar de volver a litigar la superficie de negociación desde cero.


16. Registro de transparencia y niveles de durabilidad (implementados — revisión completada)

Estado. Esta sección está implementada (W5/UP-B1, Etapas 1–8), con el alcance productor y raíz de confianza indicados aquí. Los formatos de cables, hashing y rutas de verificador están activos: los tipos principales Merkle + canónico-CBOR (qub-core), el espejo TypeScript + ANS-104 bundler (workers/api/src/crypto/), el LogDO de escritor único + el almacenamiento de nodos R2 con clave de coordenadas, el intento de añado de logarítmic /upload, los crons de ancla diaria + bundler-drain, los extremos de prueba GET /api/v1/qub/:tx_id/proof (inclusión) y GET /api/v1/log/consistency (RFC 9162), la prueba de inclusión tipada que lleva en el paquete .qub (§17.5), el verificador ancla nativo ANS-104 (tools/qub-verify), y el gancho doble de cabezas autopublicadas (§16.6). Un /upload exitoso siempre es duradero por R2, pero solo se cubre log-deck cuando LOG_DO está configurado y la adición en línea tiene éxito; solo entonces su respuesta lleva log_seq, receipt y anchor_status. Si RECEIPT_SK está ausente o es inválida, el sig_b64url de ese recibo está vacío y no proporciona no repudiación. Las rutas actuales de publicación de /seal y pact programan transacciones individuales de Arweave pero no añaden una hoja de logarítmic. Ningún código realiza actualmente la conciliación posterior propuesta por el comentario /upload tras un fallo de anexión. La revisión externa del W5 está completa: §16.15 registra las decisiones de diseño y las restricciones de lanzamiento, pero esas restricciones no amplían la cobertura de productores acabada de indicar. Tres elementos de confianza/despliegue permanecen con puerta restringida: (a) la cartera ancla dedicada (ANCHOR_JWK; LogProfile.anchor_owner sigue siendo el marcador de [0xAB; 32] posición); (b) la clave de firma de recibo y el PIN de clave pública correspondiente (RECEIPT_SK es opcional y LogProfile.receipt_pubkey está actualmente vacía); y (c) el repositorio de GitHub con cabezas autopublicadas + token (§16.6). Hasta que los pines ancla/perfil estén provisionados, un verificador independiente informa del estado de la prueba de forma honesta en lugar de reclamar una verificación totalmente anclada y fijada. El diseño es estrictamente aditivo y no hay ningún cambio en el formato de SealedQub / QubEnvelope cable.

16.1 Niveles de Justificación y Durabilidad

Las rutas de publicación actuales desacoplan el acuse de recibo de la confirmación Arweave: derivan y firman una transacción individual, persisten el artefacto y el estado exacto de envío en R2, y luego publican de forma asíncrona. El registro de transparencia añade una capa de orden anclada independientemente para el subconjunto de solicitudes generales de /upload cuyo LogDO añadido tiene éxito:

Nivel Nombre Garantía Cuándo
T1 R2-primer acuse de recibo síncrono Piso de durabilidad — los bytes sellados y el estado exacto de publicación se escriben en almacenamiento duradero antes de que se devuelva el éxito. Implementado en las rutas de publicación actuales.
T2 Inclusión en el registro de transparencia por lotes Compromiso solo de anexado, a prueba de manipulaciones + orden total una vez incluido y anclado. Productor actual: anexos exitosos LogDO desde /upload; la respuesta lleva el par de recibo. No universal.
T3 Permanencia por-qub en Arweave Una transacción individual de Arweave para el qub. Actualmente preparada para cada publicación aceptada y publicada de forma asincrónica; la transacción exacta firmada permanece en la bandeja de salida drainable hasta su entrega.

Los niveles describen propiedades distintas de evidencia y durabilidad, no el plan comercial actual. El código presente todavía programa una transacción individual de Arweave para cada publicación aceptada; no expone T3 solo como una venta adicional de pago. Los techos de cuota de clave API/cuenta siguen siendo controles de aplicación separados.

Honestidad de durabilidad. La escritura T1 es síncrona, por lo que una respuesta exitosa establece durabilidad a nivel de aplicación sin esperar un gateway de Arweave. No establece por sí misma un sello de tiempo independiente. Una transacción individual confirmada proporciona su límite superior de tiempo de bloque. Para una respuesta que lleva el par completo de recibo T2, el siguiente anclaje confirmado puede suministrar la prueba de registro descrita a continuación. Si el par está ausente, ninguna superficie puede implicar que este qub ya esté en el registro de transparencia. La latencia de anclaje y publicación no tiene un SLA numérico a nivel de protocolo.

16.2 Estructura de LogLeaf (dos formas comprometidas)

Una entrada de registro es un LogLeaf, codificada como CBOR canónica escrita a mano bajo el perfil §3.1 (longitud definida, sin etiquetas, sin flotantes, enteros en la forma más corta, texto NFC, campos opcionales omitidos cuando están ausentes, claves ordenadas por longitud de byte codificada ascendente y luego por bytes). La guardia canónica §3.1 parse → re-encode → compare se aplica en la ruta de codificación antes del hashing (no solo en la decodificación), por lo que dos implementaciones no pueden discrepar en los bytes de la hoja debido a una diferencia de ancho de enteros o de orden de claves. Todos los enteros son u8 / u64 / i64; todos los resúmenes son cadenas de bytes de 32 bytes (bstr[32]). Un id de transacción de Arweave almacenado es un hash SHA-256 crudo de 32 bytes llevado como bstr[32], nunca una cadena de texto base64url (cumple con §3.3).

La hoja tiene dos formas seleccionadas por un byte kind, porque la ruta general de carga no considera los bytes: POST /api/v1/upload trata deliberadamente ambas formas de carga aceptadas como opacas y recibe qub_id y unlock_at solo como afirmaciones de cliente no confiables. En la ruta privada predeterminada, body_hash, drand_round, created_at y drand_chain_version se ocultan además dentro del contenedor externo §13, cuya clave el Worker nunca posee. El sistema de tipos también define una forma atestiguada para un productor que deriva body_hash / drand_round por sí mismo. La ruta /seal actual tiene esos valores pero no llama a LogDO, por lo que en producción actualmente solo se emiten hojas afirmadas (0x02) de agregaciones de carga general exitosas. La separación mantiene cada valor comprometido honesto sin fingir que el productor atestiguado está conectado:

Clave Enc. len Tipo Presencia significado
seq 4 u64 requeridos índice global basado en 0 de hojas; la posición a la que se compromete la prueba de inclusión.
kind 5 u8 requeridos 0x01 autentificado (definido, no emitido actualmente) o 0x02 aseverado (cliente seal / subida a byte-blind).
ref 4 bstr[32] requería Identificador de referencia de hoja. Atestiguado → qub_id en bruto. Afirmado → el id ciego** SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1).
chash 6 bstr[32] requeridos SHA3-256(stored_bytes) de dirección de contenido — el único vínculo de contenido que el Worker siempre puede calcular honestamente, en ambos caminos.
unlock_at 10 i64 requeridos Copiado (atestiguado) o afirmado (afirmado); validado > 0 antes de que entre en la hoja.
received_at 12 i64 requeridos Reloj de pared Worker a R2-ack. No probatorio (afirmado por el operador; §16.6). Presente para autodescripción, nunca una prueba. Validado > 0.
body_hash 10 bstr[32] kind=0x01 solo Omitido en 0x02 — el Trabajador no lo tiene bajo el §13.
drand_round 12 u64 kind=0x01 solo Omitido en 0x02.

Una hoja kind=0x02 no compromete deliberadamente ni body_hash ni drand_round: atestigua el compromiso y el orden de un texto cifrado opaco en la dirección de contenido chash, reclamando qub_id y unlock_at — no su texto plano ni su ronda. Las patas de texto plano/redondeadas para un qub afirmado provienen de la verificación existente de §11 en .qub-fibrado, no del log (§16.11). drand_chain_version no está en la hoja (está dentro del envoltorio en la ruta por defecto); la granularidad de la cadena reside en el ancla (§16.7). Disciplina codificadora: rechazar un ref o chash completamente nulo, y rechazar unlock_at / received_at no positivos, reflejando el guardia centinela outcome_at > 0 en cbor.rs.

16.2.1 Cegamiento de qub privado

El registro no debe convertirse en el oráculo de enumeración que el envoltorio externo §13 existe para prevenir (§13.1). Para un qub privado (empaquetado), la hoja asserted compromete el identificador cegado SHA3-256(qub_id ‖ log_blind_secret), donde log_blind_secret es un secreto mantenido por el servidor, y omite body_hash. Un tercero no puede vincular tal hoja a un qub_id específico; el poseedor del qub, que tiene la URL de entrega y por lo tanto qub_id, puede recalcular el cegamiento para confirmar su propia inclusión. Un qub público (ya enumerable, ya llevando la etiqueta Visibility: public de Arweave según §13.8) compromete el qub_id sin procesar. Este es el único lugar donde la verificabilidad independiente cede deliberadamente a un invariante de privacidad que soporta carga; la vinculación independiente para qubs privados es chash (§16.9).

Custodia log_blind_secret (resuelto — §16.15 P4). El cegamiento protege la no vinculación de hojas, no la confidencialidad del texto plano (el envoltorio §13 lo mantiene independientemente). Ante una compromisión log_blind_secret, para cualquier qub_id que el adversario ya posea o pueda reconstruir (cada qub cuyo paquete/URL tiene, más cualquier qub_id de baja entropía o pública) vuelve a calcular la hoja ref en un solo hash y la enlaza — esta es una vinculación directa de una población conocida, no un ataque de fuerza bruta sobre un espacio desconocido. Clasifique log_blind_secret como un secreto de tipo correlación/Sybil en el mismo nivel de custodia que otros secretos de servidor, y rote hacia adelante únicamente (una rotación vuelve a cegar las hojas futuras; no puede desvincular retroactivamente las ya ancladas).

16.3 Hashing de Hoja y Nodo

Hashing separado por dominio RFC 6962 §2.1 con SHA-256 reemplazado por 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

Los bytes de prefijo de dominio 0x02 (cadena de entrada, §16.4) y 0x03 (hash STH, §16.6) están reservados y son disjuntos de estos. Son bytes únicos y por lo tanto no pueden colisionar con los separadores de dominio ASCII existentes de 10 bytes (QUB_ID_V2, etc.). El árbol es el árbol izquierdo-completo no balanceado de RFC 6962 (cada división interior en la mayor potencia de dos estrictamente menor que el número de hojas del subárbol), lo que permite que las pruebas de inclusión y consistencia compartan un único algoritmo de ruta de auditoría. La especificación de referencia incluye pseudocódigo explícito de derivación izquierda/derecha y fija un vector de prueba no potencia de dos (5 hojas) de modo que se ejercite el caso de promoción del borde derecho — que un vector de 4 hojas oculta.

16.4 Encadenamiento de Hash (interno)

El LogDO mantiene una cadena interna de entradas solo para consistencia ante fallos. Es nunca publicada y nunca visible para el verificador:

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

La autoridad publicada solo por adicción es la raíz de Merkle acumulada + su ancla (§16.5–16.6), nunca el orden bruto en que el operador sirve se va: la cadena recalcula para cualquier orden servido, por lo que solo la raíz anclada pone la posición canónica.

16.5 Árbol Merkle acumulado y lote

Existe un árbol RFC 6962 en constante crecimiento sobre todas las hojas en seq orden — no es árbol aislado por lote. (Se rechazó una construcción por lote encadenada de hojas de acarreo: no es una relación de prefijo verdadera, por lo que sus "demostraciones de consistencia" no son sólidas.) El árbol acumulativo proporciona pruebas genuinas de consistencia RFC 9162 y permite que un único ancla reciente demuestre inclusión para cualquier qub antiguo.

El LogDO Objeto Durable es el único escritor (blockConcurrencyWhile, espejando QuotaDO / EntitlementDO) — añadir a un registro compartido es lectura-modifica-escritura en estado compartido y por tanto DEBE pasar por un DO, nunca KV. Almacena en caché la frontera del borde derecho del árbol (O(log n) hashes), por lo que cerrar un lote es O(batch). Un lote es el conjunto de hojas ancladas entre sí; sus disparadores implementados son un avance tree_size de al menos LOG_BATCH_MAX_LEAVES (por defecto 4096), la edad alcanzando la cadencia del ancla, o un cierre forzado explícito administrativo/cron. root_i es el Hachís del Árbol de Merkle acumulado sobre hojas 0 .. tree_size_i.

16.6 Señalado como Cabeza de Árbol vía Arweave Anchor

La transacción de ancla de Arweave es la Cabeza de Árbol Signada y reemplaza una firma de operador para la propia cabeza del árbol: el ancla diaria no necesita clave de qub porque la owner de transferencia de Arweave es la firma. La tesis del foso se cumple — el sustrato inmutable, no un secreto contenido en qub, es portador de carga para la raíz anclada.

El diseño del registro requiere una clave de recibimiento de añada exitosa cálida (§16.10), fijada en LogProfile y firmada cruzadamente por anchor_owner. La implementación actual no ha completado esa provisión raíz de confianza: RECEIPT_SK es opcional, una clave ausente/inválida da sig_b64url: "" y el LogProfile.receipt_pubkey compilado está vacío. Dicho recibo puede describir la hoja añadida pero no es un recibo firmado no repudiable. La afirmación de diseño más fuerte se aplica solo después de que una liberación del verificador fije la clave pública correspondiente y el propietario del ancla la firme cruzadamente. Una respuesta de publicación sin la tupla completa de recibo no hace reclamación de aceptación de log; una con firma vacía hace una afirmación de añadir posición pero no de verificación de firma.

El SignedTreeHead es el CBOR canónico (claves por longitud codificada): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (prior sth_hash; génesis = 32 bytes cero), log_id:bstr[32], first_seq:u64, anchored_at:i64. Su hash es sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).

Raíz de confianza fijada. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Un verificador conforme DEBE requerir anchor_tx.owner == LogProfile.anchor_owner, donde anchor_owner (y la clave pública de la clave de recibo) se integre en qub_core como la LogProfile — junto con las constantes de quicknet ya en DrandTimelockProvider::quicknet() — y se distribuya con el binario del verificador. El verificador TAMBIÉN DEBE verificar el datos vinculando localmente el → tx_id de Arweave tx en lugar de confiar en una respuesta /raw/ pasarela. Esto cierra el agujero de ambigüedad del monedero fraudulento: "anclado en Arweave" no tiene sentido hasta que el verificador pinye cuál monedero.

**La rotación es una extensión de gobernanza §15, no una reutilización (resuelta — §16.15 Q3). La superficie de perfil de §15.2 actualmente enumera solo cadenas sig_algs / drand / versiones de envoltorio / tipos de contenido, y los disparadores de §15.3 no listan ninguno de estos — LogProfile / anchor_owner aún no está en la superficie de §15. Por lo tanto, la gobernanza de rotaciones debe construirse: §15.3 se extiende (abajo) para añadir el disparador LogProfile, y una rotación es un bump de LogProfile firmado enviado en una actualización del verificador. Una rotación planificada lleva una firma cruzada saliente → entrada; Una rotación impulsada por compromiso no puede (la clave saliente es no confiable/no disponible precisamente entonces) y vuelve al aumento regulado por §15, con la comprobación de la bifurcación del ancla previa (abajo) delimitando daño mientras tanto.

Ventana de equívoca (parámetro de confianza de primera clase). Una hoja es resistente a la equívoca solo una vez que su ancla de cobertura es Arweave-confirmada. La ventana es received_at → anchor confirmation (cadencia + finalidad Arweave, sin garantía de latencia de protocolo). Antes de la provisión raíz de confianza, la implementación actual proporciona la integridad operativa de qub más los metadatos de anexo no firmados presentes; no proporciona la garantía de no repudiación planificada. Tres artefactos de responsabilidad definen el diseño completado (el modelo testigo es la resolución de §16.15 Q2):

  1. Sellar recibo (dependiente de provisionamiento) — el análogo SCT devuelto cuando la adición de registro de una subida tiene éxito (§16.10). Se vuelve no repudiable solo cuando sig_b64url no está vacía y la relación correspondiente de clave pública/propietario ancla está fijada en el verificador. El pin de perfil de producción actualmente vacío no puede soportar ese veredicto. Este control no se aplica a una tupla de recibo omitida ni a un recibo sin firmar.
  2. Metodología de monitor publicada + recorrido prev-chain — el ancla prev cadena es head→genesis caminada; una bifurcación (dos anclajes en un size con root diferente, o un prev roto) es prueba publicable de mala conducta. La detección de equivocación es un compromiso operativo declarado, no una suposición silenciosa.
  3. Encabezados autopublicados duales — cada nuevo encabezado {sth_hash, tree_size} se publica en un repositorio público de GitHub dedicado y solo para adicciones propiedad de qub (la etapa de autopublicación a prueba de manipulación que soporta carga), con una publicación social como corroboración de mejor esfuerzo. Una página DEBE fallar de publicación (no fallar silenciosa). Implementado (Etapa 8) como gancho publishHead en el cron ancla (workers/api/src/utils/heads-publish.ts): un PUT a la API de contenidos sin sha es solo añadido (un 422 significa que el encabezado ya está publicado, nunca se sobrescribe); opt-in / desplegable bloqueado en PUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO} e inerte hasta que el repositorio esté provisionado. Un hard GitHub que falla páginas a través del canal health_alert y se impulsa una métrica de fallo duradera (m:tlog_publish_head_fail); el propio ancla de Arweave nunca reverte un fallo de publicación. "No fallar silencioso" está garantizado por esa métrica duradera — que las operaciones DEBEN alertar al panel — incluso si no se puede entregar la página de correo con mejor esfuerzo. Dos limitaciones honestas se derivan del "ancla en avance" (el cron solo publica cuando el tamaño avanza): un fallo transitorio en GitHub deja un hueco en la secuencia de encabezados publicados para ese tamaño — acotado, no silencioso (it pages), y como cada cabeza compromete un árbol de superconjunto, una prueba de consistencia §16.9 cubre la brecha; crucialmente, esa prueba de consistencia se calcula a partir del árbol autorizado anclado a Arweave, no de la superficie de GitHub, por lo que una brecha en GitHub nunca debilita la verificabilidad. Un relleno de recuperación que cubra los vacíos de cabezas publicadas es una mejora diferida.

Honestidad vinculante (restricción vinculante). Debido a que qub controla ambas superficies de publicación planificada, esto es autopublicado, no testificado de manera independiente. Ninguna superficie de producto, marketing o legal puede afirmar que el registro es "testificado de forma independiente". Después de que se provisionen las puertas de recibo/perfil/cabeza, la afirmación permitida es que la equivocación es detectable y un apéndice firmado con éxito deja un recibo no repudiable. Antes de eso, esa afirmación no está disponible. Un verdadero testigo independiente de terceros se pospone para un futuro ajuste de gobernanza §15.

received_at es declarado por el operador y ninguna afirmación puede basarse en él: nunca se presenta como prueba o como corroboración de disputa en ninguna superficie de producto / legal / API / renderizado de prueba. El tiempo del bloque ancla de Arweave T es la única marca de tiempo confiable (un límite superior de "logrado por"). Cualquier verificación de coherencia de monitor sobre received_at DEBE compararse con T, no con el campo STH controlado por el operador anchored_at; tal verificación es solo una salvaguarda contra un error de reloj de un operador honesto, no un control de responsabilidad contra un operador malicioso (§16.15 Q5).

16.7 Formato y Cadencia de la Transacción Ancla

AnchorBundle es el cuerpo de la transacción Arweave canonical-CBOR, escrito a través del §16.8 bundler: ver:u8, sth:bstr (bytes canonicos SignedTreeHead), prev_anchor:bstr (id de tx ancla previa en bytes crudos; omitido en el génesis), chain_hash:tstr (la cadena drand en vigor — quicknet), y el flujo leaf-CBOR del lote en orden seq para que el ancla sea autosuficiente: un monitor re-deriva root a partir del cuerpo sin dependencia de qub. (Si el flujo leaf se vuelve grande a alto volumen, una futura revisión puede comprometer solo un rango de hojas por referencia; señalado, no adoptado en v1.)

Las etiquetas de Arweave son intencionalmente enumerables — el registro está destinado a ser encontrado, a diferencia de los qubs privados: App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. Las etiquetas son indicaciones no confiables; el cuerpo CBOR es la única autoridad.

Cadencia: diario por defecto, revisada con volumen (el disparador de tamaño acorta automáticamente la cadencia efectiva bajo carga). El productor actual no implementa un gancho de ancla forzada con sellado pagado. La cartera ancla es dedicada y de baja velocidad, separada de la cartera de subida — DEBE ser su propia JWK (una clave distinta, no un rol lógico en la cartera de subida) por lo que un compromiso de cartera de subida no puede forjar anclajes — con un presupuesto duro de transacción ancla diaria. La postura de custodia se expresa claramente: una tecla rápida de alcance estrecho con un interruptor automático ajustado y bajo saldo, no "fría" — una cartera que firma automáticamente a diario no puede estar en frío, y la especificación no pretende lo contrario.

16.8 ANS-104 Bundler

Un codificador interno ANS-104 DataItem y un firmador de hash profundo, aproximadamente 300 líneas, solo Web Crypto, cero dependencias npm (ambos SDKs Turbo fallan en la npm ci --ignore-scripts puerta de la cadena de suministro). Distribución de bytes 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 es Arweave deepHash — un resumen recursivo SHA-384 (requisito de cable de Arweave, crypto.subtle.digest("SHA-384")) sobre ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — luego RSA-PSS sobre el hash profundo con la cartera JWK vía crypto.subtle; id = base64url(SHA-256(signature)). El SHA-384 aquí está cuarentenado como primitivo solo de cable Arweave, nunca primitivo de confianza qub (§15 registra la cerca; el hash de confianza qub es SHA3-256 en todo momento).

La ruta de código ANS-104 sirve a la maquinaria de respaldo/drenaje diferido y escribe AnchorBundle DataItems. La ruta de publicación ordinaria crea primero una transacción Arweave firmada exacta y mantiene su JSON en una bandeja de salida duradera; la publicación directa es una optimización de latencia, y la ruta de drenaje intenta la misma transacción antes de aplicar su reserva de empaquetador. Esquema de firma (resuelto — §16.15 Q8): v1 firma con RSA-PSS (tipo de firma 1) reutilizando el mecanismo existente de la cartera Arweave JWK (cero nueva custodia de claves de larga duración, sirviendo a la tesis de "un secreto menos"); Ed25519 se aplaza a la ruta de migración PQ §15.

El hash profundo enrollado a mano es el código de mayor riesgo y menor cobertura natural en W5, por lo que su barrera es innegociable (§16.15 Q8):

  1. El tlog_v1.json de fijación multi-lenguaje (Rust + TS, el patrón wrapper_v1.json §14.5) cubre el hash profundo, bytes DataItem + id, hashes de hoja, una raíz de 5 hojas + ruta de auditoría, un hash STH, una prueba de inclusión y una prueba de consistencia — en tanto en la señal como en las instrucciones de verificación (la dirección de verificación importa porque la verificación local de → tx_id de §16.6 extrae el hash profundo a todos los verificadores independientes, no solo al autor).
  2. Un viaje de ida y vuelta de interoperabilidad única a través de un bundler de referencia ANS-104, consumido solo como datos de prueba estáticos — nunca una dependencia en tiempo de ejecución npm (la postura solo Web-Crypto / sin scripts de instalación se mantiene).
  3. La ruta deep-hash + RSA-PSS debe recorrer los mismos crypto.subtle primitivas usa en producción, por lo que el codificador interno es compatible con bytes.
  4. Un monitor de aceptación post-bundle continuo confirma que cada DataItem ancla / respaldo realmente logra la aceptación de Arweave, con alarma + interruptor automático — porque el hash profundo también sirve a la cola de reserva de indisponibilidad de Arweave, por lo que una regresión silenciosa llenaría esa cola con elementos rechazados por la red durante la misma caída que debe cubrir.

16.9 Demostraciones de inclusión y consistencia

Ambos son RFC 9162, SHA3-256, sirvieron como CBOR canónico.

InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (el CBOR exacto de hoja — el verificador se recalcula a sí leaf_hash mismo y nunca confía en un hash suministrado), 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 única lista de claves inequívoca, fijada por vector de prueba.

Verificación independiente (sin servidor qub, extiende §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).

El almacenamiento con servicio de pruebas DEBE estar con clave por coordenadas (resuelto — §16.15 Q7, precondición de bloqueo). La generación de pruebas en hoja fría es neutral en la corrección solo si el material de auditoría R2 es un almacén persistente de nodos de Merkle claveado por (level, index) de coordenadas absolutas del árbol** — no por batch deltas de nodo. Con un almacén con clave por coordenadas, cualquier ruta de auditoría de (leaf i, size N) es O(log N) un conjunto de GET directos R2 sin sin recomputación a través de límites de lote; con un almacenamiento con clave por lotes no lo es, que es la brecha de almacenamiento y disposición que esta resolución cierra. Los cuerpos de hoja también son direccionables por contenido por seq. Un vector de prueba W5 DEBE demostrar una hoja fría de la era génesis contra una raíz mucho más tardía usando solo R2 + Arweave con el almacenamiento LogDO borrado, por lo que la afirmación de seguridad de reclamación en §16.13 está respaldada en lugar de afirmada. Los ZXXZ R2 secuenciales de O(log N) GET pertenecen solo al extremo de prueba asíncrona — nunca en el camino caliente del sello (§16.10) ni en un cron por tick.

16.10 R2 - Primer pedido de captura

La secuencia de POST /api/v1/upload implementada es:

  1. Puertas de la mitad frontal (autenticación, validación, clave de fragmento de idempotencia) — sin cambios.
  2. Crear, etiquetar y firmar la transacción Arweave individual exacta. Esto se obtiene tx_id localmente, aunque la creación de transacciones puede obtener metadatos de recompensa/ancla de una pasarela. Un fallo de preparación sigue fallando la solicitud antes de que se recomigue.
  3. Escribe sincronizada el artefacto seleccionado en qub-cache/<tx_id> y persiste los registros estables de creación-operación/salida de salida. Estos son el suelo de durabilidad y de reintentos; los fallos antes de la resolución regresan 503.
  4. Cuando LOG_DO está configurado, intento sincrónico LogDO.append(leaf). El único escritor asigna seq, extiende la cadena de entrada y actualiza la frontera. El RPC de añadir solo hace eso; el cierre por lotes se desvía de la ruta en la alarma. Un fallo de transporte/aplicación de anexo es actualmente fallo-soft: la respuesta aún puede tener éxito sin log_seq, receipt o anchor_status. A pesar de un comentario de implementación, hoy no se conecta con cable una conciliación automática de registros posteriores.
  5. Devolver el acuse de recibo. Incluir { log_seq, anchor_status: "pending", receipt } solo cuando el anexo haya devuelto la tupla completa y exitosa. receipt.sig_b64url está vacía cuando el firmante del recibo no está disponible; los clientes NO DEBEN llamar a ese valor firmado o no repudiable. La ausencia de la tupla significa solo publicación duradera, no aceptación por registro de transparencia.
  6. Usar una tarea diferida para publicar la transacción firmada exacta. El éxito elimina la bandeja de salida; el fallo la deja para el cron de drenaje acotado y no debe cambiar la tx_id ya reconocida. Los metadatos provisionales y otros sidecars de mejor esfuerzo también se posponen.

Límite de latencia. La ruta de la solicitud incluye trabajo de autoridad/cupón de la primera mitad, preparación/firma de transacciones, escrituras duraderas R2 y (cuando está configurado) el intento LogDO. < 300 ms aparece en la revisión de diseño como un objetivo operativo, no como una garantía del protocolo; el paso actual de preparación de transacciones puede realizar una solicitud de metadatos del gateway. Las alarmas de latencia y las puertas de lanzamiento son controles operativos, no evidencia disponible para un verificador.

16.11 Modelo de Confianza — la reclamación precisa, limitada por tipo de hoja

Para kind=0x01 (atestiguado): "Este contenido — cuerpo que coincide con body_hash, identificado por qub_id — fue comprometido en el registro sólo-apéndice de qub en la posición seq y existió a más tardar en el tiempo de bloque de Arweave T; era criptográficamente ilegible hasta la ronda drand R = unlock_round(unlock_at)." Este es el trío completo {tlock round binding + Merkle inclusion + anchored root}.

Para kind=0x02 (afirmado, por defecto): "Un cifrado opaco con dirección de contenido chash, reclamando qub_id y unlock_at, fue comprometido en el registro sólo-apéndice en la posición seq y existió a más tardar en el tiempo de bloque de Arweave T." Las etapas de ronda y cuerpo son proporcionadas por la verificación del paquete §11 .qub existente (qub_core::unlock), no por el registro; lo que el registro añade sobre una simple transacción por qub es orden a prueba de manipulación, un tiempo de compromiso máximo sin necesidad de confianza, y resistencia a la equivocación.

Ambas reclamaciones excluyen, según §11: autoría sin sig_alg ≥ 0x01, intención y sincronización de sub-anclaje. Ninguna permite que alguna reclamación se apoye en received_at.

Techo de reclamación (restricción de lanzamiento vinculante — resuelto en §16.15 P1). Para una hoja afirmada (kind=0x02), la reclamación limitada anterior es el techo de lo que cualquier producto, marketing, términos o superficie de demostración de pruebas puede afirmar. Ninguna superficie puede declarar o implicar que el registro prueba el contenido o la ronda de desbloqueo de una subida ciega por byte — el registro prueba orden + un tiempo de compromiso máximo sin necesidad de confianza de un cifrado opaco. La prueba de contenido y ronda proviene exclusivamente de la verificación del paquete §11 .qub existente, que es independiente del registro. Una publicación sin un apéndice/recibo exitoso no tiene ninguna reclamación de registro.

16.12 Versionado y coordinación W3

No hay un aumento de cable SealedQub y, por lo tanto, no hay un aumento de versión de protocolo (§12.2): el registro es un sidecar que se compromete con campos y bytes existentes, por lo que no entra en el historial de versión de protocolo §12.3. El drand_chain_version opcional de W3 permanece intacto y sigue siendo el único campo SealedQub opcional. En cambio, el registro introduce sus propios espacios de versión independientes — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — reflejando la independencia de versión de wrapper de §12.5 (el wrapper lleva un byte de versión independiente de la versión del protocolo, y las versiones del registro siguen la misma separación).

La entrega de prueba se obtiene por defecto, con un acompañamiento opcional. Una prueba no puede existir en el momento del sellado (el ancla aún no ha sido escrita), por lo que el paquete .qub en el momento del sellado permanece sin prueba. El verificador de W7 obtiene GET …/proof una vez, o en modo completamente offline reconstruye la prueba a partir del AnchorBundle público mediante una consulta en Arweave sobre Log-Id. El paquete .qub (W7) reserva un miembro inclusion_proof opcional — ausente en el sellado, poblado por una reexportación post-ancla para archivo en frío — siguiendo el mismo patrón de "opcional, omitido por defecto, aditivo" que drand_chain_version de W3.

16.13 Retención

Las ventanas de retención para la cola abierta de LogDO, el sustrato de servicio de prueba R2, los contadores de circuito de anclaje y la cola de reserva del empaquetador se especifican en docs/DATA-RETENTION.md. Principio: el almacenamiento caliente por entrada del registro (LogDO) puede ser reclamado después del anclaje; su material de auditoría — la tienda de nodos Merkle con clave de coordenadas (level, index) + los cuerpos de hojas dirigidos por dirección seq (§16.9) + los anclajes de Arweave — es permanente. Reivindicar una hoja fría del DO nunca invalida una prueba emitida, porque una prueba se resuelve contra esa tienda de nodos R2 permanente y el ancla de Arweave, no contra el DO (y el vector de prueba con DO borrado §16.9 lo demuestra).

16.14 Vectores de prueba

W5 incluye el fixture cross-language tlog_v1.json (§16.8) más vectores trabajados: una hoja kind=0x01 y kind=0x02 → leaf_hash; la raíz acumulativa de 5 hojas; una prueba de inclusión; una prueba de consistencia; un AnchorBundle; y un id de DataItem. Estos coexisten con los vectores de outer-wrapper §14.5 y son ejercitados tanto por la implementación en Rust (qub-core) como por la de TypeScript (Worker).

16.15 Decisiones de revisión (W5 — resuelto)

La revisión externa de W5 (una revisión de diseño adversarial + aprobación del propietario) está completa. Cada decisión a continuación está resuelta y reflejada en el texto §16 anterior; las restricciones vinculantes de lanzamiento se reiteran al final. La implementación puede proceder bajo ellas.

  1. Honestidad hoja de ruta por defecto (kind=0x02) — RESUELTO. Enviar la división de dos hojas como se especifica: kind=0x02 no compromete ni body_hash ni drand_round. No hay campo de *_body_hash en la ruta ciega a bytes (sería la señal falsa "verificada" más legible para integradores y es una conveniencia que §11 ya proporciona desde el paquete). No requieren sellado de servidor para qubs atestiguados por registro (eso forzaría el texto plano a través del Worker y destruiría el foso de destrucción de cripto). Cualquier cortocircuito autodescriptivo pertenece al paquete .qub / envolvente de prueba como campo recalculado por verificador, nunca como campo hoja. Techo de reclamación confirmado por el propietario: §16.11.
  2. Responsabilidad por equívoca / omisión — DISEÑO RESUELTO, PROVISIÓN INCOMPLETA. El diseño requiere que la clave de recibo de sello esté fijada en LogProfile y firmada cruzadamente por anchor_owner, además de metodología de monitoreo, caminata prev-chain y cabezas autopublicadas dobles. El perfil compilado y los ganchos de despliegue siguen siendo marcadores/opcionales según se detalla en §16.6, por lo que la afirmación más fuerte detectable + recibido no está vigente hasta que esas puertas cierren. Nunca debe comercializarse como testigo independiente. Un verdadero testigo de terceros se aplaza a un aumento de gobernanza §15.
  3. Raíz de confianza ancla-propietario fijada + rotación — RESUELTO. Adoptar el pin de LogProfile (§16.6); el verificador comprueba anchor_tx.owner == anchor_owner y verifica los datos de transmisión → tx_id vinculación localmente. La gobernanza de rotación es una extensión §15 a la compilación (§15.3 disparador añadido), no una reutilización; las rotaciones planificadas cruzan la señal cruzada, las rotaciones impulsadas por compromiso vuelven al aumento §15 con el daño delimitado de la comprobación de la horquilla.
  4. Cegado de hojas de qub privado — RESUELTO. Mantener cegamiento para qubs privados (ref = SHA3-256(qub_id ‖ log_blind_secret)), qub_id bruto para qubs públicos (ya §16.2.1), chash como el empate independiente. log_blind_secret es un secreto de correlación/grado de Sybil, solo para rotar hacia adelante (§16.2.1).
  5. received_at — RESUELTO. Manténlo en la hoja, comprometido pero explícitamente no probatorio; nunca aparece como prueba o corroboración de disputa en ninguna superficie. Cualquier comprobación de cordura del monitor se compara con la T de tiempo de bloqueo de Arweave, no con la anchored_at controlada por el operador (§16.6).
  6. Temporización demostrable por niveles — RESOLUCIÓN DE DISEÑO, NO ENRUTAMIENTO ACTUAL. El diseño revisado asigna temporización de bloque ancla al nivel agrupado y prueba de hora exacta al T3 pagado, sin SLA numérico para el primero. Las rutas actuales no han establecido esa distinción comercial: programan una transacción individual para cada publicación aceptada, y la cobertura de registros permanece condicional como se indica en §16.1/§16.10. El texto del producto debe describir la implementación, no esta división de nivel futuro.
  7. Árbol acumulativo sobre Workers — RESUELTO. Árbol RFC 9162 acumulativo único + LogDO de escritor único con caching de frontera (margen cómodo frente al techo de ~1k escrituras/seg en DO; aplazar el sharding de Merkle-de-raíces-de-shards hasta cerca de él). El almacén de nodos R2 (level, index) con clave de coordenada + vector de prueba de hoja fría DO borrada están implementados (§16.9). < 300 ms sigue siendo un objetivo de diseño/operacional, no una promesa de protocolo (§16.10).
  8. Esquema de firma ANS-104 + hash profundo — RESUELTO. RSA-PSS (tipo de firma 1, reutilizando la JWK de wallet-ancla dedicada); Ed25519 diferido hacia la ruta PQ de §15. El hash profundo SHA-384 hecho a mano está condicionado por el fixture de cross-impl en ambas direcciones, una verificación de interoperabilidad de empaquetador de referencias solo estático, el viaje de ida y vuelta shared-crypto.subtle, y el monitor de aceptación de Arweave post-empaquetado (§16.8).

Restricciones vinculantes de lanzamiento (llevar a la implementación + revisión de producto/legal):


17. Bundle portátil de verificación (.qub)

Estado. Esta sección está implementada (W7 / UP-C2): qub_core::export produce y analiza el bundle, y tools/qub-verify es una CLI pública y autónoma que lo verifica sin conexión. §11 y §16.9 ya se refieren al «bundle .qub» como la unidad que consume un verificador independiente; esta sección especifica sus bytes y el recorrido de verificación. Es estrictamente aditiva: el bundle empaqueta las entradas existentes de §11 y no modifica ningún formato de transmisión on-chain.

17.1 Propósito

§11 establece que cualquier tercero puede verificar el artefacto criptográfico de un qub sin la cooperación de qub. El bundle .qub hace que esa verificación sea portátil y sin conexión: empaqueta el CBOR sellado y la firma de la ronda de drand que lo desbloquea en un único artefacto autónomo, para que un destinatario pueda verificar la integridad del contenido, la vinculación con la ronda y cualquier firma de autoría sin realizar ninguna llamada de red (sin obtener datos del almacenamiento, sin consultar drand en vivo y sin API de qub). Un bundle por sí solo no demuestra cuándo se creó su texto cifrado; una transacción de almacenamiento verificada de forma independiente o una prueba anclada del registro aporta esa afirmación separada sobre el momento de existencia (§11, §17.5).

17.2 Formato del bundle

Un QubBundle es CBOR canónico escrito a mano conforme al perfil de §3.1 (longitudes definidas, sin tags, sin floats, enteros en la forma más corta, texto NFC, campos opcionales omitidos cuando no están presentes y claves ordenadas primero por la longitud de bytes codificada y después byte a byte). Las tres claves de 15 caracteres se ordenan d < i < s. Un archivo .qub sin procesar consta exactamente de esos bytes; para transportarlo por URL o copiarlo y pegarlo, los mismos bytes se codifican en base64url sin padding.

Clave Long. cod. Tipo Presencia Significado
version 8 u8 obligatoria Versión del formato del bundle (0x01).
sealed_at 10 i64 opcional Hora de sellado afirmada por el creador (segundos Unix); autodescriptiva, sin valor probatorio.
drand_round 12 u64 obligatoria Ronda a la que está bloqueado el qub. Proyección del qub sellado incluido.
arweave_tx_id 14 tstr obligatoria Identificador de la transacción bajo la que se almacenaron los bytes sellados (puntero de procedencia).
drand_chain_id 15 tstr obligatoria Cadena de drand (hexadecimal). Proyección del qub sellado incluido.
drand_signature 16 bstr obligatoria Firma de la baliza drand para drand_round: el valor que desbloquea el texto cifrado.
inclusion_proof 16 bstr opcional Prueba de inclusión Merkle del registro de transparencia de §16, una vez disponible (§17.5).
sealed_qub_cbor 16 bstr obligatoria Bytes internos de SealedQubCbor (después de retirar la envoltura de §13), es decir, la entrada de verificación de §11.

drand_round y drand_chain_id son proyecciones de conveniencia de sealed_qub_cbor, incluidas para que las herramientas puedan leerlas sin analizar el CBOR interno. Se derivan al construir el bundle y se vuelven a comprobar al decodificarlo frente al qub sellado analizado; se rechaza un bundle cuyo campo superior no coincida con su carga útil. La disciplina del codificador refleja la del resto del formato de transmisión: rechazar una drand_signature o un arweave_tx_id vacíos y limitar todos los campos de longitud variable.

17.3 Qué demuestra la firma de drand incluida

El bundle incluye la firma de drand en vez de exigir que el verificador la obtenga. El descifrado timelock (tlock sobre la cadena de drand, §8) solo puede funcionar con la firma genuina de la baliza para la ronda vinculada: un valor que la cadena publica únicamente cuando transcurre esa ronda y que es una firma BLS válida bajo la clave pública de la cadena. Una firma falsificada o equivocada falla en la verificación BLS o en el descifrado IBE/AEAD. Por tanto, un bundle que se descifra demuestra que el texto cifrado está vinculado a la ronda R y la ronda R ya ha transcurrido. El verificador fija la cadena (DrandTimelockProvider::quicknet()) y aplica la comprobación de vinculación con la ronda de §11, por lo que un bundle no puede afirmar una ronda a la que no esté vinculado su texto cifrado.

Esta es una prueba de la condición de revelación, no una marca temporal de creación. Después de que transcurra la ronda R, cualquiera puede crear un texto cifrado nuevo para R y empaquetarlo con su firma, ya pública. Por ello, el bundle por sí solo NO DEBE describirse como prueba de que el texto cifrado o el contenido existieran antes de R, antes de unlock_at ni antes de ningún acontecimiento.

17.4 Recorrido de verificación sin conexión

qub-verify <file.qub> ejecuta el procedimiento estándar de §11 enteramente a partir del bundle, mediante qub_core::unlock::unlock con un DrandTimelockProvider fijado:

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 (verificado), 1 (falló la verificación: aún bloqueado, discrepancia del hash del cuerpo, vinculación de ronda o cadena rota, o una firma que no se verifica) o 2 (uso incorrecto o bundle malformado). Un informe --json contiene los mismos veredictos para la automatización. Como el bundle es autónomo, el crate verificador (qub-core) y la CLI (qub-verify) son el único software que necesita un tercero; ambos son públicos y reutilizan la ruta de verificación ya existente del protocolo, sin criptografía ad hoc.

17.5 Relación con el registro de transparencia

inclusion_proof es un campo opcional para la prueba de inclusión Merkle de §16. La verificación exclusiva del bundle (§17.4) es completa para la integridad, la vinculación y transcurso de la ronda y la autoría opcional, pero deliberadamente no aporta ninguna afirmación de existencia con marca temporal independiente. Una inclusion_proof presente y verificada por completo hasta su anclaje añade el compromiso específico del tipo de hoja y el límite superior temporal de §16.11 sin cambiar la versión del formato del bundle. Una prueba ausente solo significa «no se incluyó una prueba», no «inválido» ni necesariamente «no anclado».

En la implementación de referencia, el campo ya está tipado: qub_core::export::QubBundle::inclusion_proof_typed() devuelve un Option<InclusionProof> que transporta toda la estructura de §16.9 (hoja, ruta de auditoría, raíz anclada y AnchorRef) mediante el mismo campo CBOR opaco, sin cambiar la versión del formato del bundle. La CLI independiente qub-verify la consume mediante su rama --anchor y, hasta que se aprovisione la billetera de anclaje (§16, Estado), informa una prueba presente pero con propietario marcador como solo inclusión, no como anclada-verificada por completo.