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:
- Cambiar cualquier campo vinculado por la preimagen —
version,content_type,created_at,unlock_at,outcome_at,drand_round, los bytes sin procesar debody(mediantebody_hash) otitle(mediantetitle_hash)— produce unqub_iddistinto. - El qub_id se calcula antes del cifrado. Tanto el QubEnvelope como el SealedQub portan el mismo qub_id. El visor verifica que coincidan después del descifrado.
qub_idno depende desender_label,reply_to, los bytes de firma ni las claves públicas de firma. Sin embargo, con la construcción de firma V2 actual,sender_labelyreply_tose autentican directamente mediantesender_label_hashyreply_to_or_zero(§9.3) siempre que haya firmas.- Cambiar el
titledel SealedQub (con todo lo demás fijo) cambia elqub_idmediantetitle_hash. Por lo tanto, un gateway no puede sustituir el título en plaintext mostrado en la cuenta atrás sin invalidar la identidad del qub. - Cambiar el
outcome_atdel SealedQub (con todo lo demás fijo) cambia elqub_idmediante la preimagen. Un gateway no puede sustituir la fecha de verdict-on previa a la revelación mostrada en la cuenta atrás sin invalidar la identidad del qub. - Cambiar
drand_round(con todo lo demás fijo) cambia elqub_idmediante la preimagen. Un gateway no puede revincular el ciphertext del timelock a un round distinto sin invalidar la identidad del qub; combinado con la comprobación del round de la stanza al momento del unlock (§8), elunlock_atmostrado es el round que realmente controla el descifrado.
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:
- Solo HTTPS. La cadena DEBE empezar con la secuencia de bytes
https://. Cualquier otro esquema —http,ftp,javascript,data,file, etc. — se rechaza. - Tope de longitud. ≤ 2.048 bytes (límite práctico de URL en navegadores).
- Verificación NFC + de codepoints hostiles. Misma regla que para
titleyreflection— los codepoints bidi-override / zero-width / tag-block / BOM / C0 / C1 se rechazan. La definición coincide con la Rustcrate::handle::contains_hostile_text_codepointy la TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(mantenlas sincronizadas). - Sin espacios en blanco, sin controles ASCII. Cualquier espacio en blanco / DEL / byte sub-
0x20en cualquier parte de la URL se rechaza — cierra el vector de inyección\n/\tque la regla bidi no cubre. - 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.
- Bajo la preimagen V1 retirada, una parte con acceso de escritura a los bytes almacenados podía sustituir
sender_label(«Alice» → «Mallory») o reemparentarreply_to— y volver a cifrar tras el round — sin invalidar la firma del autor, porque ninguno de los dos campos estaba en la preimagen firmada. V2 cubre ambos, así que cualquier cambio en cualquiera de los dos campos hace que la verificación pase a «fallida». Como los verificadores ahora aceptan únicamente V2, esta sustitución queda cerrada para toda firma: una firma que no vincula ninguno de los dos campos (es decir, que solo se verifica contra V1) se rechaza de plano en lugar de aceptarse como degradación. - La
author_pubkeydentro del envelope sigue siendo el ancla de identidad verdadera — los visores DEBEN derivar la identidad de visualización a partir deauthor_pubkey(a través de la capa de attestation §9.5) en lugar de confiar ensender_label.
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:
cosigner_pubkey: clave pública ML-DSA-65 del cofirmante (Party B).cosigner_signature: firma sobre el mismosig_inputque el autor (§9.3).
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:
- El cosigner firma el mismo
sig_inputidéntico que el autor — ambas partes se comprometen al mismoqub_id,body_hashyunlock_at(y, bajo V2, al mismosender_label_hashyreply_to_or_zero). - Para que el cofirmante pueda reconstruir la preimagen V2 sin acceso a los bytes crudos del envelope, el servicio de staging exige en el momento del staging que el
sender_labelde un envelope de pact sea igual apact_terms.party_a.labely quereply_toesté ausente. Ambas condiciones se cumplen en todo pact de cliente de referencia; los envelopes que las violen se rechazan en el staging. - La derivación de
qub_id(§4.1) NO incluye campos del cosigner. Añadir un cosigner a un envelope existente no cambia elqub_id. - Un pact puede estar firmado solo por el autor (compromiso unilateral), solo por el cosigner (inusual) o por ambos (prueba bilateral completa).
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
- Encabezados: de
#a####(no#####ni######) - Énfasis: negrita (
**), itálica (*), tachado (~~) - Listas: ordenadas (
1.) y desordenadas (-,*) - Citas en bloque (
>) - Código: spans inline (```) y bloques con vallas (`````)
- Reglas horizontales (
---) - Saltos de línea (dos espacios al final o línea en blanco)
- Párrafos
10.2 Elementos prohibidos
| Elemento | Tratamiento |
|---|---|
HTML crudo (<div>, <script>, etc.) |
Eliminado por completo. Ningún HTML pasa. |
Imágenes () |
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:
- Parsear Markdown usando
pulldown-cmark(o equivalente). - Recorrer el AST y descartar cualquier nodo que no esté en la allowlist (§10.1).
- Para nodos de enlace: emitir la URL como texto visible, no como elemento
<a>clickable. - Convertir el AST filtrado en una representación intermedia tipada (p. ej., un enum
MarkdownNodecon solo variantes seguras). El HTML crudo es estructuralmente irrepresentable en este IR. - 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
innerHTMLen 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
- Profundidad máxima de encabezado renderizado:
####(H4).#####y más profundos se renderizan como texto en negrita. - Sin límite en el número de párrafos (los límites de tamaño del cuerpo en §6 son la restricción).
- Bloques de código con vallas: sin syntax highlighting en MVP. Renderizados como texto preformateado monoespaciado.
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>.
- PATCH: corrección de precisión o editorial que no cambia los bytes conformes ni el comportamiento obligatorio.
- MINOR: adición normativa compatible con versiones anteriores, nueva entrada del registro o nuevo formato auxiliar versionado de forma independiente.
- MAJOR: cambio normativo incompatible, incluida una nueva interpretación obligatoria del formato de transmisión.
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.
- Los visores DEBEN rechazar versiones mayores desconocidas con un error claro.
- Dentro de una versión mayor conocida, los decodificadores DEBEN rechazar las claves de map desconocidas (§3.1) — la evolución del esquema se hace introduciendo una nueva
version, no añadiendo claves que los decodificadores existentes omitirían. (Revisiones anteriores de esta especificación permitían tolerar campos opcionales desconocidos; esa cláusula queda retirada — hacía queencode(decode(x))no fuera inyectiva y abría un vector de contenido firmado oculto en los payloads de pactos.) - Los tipos de contenido (
content_type) y los esquemas de firma (sig_alg) están sujetos a versión: solo pueden introducirse nuevos valores junto con una nueva versión del protocolo o una actualización explícita del registro.
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:
- Resistencia a la enumeración para la entrega privada.
OuterWrappersigue siendo CBOR estructurado reconocible —no es literalmente indistinguible de bytes aleatorios—, pero su campo de texto cifrado oculta la forma reconocible delSealedQubinterno. La estrategia documentada de «consultar mediante GraphQL las cargas sin envoltura con forma de qub y descifrarlas en masa con firmas públicas de drand» no produce texto plano sin K. - Postura de privacidad por crypto-shredding para el flujo privado predeterminado del navegador. qub.social no puede descifrar esos artefactos almacenados a partir de sus datos predeterminados del lado del servidor. La recuperación explícita, la entrega pública y el sellado de confianza del lado del servidor tienen otros límites de confianza ya descritos.
- Escalera de confidencialidad de dos niveles. Por defecto = acceso controlado por enlace (esta sección). Los qubs privados cifrados al destinatario (una función reservada para la fase 2, aún sin especificar) se apilan encima como segundo nivel.
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.
versionDEBE ser igual a0x01para los bytes de wrapper v1.0.qub_idDEBE ser igual al campoqub_iddel SealedQub recuperado tras desenvolverlo. Tantowrap_sealed_qubcomounwrap_sealed_qubde referencia analizan el CBOR interno y comprueban directamente esta igualdad; por separado, la vinculación AAD hace que cualquier manipulación posterior delqub_idexterno falle en la autenticación.nonceDEBE ser de 96 bits (12 bytes), generado de nuevo por un CSPRNG en cada operación de wrap. Reutilizar un nonce bajo la misma clave permite ataques de reutilización de nonce AEAD que recuperan el plaintext; los productores DEBEN tratar los pares (key,nonce) como de un solo uso.ciphertextes la salida de AES-256-GCM: bytes de ciphertext concatenados con el tag de autenticación de 16 bytes.ciphertext.len() == SealedQubCbor.len() + 16exactamente.
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:
- WASM creador:
getrandom(WebCrypto bajo el backendwasm_js). - Cliente de la API de sellado del lado del servidor: su CSPRNG local; el cliente proporciona y conserva
Kcomowrapper_key_b64url. El Worker usaKen memoria para el wrapper pero NO DEBE persistirla. Esto permite que un reintento idempotente recupere una respuesta con los valores sensibles omitidos usando la capacidad que el cliente conserva, en lugar de depender de un secreto de un solo uso generado por el servidor.
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
- La firma de autoría (§9) no cambia: las firmas se computan dentro del
QubEnvelopeinterno y se recuperan tras el unwrap → tlock decrypt → parseo CBOR. - El cifrado con clave pública del destinatario (el campo reservado
recipient_pubkey) es una función futura distinta del modo privado actual, protegido por la capacidad del enlace mediante la envoltura. - El flujo actual de cofirma de pactos del lado del servidor emite
SealedQubCborpúblico sin envoltura con visibilidad0x01; no puede cumplir el modelo de secreto de K exclusivo del navegador porque el sellado final ocurre después de la cofirma mediada por el servidor. Un futuro productor de pactos privados podrá usar la misma envoltura, que es ciega al tipo de contenido interno.
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:
- Sin inmunidad a la enumeración. Los qubs públicos renuncian por construcción a la resistencia a la enumeración de §13.1. El servicio de carga de referencia les añade un tag de almacenamiento permanente
Visibility: public(y solo a ellos) para que puedan encontrarse de forma intencionada; los qubs privados no llevan ese tag y conservan la confidencialidad de su cuerpo mediante la envoltura. - Título en plaintext expuesto en el momento del sellado. El campo
titlede §3.2 está en plaintext dentro delSealedQubCbor. Bajo el wrapper queda oculto hasta que un visor aportaK; sin el wrapper es legible por cualquiera en el almacenamiento permanente desde el momento de la carga, antes del unlock. Las aplicaciones creadoras conformes DEBEN divulgar esto en el momento del sellado. - La detección es estructural y se comprueba de forma cruzada. Un visor o incrustación conforme distingue las dos formas al analizarlas: los bytes que se interpretan como
OuterWrappersiguen la ruta de desenvuelto conK; los bytes que se interpretan comoSealedQubCborsin envoltura se aceptan directamente. El valor interno recuperado DEBE coincidir (0x00para envuelto/privado,0x01para sin envoltura/público).qub_idno vincula la visibilidad, pero los bytes canónicos deSealedQubsí la contienen, por lo que las codificaciones internas pública y privada no son idénticas byte por byte.
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:
- Rust:
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors). - TypeScript:
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test).
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:
- Firma: ML-DSA-65 (
sig_alg = 0x01; clave pública de 1.952 bytes, firma de 3.309 bytes) y sin firma (sig_alg = 0x00). El código reserva0x02para Ed25519, pero el protocolo v1 no lo activa; un verificador v1 DEBE rechazar todosig_algfuera de{0x00, 0x01}. - Timelock: únicamente drand quicknet — el hash de la chain, la clave pública, el tiempo de génesis y el período son parámetros de red fijos transportados por la implementación de referencia
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) yconfig/drand-endpoints.json. - Wrapper externo: únicamente AES-256-GCM v1 (§13).
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:
- Un segundo byte
sig_alg(activación de Ed25519, ML-DSA-87 o cualquier nueva entrada en el registro de §9). - Una segunda chain de drand en uso en producción.
- Una segunda versión del wrapper externo.
- Una rotación de la raíz de confianza del registro de transparencia: la dirección
LogProfile.anchor_ownero la clave pública fijada para los recibos (§16.6).LogProfilese incorpora a la superficie del perfil de §15.2 como primitiva gobernada: una rotación es un incremento firmado deLogProfiledistribuido en una actualización del verificador (las rotaciones planificadas firman de forma cruzada de la raíz saliente a la entrante; las motivadas por compromiso no pueden hacerlo y dependen de este incremento, con la comprobación de bifurcación del anclaje anterior limitando el daño intermedio). Los espacios de versión del registro de transparencia (LOG_VERSION,ANCHOR_FORMAT) evolucionan como hermanos independientes, igual que la versión de la envoltura de §12.5 es independiente de la versión del protocolo.
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/), elLogDOde 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 pruebaGET /api/v1/qub/:tx_id/proof(inclusión) yGET /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/uploadexitoso siempre es duradero por R2, pero solo se cubre log-deck cuandoLOG_DOestá configurado y la adición en línea tiene éxito; solo entonces su respuesta llevalog_seq,receiptyanchor_status. SiRECEIPT_SKestá ausente o es inválida, elsig_b64urlde ese recibo está vacío y no proporciona no repudiación. Las rutas actuales de publicación de/sealy 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/uploadtras 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_ownersigue siendo el marcador de[0xAB; 32]posición); (b) la clave de firma de recibo y el PIN de clave pública correspondiente (RECEIPT_SKes opcional yLogProfile.receipt_pubkeyestá 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 deSealedQub/QubEnvelopecable.
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):
- 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_b64urlno 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. - Metodología de monitor publicada + recorrido prev-chain — el ancla
prevcadena es head→genesis caminada; una bifurcación (dos anclajes en unsizeconrootdiferente, o unprevroto) es prueba publicable de mala conducta. La detección de equivocación es un compromiso operativo declarado, no una suposición silenciosa. - 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 ganchopublishHeaden el cron ancla (workers/api/src/utils/heads-publish.ts): unPUTa la API de contenidos sinshaes solo añadido (un422significa que el encabezado ya está publicado, nunca se sobrescribe); opt-in / desplegable bloqueado enPUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO}e inerte hasta que el repositorio esté provisionado. Un hard GitHub que falla páginas a través del canalhealth_alerty 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):
- El
tlog_v1.jsonde fijación multi-lenguaje (Rust + TS, el patrónwrapper_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). - 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).
- La ruta deep-hash + RSA-PSS debe recorrer los mismos
crypto.subtleprimitivas usa en producción, por lo que el codificador interno es compatible con bytes. - 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:
- Puertas de la mitad frontal (autenticación, validación, clave de fragmento de idempotencia) — sin cambios.
- Crear, etiquetar y firmar la transacción Arweave individual exacta. Esto se obtiene
tx_idlocalmente, 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. - 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. - Cuando
LOG_DOestá configurado, intento sincrónicoLogDO.append(leaf). El único escritor asignaseq, 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 sinlog_seq,receiptoanchor_status. A pesar de un comentario de implementación, hoy no se conecta con cable una conciliación automática de registros posteriores. - 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_b64urlestá 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. - 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_idya 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.
- Honestidad hoja de ruta por defecto (
kind=0x02) — RESUELTO. Enviar la división de dos hojas como se especifica:kind=0x02no compromete nibody_hashnidrand_round. No hay campo de*_body_hashen 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. - 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
LogProfiley firmada cruzadamente poranchor_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. - Raíz de confianza ancla-propietario fijada + rotación — RESUELTO. Adoptar el pin de
LogProfile(§16.6); el verificador compruebaanchor_tx.owner == anchor_ownery 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. - Cegado de hojas de qub privado — RESUELTO. Mantener cegamiento para qubs privados (
ref = SHA3-256(qub_id ‖ log_blind_secret)),qub_idbruto para qubs públicos (ya §16.2.1),chashcomo el empate independiente.log_blind_secretes un secreto de correlación/grado de Sybil, solo para rotar hacia adelante (§16.2.1). 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 laTde tiempo de bloqueo de Arweave, no con laanchored_atcontrolada por el operador (§16.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.
- Á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 mssigue siendo un objetivo de diseño/operacional, no una promesa de protocolo (§16.10). - 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):
- Techo de reclamo (Q1/Q6). Ninguna superficie puede decir el registro prueba el contenido de una hoja afirmada o desbloquear la ronda; el reclamo permitido para una hoja
kind=0x02anclada con éxito es ordenada, con evidencias de manipulación, con un tiempo máximo de compromiso sin confianza. Una respuesta sin una tupla de recibo no tiene reclamo de registro. Ninguna copia de marca temporal lleva una garantía de latencia numérica. - Honestidad del testigo (Q2). Equivocación de mercado como detectable + recibida, nunca independientemente presenciada.
- Claves de recibo + ancla (Q2/Q8). Antes de que se envíen los reclamos de no repudio/verificación anclada, provisionar y compilar-fijar la clave pública de recibo, firmarla cruzadamente con el propietario de ancla provisionado y mantener la cartera del ancla como su propio JWK distinto de la cartera de subida.
- Puerta de hash profundo (Q8). Ningún ancla o transacción T3 se envía hasta que pasen la verificación de fixture en ambas direcciones + interoperabilidad; el monitor de aceptación alerta ante fallo.
- Precondición de almacenamiento (Q7). El almacén de nodos con clave coordinada + vector de hojas frías DO borradas son prerrequisitos para la garantía de "la reclamación nunca invalida una prueba".
17. Bundle portátil de verificación (.qub)
Estado. Esta sección está implementada (W7 / UP-C2):
qub_core::exportproduce y analiza el bundle, ytools/qub-verifyes 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.