Especificación del protocolo de qub
qub es un protocolo de compromisos temporales criptográficos: un sistema para sellar palabras a una fecha futura y demostrar, cuando esa fecha llegue, exactamente qué se dijo y cuándo.
Tres primitivas lo hacen posible. drand es una baliza de aleatoriedad descentralizada — la fecha de revelación se hace cumplir por la física, no por la buena voluntad de ninguna parte. El almacenamiento público permanente es un almacén público a prueba de manipulaciones — ninguna parte puede editar o eliminar un qub una vez sellado. ML-DSA-65 es una firma digital post-cuántica — cada qub está 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, a prueba de manipulaciones y atribuible — 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 | 1.0 (versión del protocolo 0x01, versión del wrapper externo 0x01) |
| Fecha | 2026-05-01 |
| Estado | Borrador |
| Revisado hasta | 2026-05-01 |
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], // Aleatorio, generado localmente
created_at: i64, // Segundos Unix UTC
unlock_at: Option<i64>, // Segundos Unix UTC; None mientras se compone
visibility: u8, // 0x01 = público (único valor en MVP)
content_type: u8, // 0x01 = texto (único valor en MVP)
plaintext: Vec<u8>, // Cuerpo del qub UTF-8
sender_label: Option<String>, // Nombre decorativo; no autenticado
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, // Versión mayor del protocolo (0x01 para v1)
qub_id: [u8; 32], // Derivado (ver §4.1)
content_type: u8, // Registro de tipos de contenido (ver §6)
created_at: i64, // Segundos Unix UTC
unlock_at: i64, // Segundos Unix UTC
outcome_at: Option<i64>, // V1.1 — cuando la realidad emite su veredicto (verdict-uplift-plan §3.1)
sender_label: Option<String>, // Decorativo; no autenticado en MVP
reply_to: Option<[u8; 32]>,// qub_id padre para cadenas de respuesta; no en la preimagen de qub_id; no firmado (ver §9.3)
body: Vec<u8>, // Carga útil del contenido (UTF-8 para texto, CBOR para pact)
body_hash: [u8; 32], // SHA3-256(body) (ver §4.2)
sig_alg: u8, // Algoritmo de firma (ver §9.2)
author_signature: Option<Vec<u8>>, // Presente cuando sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Presente cuando sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Presente para acuerdos bilaterales pact con cofirma
cosigner_signature: Option<Vec<u8>>, // Presente para acuerdos bilaterales pact con cofirma
}
Línea base (qub de texto sin firma): version = 0x01, content_type = 0x01, sig_alg = 0x00, todos los campos Option ausentes.
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 usando CBOR canónico (§3). Cargado al almacenamiento permanente. Es el artefacto on-chain.
SealedQub {
version: u8, // Versión mayor del protocolo (0x01 para v1)
qub_id: [u8; 32], // Igual que QubEnvelope.qub_id
visibility: u8, // 0x01 = público; los visores v1 rechazan otros valores
unlock_at: i64, // Segundos Unix UTC
outcome_at: Option<i64>, // V1.1 — visible en el CTA de verdict-watch
// antes de la revelación; refleja QubEnvelope.outcome_at;
// vinculado a qub_id mediante la preimagen de §4.1.
drand_chain_id: String, // Hash de la cadena drand (cadena hex)
drand_round: u64, // Número de round drand objetivo
tlock_ciphertext: Vec<u8>, // Bytes CBOR del QubEnvelope cifrados con tlock
recipient_pubkey: Option<[u8; 32]>,// Campo reservado; aceptado por el CBOR canónico
// pero no interpretado por el visor de referencia v1
title: Option<String>, // Título en plaintext mostrado en la cuenta atrás
// del visor antes de la revelación. Vinculado a qub_id
// vía title_hash (§4.1). 1..=100 code points NFC,
// sin caracteres de control.
}
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>, // V1.1 — trasladado desde QubEnvelope.outcome_at / SealedQub.outcome_at; impulsa el bloque verdict-watch de la página de revelación (verdict-uplift-plan §5.1)
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Trasladado desde 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) ← solo si está presente (cadenas de respuesta)
"body_hash" (10 encoded bytes)
"unlock_at" (10 encoded bytes)
"created_at" (11 encoded bytes)
"outcome_at" (11 encoded bytes) ← solo si está presente (mecánica verdict V1.1)
"content_type" (13 encoded bytes)
"sender_label" (13 encoded bytes) ← solo si está presente
"author_pubkey" (14 encoded bytes) ← solo si está presente
"cosigner_pubkey" (16 encoded bytes) ← solo si está presente (cofirma de pact)
"author_signature" (17 encoded bytes) ← solo si está presente
"cosigner_signature" (19 encoded bytes) ← solo si está presente (cofirma de pact)
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) ← solo si está presente
"qub_id" (7 encoded bytes)
"version" (8 encoded bytes)
"unlock_at" (10 encoded bytes)
"outcome_at" (11 encoded bytes) ← solo si está presente (mecánica verdict V1.1)
"visibility" (11 encoded bytes)
"drand_round" (12 encoded bytes)
"drand_chain_id" (15 encoded bytes)
"recipient_pubkey" (17 encoded bytes) ← solo si está presente
"tlock_ciphertext" (17 encoded bytes)
PactTerms (cuerpo pact, content_type 0x03):
"notes" (6 encoded bytes) ← solo si está presente
"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) ← solo si está presente
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" || // separador de dominio: bytes ASCII [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + relleno 0x00 (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 cuando outcome_at está ausente)
drand_round || // u64 big-endian (8 bytes)
body_hash || // [u8; 32] (32 bytes)
title_hash // [u8; 32] (32 bytes; centinela-ausente = [0u8; 32])
)
// Preimagen total: 108 bytes → salida de 32 bytes
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: V1.1 amplió la preimagen de 92 a 100 bytes para incorporar el campo opcional outcome_at al vínculo criptográfico. 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 cable) y el documento interno tasks/verdict-uplift-plan.md para la mecánica verdict que motiva este campo.
Codificación de drand_round: V1.2 amplió la preimagen de 100 a 108 bytes para incorporar drand_round (el round drand objetivo, §4.3) al vínculo criptográfico, y elevó el separador de dominio a QUB_ID_V2. Esto vincula el round del timelock a la identidad del qub: un gateway no puede revincular el ciphertext a un round distinto (por ejemplo, ya pasado) del que implica el unlock_at mostrado. El procedimiento de unlock (§8) verifica además que el round incrustado en la stanza del ciphertext tlock coincide con unlock_round(unlock_at), de modo que la hora de unlock mostrada es demostrablemente el round que controla el descifrado.
Propiedades:
- Cambiar cualquier campo del QubEnvelope (body, marcas de tiempo, tipo de contenido, versión) produce un qub_id diferente.
- 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_id no depende de
sender_label,author_signatureniauthor_pubkey. Esto significa que el mismo contenido sellado en el mismo momento produce el mismo qub_id sin importar quién lo firme. - 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) si title está presente
title_hash = [0u8; 32] si title está ausente
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 = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| 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 |
La operación ceil() selecciona el primer round drand cuya hora de revelación sea ≥ unlock_at. Esto garantiza que el qub no se vuelva descifrable antes del momento de unlock elegido.
Caso límite: si (unlock_at - chain_genesis_time) es exactamente divisible entre chain_period_seconds, el resultado es ese round exacto — el qub se desbloquea precisamente en la hora de revelación de ese round.
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() |
Carga al almacenamiento permanente, fetch del 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 para structured/v1
title: String, // ≤ 200 bytes, NFC
terms: Vec<PactTerm>, // ≤ 20 filas
party_a: PartyIdentifier, // iniciador
party_b: PartyIdentifier, // cofirmante
notes: Option<String>, // ≤ 5.000 bytes, NFC; clave ausente si no hay
}
PactTerm { key: String (≤ 100), value: String (≤ 2,000) } // NFC en ambos lados
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }
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 para structured/v1
outcome: u8, // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
reflection: Option<String>, // ≤ 2.000 bytes NFC; "qué cambió, qué aprendiste"
evidence_url: Option<String>, // ≤ 2.048 bytes; solo HTTPS; clave ausente cuando se omite
}
Orden canónico de claves CBOR:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← solo si está presente
"evidence_url" (13 encoded bytes) ← solo si está presente
"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. El usuario compone plaintext y metadatos en ComposeQub.
2. Validar:
a. body no está vacío.
b. tamaño del body ≤ máximo para content_type y nivel del usuario (ver §6).
c. unlock_at está en el futuro.
d. unlock_at ≤ created_at + 10 años.
e. content_type es un valor conocido y soportado.
3. Calcular body_hash = SHA3-256(body).
4. Establecer created_at = segundos Unix UTC actuales.
5. Seleccionar cadena drand. Cargar chain_genesis_time y chain_period_seconds, y
calcular drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
(Se calcula aquí, antes de qub_id, porque drand_round se vincula a la preimagen
de qub_id — §4.1, V1.2.)
6. Calcular qub_id (ver §4.1), incorporando drand_round del paso 5.
7. Construir QubEnvelope con todos los campos.
8. Serializar QubEnvelope usando CBOR canónico → bytes B.
Aserción: la salida serializada coincide con el perfil canónico (§3).
9. Calcular C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construir SealedQub con tlock_ciphertext = C, y qub_id, version,
unlock_at, drand_chain_id, drand_round coincidentes.
12. Serializar SealedQub usando CBOR canónico → SealedQubCbor.
12a. Generar K = 32 bytes aleatorios (CSPRNG) y N = 12 bytes aleatorios (CSPRNG).
Calcular W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
según §13. Los bytes cargados a almacenamiento permanente son el CBOR del OuterWrapper W,
nunca el SealedQubCbor pelado. K abandona el dispositivo solo como
fragmento de URL en el paso 16.
13. Mostrar la divulgación al momento del sellado. El usuario confirma.
14. Validar la elegibilidad de carga vía el servicio de carga de qubs (detección de bots, entitlement, límites de tasa).
15. Enviar W (los bytes del OuterWrapper) al servicio de carga de qubs; el servicio
firma y carga a almacenamiento permanente. El servicio es ciego a los bytes del SealedQubCbor
interno y nunca recibe K.
16. Recibir arweave_tx_id del servicio. Construir la URL de entrega como
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (o `<origin>/s/<short_code>#<base64url(K)>`
cuando se asigna un código corto). Los navegadores no transmiten los fragmentos
de URL a los servidores, por lo que K nunca es observada por qub.social ni por
ningún gateway de almacenamiento.
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 el payload envuelto. Content-Type=application/octet-stream es normativamente requerido. El servicio de referencia adjunta adicionalmente tres tags opcionales cuando el creador elige exponerlos: Intent (intención de composición validada por allowlist — p. ej., quote, reply, commitment), Author (fingerprint de la pubkey §9.3 del creador como hex en minúsculas de 64 caracteres) y Parent-Tx-Id (ID de transacción de almacenamiento del qub padre para cadenas de respuesta, base64url de 43 caracteres).
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 por defecto — no se escribe ningún tag Author y el qub queda sin atribución en la cadena: nada en el almacenamiento permanente vincula la carga con el handle, 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 attestation §9.5. Las relaciones de cadena de respuesta y el Intent no son identificativos. El wrapper externo (§13) protege el cuerpo interno de la correlación de ciphertext — impidiendo que un harvester reconozca y descifre en masa cargas con forma de qub después de que su round drand se publique.
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. El visor abre la URL de entrega. Extraer arweave_tx_id de la ruta Y
K = base64url_decode(fragment) del fragmento de URL. Si el fragmento
está ausente o malformado → mostrar "esta URL no tiene su clave de
descifrado" y detener; el visor NO DEBE contactar al gateway de almacenamiento
sin K, ya que obtener bytes envueltos que el visor no puede descifrar
no sirve para nada y solo filtra el intento de acceso.
2. Comprobar la denylist. Si tx_id está en la denylist → mostrar mensaje de bloqueo. Detener.
3. Obtener los bytes del OuterWrapper de almacenamiento permanente (con fallback multi-gateway).
3a. Desenvolver: parsear los bytes como OuterWrapper (§13), verificar que
el byte `version` del wrapper sea `0x01`, y calcular SealedQubCbor =
unwrap_sealed_qub(OuterWrapper, key=K). Cualquier fallo de autenticación
AEAD (K incorrecta, ciphertext manipulado, qub_id-como-AAD intercambiado,
nonce intercambiado) → mostrar "la clave de descifrado de esta URL no
coincide con el qub almacenado" y detener. Los fallos de autenticación
son indistinguibles para el visor según §13.5.
4. Parsear SealedQubCbor → SealedQub.
5. Validar: SealedQub.version es conocida (0x01). Rechazar versiones desconocidas.
6. Si la hora actual < SealedQub.unlock_at → mostrar cuenta atrás. Hacer poll o esperar.
6a. Comprobación de vinculación de round (V1.2). Recalcular expected_round =
ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
Rechazar a menos que SealedQub.drand_round == expected_round Y el round incrustado
en la stanza del ciphertext tlock (leído vía la cabecera age/tlock, sin requerir
firma) == expected_round. El round de la stanza es el que realmente controla el
descifrado; sin esta comprobación, un creador malicioso podría vincular el ciphertext
a un round ya pasado mientras muestra una cuenta atrás futura, de modo que cualquiera
que lea los bytes almacenados podría descifrar antes de unlock_at. Las implementaciones
sin identidad de cadena (mocks de prueba) omiten esta comprobación.
7. Una vez que la hora actual ≥ SealedQub.unlock_at:
a. Obtener la firma del round drand para SealedQub.drand_round de la red drand.
b. Calcular B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
8. Parsear B → QubEnvelope.
9. Validar que QubEnvelope.version es conocida.
10. Verificar: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
Falla → error de integridad.
11. Verificar: QubEnvelope.qub_id == SealedQub.qub_id.
Falla → error de integridad.
12. Verificar: QubEnvelope.unlock_at == SealedQub.unlock_at.
Falla → error de integridad.
13. Verificar: QubEnvelope.content_type es conocido y renderizable.
Valores conocidos: 0x01 (texto), 0x03 (pact). Desconocido → mostrar error.
14. Si QubEnvelope.sig_alg != 0x00 → verificar la firma del autor (ver §9.4).
15. Si cosigner_pubkey o cosigner_signature están presentes → verificar el cosigner (ver §9.7).
16. Renderizar el contenido usando el renderer apropiado (ver §10 para texto, §6 para pact).
17. Construir RevealedQub para mostrar.
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 |
|---|---|---|---|
0x00 |
Sin firma (unsigned) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1.952 bytes | 3.309 bytes |
Los visores DEBEN rechazar valores sig_alg desconocidos.
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" || // separador de dominio (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): DEBE ser 0x00 en v1.x
sender_label_hash || // [u8; 32]: SHA3-256(NFC(sender_label)),
// o 32 bytes a cero cuando está ausente
reply_to_or_zero // [u8; 32]: qub_id padre, o 32 bytes a
// cero cuando está ausente
)
// Preimagen total: 155 bytes → hash de 32 bytes
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" || // separador de dominio (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): DEBE ser 0x00 en v1.0
)
// Preimagen total: 91 bytes → hash de 32 bytes
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 (V1.2) |
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. Leer sig_alg del QubEnvelope.
2. Si sig_alg == 0x00 → sin firma. Sin verificación. Mostrar "qub sin firma."
3. Si sig_alg es desconocido → rechazar. Mostrar "esquema de firma no reconocido."
4. Extraer author_signature y author_pubkey. Si alguna está ausente → error de integridad.
5. Reconstruir sig_input usando los campos del QubEnvelope (fórmula V2, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). La preimagen V2 es la
única forma aceptada — el fallback V1 heredado está retirado (§9.3), de modo
que una firma que no se verifica contra V2 falla, y punto.
7. Si la verificación tiene éxito → mostrar "firmado por [fingerprint de la clave]."
8. Si la verificación falla → mostrar "verificación de firma fallida."
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. Si cosigner_pubkey ausente y cosigner_signature ausente → sin cosigner. Hecho.
2. Si exactamente uno está presente → error de integridad.
3. Verificar cosigner_pubkey != author_pubkey (impedir auto-cofirma).
Falla → mostrar "la pubkey del cosigner debe diferir del autor."
4. Reconstruir sig_input usando la misma fórmula que §9.3 (solo V2 — el fallback
V1 heredado está retirado; todos los clientes pact producen firmas V2).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Éxito → mostrar "cofirmado por [fingerprint del cosigner]."
7. Falla → mostrar "verificación de cofirma fallida."
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 puede verificar un qub público sin la cooperación de qub. El procedimiento de verificación:
1. Obtener arweave_tx_id (de la URL de entrega o por conocimiento directo).
2. Obtener SealedQubCbor de cualquier gateway de almacenamiento.
3. Confirmar la inclusión en el bloque de almacenamiento (altura del bloque, marca de tiempo del bloque).
4. Parsear SealedQubCbor → SealedQub.
5. Obtener la firma del round drand para SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → bytes CBOR del QubEnvelope.
7. Parsear → QubEnvelope.
8. Verificar SHA3-256(body) == body_hash.
9. Verificar QubEnvelope.qub_id == SealedQub.qub_id.
10. Verificar QubEnvelope.unlock_at == SealedQub.unlock_at.
11. Si sig_alg != 0x00: verificar author_signature (ver §9.4).
12. Todas las comprobaciones pasan → el qub está verificado.
Qué prueba la verificación:
| Prueba | Qué establece |
|---|---|
| Compromiso | El ciphertext existía en la marca de tiempo del bloque de almacenamiento. |
| Integridad | El cuerpo plaintext coincide con el hash comprometido y no ha sido alterado. |
| Temporalidad | El contenido era ilegible hasta el round drand, que corresponde al momento de unlock elegido (sujeto a las suposiciones de seguridad de tlock y drand). |
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. |
| Temporalidad pre-evento | La inclusión en el bloque de almacenamiento puede ir retrasada respecto a la carga real por minutos. La marca de tiempo del compromiso es la hora del bloque, no el momento en que el usuario pulsó "sellar." |
12. Versionado
12.1 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.2 Historial de versiones
| Versión | Valor | Descripción |
|---|---|---|
| v1 | 0x01 |
qubs de texto público (content_type 0x01), acuerdos bilaterales pact (0x03, esquema structured/v1, autor + cosigner ML-DSA-65), tlock, SHA3-256 |
12.3 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.4 Versión del wrapper externo
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 |
v1 por defecto |
| — | 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.
El wrapper externo de cifrado cierra ese canal interponiendo una capa simétrica AEAD adicional entre el SealedQubCbor canónico y los bytes escritos en el almacenamiento permanente. La clave de 256 bits K vive únicamente en el fragmento de URL 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, todo gateway de almacenamiento y todo CDN frente a cualquiera de ellos están observacionalmente ciegos a K. Por lo tanto, cada qub en el almacenamiento permanente es un ciphertext opaco cuyo plaintext es irrecuperable sin la URL que el creador eligió compartir.
Efecto neto:
- Inmunidad a la enumeración por defecto. Los bytes envueltos en el almacenamiento permanente son indistinguibles a nivel de byte de un ciphertext arbitrario. Una estrategia de harvester de "consultar GraphQL para cargas con forma de qub, descifrar en masa con firmas drand públicas" no termina con plaintext.
- Postura de privacidad por crypto-shredding. qub.social literalmente no puede descifrar su propio corpus. Las citaciones llegan a ciphertext, no a plaintext.
- 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
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12a, this section)
OuterWrapper CBOR bytes ← uploaded to permanent storage (§7 step 15)
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.4
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 el desenvuelto. El paso de desenvuelto no lo aplica directamente (la vinculación AAD del AEAD hace imposible la manipulación a nivel de byte), pero la capa de unlock comprueba la relación transitivamente: si un creador envuelve unSealedQubCborcuyoqub_idinterno no coincide con elqub_iddel wrapper, el paso 11 de §8 falla.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 de almacenamiento permanente del qub B | Discrepancia AAD → 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
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.4
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
return P // P is the inner 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. - Los qubs privados cifrados al destinatario (una función reservada para la fase 2, aún sin especificar) se componen sobre este wrapper como un segundo nivel de confidencialidad; ambos niveles pueden estar activos simultáneamente.
- Los pacts (§6, content_type
0x03) se envuelven exactamente igual que los qubs de texto; el wrapper es ciego a los bytes del 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 se escribe directamente en el almacenamiento permanente, sin capa OuterWrapper y sin clave K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
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 unlock cualquiera que tenga el arweave_tx_id 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 embeds de terceros y un SEO post-revelación más rico necesitan todos un enlace que funcione sin un secreto que el servidor nunca posee (§13.6).
Consecuencias que un productor DEBE tener en cuenta:
- Sin inmunidad a la enumeración. Los qubs públicos renuncian por construcción a la propiedad de inmunidad a la enumeración de §13.1. El servicio de carga de referencia les estampa un tag de almacenamiento permanente
Visibility: public(y solo a ellos) para que sean intencionadamente descubribles; los qubs privados no portan tal tag y conservan su indistinguibilidad a nivel de byte. - 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. Un visor/embed conforme distingue las dos formas mediante el parseo: los bytes que se parsean como
OuterWrappersiguen la ruta de desenvuelto conK; los bytes que se parsean como unSealedQubCbordesnudo se aceptan directamente. No se requiere ninguna marca de cable, yqub_idno vincula la visibilidad — el mismo contenido es idéntico a nivel de byte en la capaSealedQubtanto si se sella público como privado.
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 = 4695445 (= (1736294400 - 1595431050) / 30, 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 — V1.2):
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)
0x000000000047A595 || // drand_round as u64 big-endian (4695445)
body_hash || // 32 bytes
title_hash // 32 bytes (all-zeros sentinel; title absent)
Expected output:
qub_id = SHA3-256(preimage)
= 3a9fcb31b750d985c262fada6d4f777f
d6a28be831d941d85c131f5a4bbaf8a4
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 computados por la implementación de referencia y DEBEN coincidir bit por bit. Disposiciones históricas de la preimagen (pre-lanzamiento — ningún qub en producción dependía de estas): el qub_id V1.0 de 92 bytes era 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; el qub_id V1.1 de 100 bytes (tras incorporar outcome_at_or_zero) era b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 incorpora drand_round y eleva el separador de dominio a QUB_ID_V2.
14.2 Mapeo de unlock-round
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calculation:
(1735689600 - 1595431050) / 30 = 4675285.0
ceil(4675285.0) = 4675285
drand_round = 4675285
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 actualmente fija tres casos:
| Caso | Cobertura |
|---|---|
basic-text-public |
La forma de SealedQub realista más pequeña; sin campos opcionales. Establece la forma canónica del wrapper para un qub típico v1.0. |
with-recipient-pubkey |
SealedQub con recipient_pubkey establecido (ruta de Phase 2). Conjunto de claves CBOR interno diferente, qub_id diferente. |
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 registro de §9.2 no define ningún otro valor; un visor v1 DEBE rechazar todosig_algfuera de{0x00, 0x01}. Se anticipa una futura entrada Ed25519 (§15.3), pero no está asignada en v1. - 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 visores actualmente codifican de forma fija las longitudes de clave y firma por primitiva. El formato de cable no expone ninguna superficie de agilidad.
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.
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 (Diseño — revisión completada)
Estado. Esta sección es una especificación de diseño. Los formatos de cable, el hashing y el modelo de confianza que siguen son normativos para la implementación, pero todavía no se ha enviado ningún código de registro de transparencia. La revisión externa W5 está completada: §16.15 recoge las decisiones resueltas y las restricciones vinculantes de lanzamiento que surgieron de ella. La implementación puede proceder bajo esas restricciones. §16 sigue siendo prospectiva en el mismo sentido que §15 — fija el objetivo para que la implementación aterrice contra un diseño asentado en lugar de volver a derivar el modelo de confianza en la revisión de código. Es estrictamente aditiva — cada qub existente conserva su transacción individual de Arweave y no hay ningún cambio en el formato de cable de
SealedQub/QubEnvelope.
16.1 Justificación y niveles de durabilidad
Hoy la durabilidad de un qub y su compromiso temporal descansan ambos sobre una única transacción de Arweave por qub (§11). Esto acopla la latencia del sellado a la finalidad de Arweave, convierte la subida por qub en un tope de coste de producto (ARWEAVE_DAILY_CEILING) y no aporta ningún ordenamiento a prueba de manipulaciones entre qubs. El registro de transparencia añade dos capas por debajo y alrededor de ese único nivel:
| Nivel | Nombre | Garantía | Cuándo |
|---|---|---|---|
| T1 | Confirmación síncrona R2-first | Suelo de durabilidad — los bytes sellados se escriben en almacenamiento durable antes de que el sellado retorne (< 300 ms p95). |
Cada qub, de forma síncrona (§16.10). |
| T2 | Inclusión por lotes en el registro de transparencia | Compromiso universal de solo anexado, a prueba de manipulaciones + ordenamiento total, anclado a Arweave. | Cada qub, diferido + por lotes (§16.5–16.7). |
| T3 | Permanencia en Arweave por qub | Una transacción individual de Arweave para el qub. | Upsell de pago, y el fallback de indisponibilidad de Arweave (§16.8). |
T2 convierte el Arweave por qub en una elección (T3) en lugar de la única ruta de durabilidad. ARWEAVE_DAILY_CEILING se retira como tope de producto y queda degradado a un disyuntor sobre la wallet de anclaje dedicada únicamente (§16.7); los sellados de usuario nunca se rechazan por sobrepasarlo.
Honestidad sobre la durabilidad (resuelto — §16.15 Q6). La durabilidad no regresa: la escritura R2 de T1 es síncrona y de escritura única, por lo que un qub de nivel gratuito que no compró T3 es plenamente durable en el instante en que el sellado retorna. Lo que se vuelve más grueso es el tiempo de compromiso de cota superior demostrable: para un qub gratuito pasa a ser el tiempo de bloque del anclaje en lugar del tiempo de bloque de una transacción por qub. A bajo volumen — el estado realista de lanzamiento temprano y fuera de pico — la cadencia diaria completa es el suelo típico, no un caso extremo poco frecuente. Por tanto, el encuadre de producto es una cota superior sin ninguna latencia numérica comprometida — «sellado y durable ahora; una marca de tiempo pública independiente se añade en el siguiente anclaje del registro (típicamente a diario)» — y la prueba de compromiso con hora exacta es una propiedad de pago de T3, divulgada en la superficie de comparación de niveles y en los términos (§16.11, §16.15 Q6). Cualquier cota de tiempo es solo un SLO interno, nunca un SLA comercializado.
16.2 Estructura de LogLeaf (dos formas comprometidas)
Una entrada de registro es un LogLeaf, codificado como CBOR canónico escrito a mano bajo el perfil §3.1 (longitud definida, sin tags, sin floats, enteros en forma más corta, texto NFC, campos opcionales omitidos cuando están ausentes, claves ordenadas por longitud de bytes codificados ascendente y luego byte a byte). 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), de modo que dos implementaciones no puedan discrepar sobre los bytes de la hoja a través de una diferencia de ancho de entero o de orden de claves. Todos los enteros son u8 / u64 / i64; todos los digests son cadenas de bytes de 32 bytes (bstr[32]). Un id de transacción de Arweave almacenado es un digest SHA-256 crudo de 32 bytes transportado como bstr[32], nunca una cadena de texto base64url (coincide con §3.3).
La hoja tiene dos formas seleccionadas por un byte kind, porque en la ruta de subida por defecto el Worker está ciego a los bytes: POST /api/v1/upload recibe únicamente qub_id y unlock_at como afirmaciones no fiables del cliente — body_hash, drand_round, created_at y drand_chain_version están todos sellados dentro del wrapper externo §13, cuya clave el Worker nunca tiene. Solo la ruta de sellado en servidor (POST /api/v1/seal) deriva body_hash / drand_round del plaintext. Una única forma de hoja que transportara body_hash + drand_round comprometería por tanto valores que el operador nunca verificó para la mayoría de los qubs reales. La división mantiene honesto cada valor comprometido:
| Clave | Long. enc. | Tipo | Presencia | Significado |
|---|---|---|---|---|
seq |
4 | u64 |
requerido | Índice global de hoja basado en 0; la posición a la que se compromete la prueba de inclusión. |
kind |
5 | u8 |
requerido | 0x01 atestiguado (sellado en servidor) o 0x02 afirmado (sellado en cliente / subida ciega a los bytes). |
ref |
4 | bstr[32] |
requerido | Id de referencia de la hoja. Atestiguado → qub_id crudo. Afirmado → el id cegado SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
6 | bstr[32] |
requerido | Dirección de contenido SHA3-256(stored_bytes) — el único vínculo de contenido que el Worker siempre puede computar honestamente, en ambas rutas. |
unlock_at |
10 | i64 |
requerido | Copiado (atestiguado) o afirmado (afirmado); validado > 0 antes de entrar en la hoja. |
received_at |
12 | i64 |
requerido | Reloj de pared del Worker en la confirmación R2. No evidenciario (afirmado por el operador; §16.6). Presente para autodescripción, nunca una prueba. Validado > 0. |
body_hash |
10 | bstr[32] |
solo kind=0x01 |
Omitido en 0x02 — el Worker carece de él bajo §13. |
drand_round |
12 | u64 |
solo kind=0x01 |
Omitido en 0x02. |
Una hoja kind=0x02 deliberadamente no compromete ni body_hash ni drand_round: atestigua el compromiso y el ordenamiento de un ciphertext opaco en la dirección de contenido chash, reclamando qub_id y unlock_at — no su plaintext ni su round. Las patas de plaintext/round de un qub afirmado provienen de la verificación existente del bundle .qub de §11, no del registro (§16.11). drand_chain_version no está en la hoja (está dentro del wrapper en la ruta por defecto); la granularidad de chain reside en el anclaje (§16.7). Disciplina del codificador: rechazar un ref o chash todo-ceros, y rechazar unlock_at / received_at no positivos, reflejando la guardia centinela outcome_at > 0 en cbor.rs.
16.2.1 Cegado de qubs privados
El registro no debe convertirse en el oráculo de enumeración que el wrapper externo §13 existe para prevenir (§13.1). Para un qub privado (envuelto), la hoja asserted compromete el identificador cegado SHA3-256(qub_id ‖ log_blind_secret), donde log_blind_secret es un secreto en custodia del servidor, y omite body_hash. Un tercero no puede vincular esa hoja a un qub_id específico; el poseedor del qub, que tiene la URL de entrega y por tanto qub_id, puede recomputar el cegado para confirmar su propia inclusión. Un qub público (ya enumerable, que ya lleva el tag Visibility: public de Arweave según §13.8) compromete el qub_id crudo. Este es el único lugar donde la verificabilidad autónoma cede deliberadamente ante un invariante de privacidad que es carga estructural; el vínculo autónomo para los qubs privados es chash (§16.9).
Custodia de log_blind_secret (resuelto — §16.15 Q4). El cegado protege la no vinculabilidad de la hoja, no la confidencialidad del plaintext (de eso se ocupa el wrapper §13 de forma independiente). Ante un compromiso de log_blind_secret, para cualquier qub_id que el adversario ya posea o pueda reconstruir (cada qub cuyo bundle/URL tenga, más cualquier qub_id de baja entropía o público) recomputa el ref de la hoja en un solo hash y lo vincula — esto es vinculación directa de una población conocida, no una fuerza bruta sobre un espacio desconocido. Clasifica log_blind_secret como un secreto de grado correlación/Sybil en el mismo nivel de custodia que otros secretos del servidor, y rótalo solo hacia adelante (una rotación vuelve a cegar las hojas futuras; no puede desvincular retroactivamente las ya ancladas).
16.3 Hashing de hojas y nodos
Hashing con separación de dominio del 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 entradas, §16.4) y 0x03 (hash de STH, §16.6) están reservados y son disjuntos de estos. Son bytes únicos y por tanto no pueden colisionar con los separadores de dominio ASCII de 10 bytes existentes (QUB_ID_V2, etc.). El árbol es el árbol desequilibrado left-full del RFC 6962 (cada división interior en la mayor potencia de dos estrictamente menor que el conteo de hojas del subárbol), lo que permite que las pruebas de inclusión y de consistencia compartan un único algoritmo de audit-path. La especificación de referencia lleva pseudocódigo explícito de derivación izquierda/derecha y fija un vector de prueba que no es potencia de dos (5 hojas) para que el caso de promoción del borde derecho — que un vector de 4 hojas oculta — quede ejercitado.
16.4 Encadenamiento de hashes (interno)
El LogDO mantiene una cadena interna de entradas solo para consistencia ante caídas. Nunca se publica y nunca es accesible al 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 de solo anexado es la raíz Merkle acumulativa + su anclaje (§16.5–16.6), nunca el orden crudo en que el operador resulte servir las hojas: la cadena se recomputa para cualquier orden servido, por lo que solo la raíz anclada fija la posición canónica.
16.5 Árbol Merkle acumulativo y agrupación en lotes
Hay un único árbol del RFC 6962 en crecimiento perpetuo sobre todas las hojas en orden de seq — no árboles aislados por lote. (Una construcción encadenada por hoja-de-acarreo por lote fue rechazada: no es una verdadera relación de prefijo, por lo que sus «pruebas de consistencia» no son sólidas.) El árbol acumulativo da pruebas de consistencia genuinas del RFC 9162 y permite que un único anclaje reciente pruebe la inclusión de cualquier qub más antiguo.
El Durable Object LogDO es el escritor único (blockConcurrencyWhile, reflejando QuotaDO / EntitlementDO) — anexar a un registro compartido es leer-modificar-escribir sobre estado compartido y por tanto DEBE pasar por un DO, nunca por KV. Cachea la frontera del borde derecho del árbol (O(log n) hashes) de modo que cerrar un lote es O(batch). Un lote es el conjunto de hojas ancladas juntas; sus disparadores son configurables, no congelados en el protocolo: un avance de tree_size de al menos LOG_BATCH_MAX_LEAVES (por defecto 4096), o que la edad alcance la cadencia de anclaje, o un flush forzado cuando aterriza un sellado T3 de pago. root_i es el Merkle Tree Hash acumulativo sobre las hojas 0 .. tree_size_i.
16.6 Signed Tree Head vía anclaje en Arweave
La transacción de anclaje en Arweave es el Signed Tree Head y reemplaza una firma del operador para la cabecera del árbol en sí: el anclaje diario no necesita ninguna clave de qub porque el owner de la tx de Arweave es la firma. La tesis del foso se sostiene — el sustrato inmutable, no un secreto en posesión de qub, es la carga estructural para la raíz anclada.
Hay exactamente una clave caliente de firma de qub en el diseño, y está fijada (pinned): la clave de recibo por sellado (§16.10). Su clave pública se compromete en LogProfile (distribuido con el verificador) y está cofirmada por anchor_owner, de modo que un verificador valida un recibo contra la misma raíz fijada que el anclaje. Esta es la resolución de §16.15 Q2 — una clave de recibo no fijada y rotable por el operador sería repudiable (el operador podría negar que la clave fuera suya), lo que anularía el valor de rendición de cuentas del recibo frente al adversario a nivel de operador que el recibo existe para disuadir. Por tanto: qub no posee ninguna clave de firma del registro no fijada; la clave de recibo está fijada y cofirmada por anchor_owner.
El SignedTreeHead es CBOR canónico (claves por longitud codificada): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (el sth_hash previo; 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) está horneado en qub_core como el LogProfile — junto a las constantes de quicknet ya presentes en DrandTimelockProvider::quicknet() — y distribuido con el binario del verificador. El verificador DEBE además verificar localmente la vinculación datos → tx_id de la tx de Arweave en lugar de confiar en una respuesta /raw/ de un gateway. Esto cierra el agujero de equivocación de wallet pícara: «anclado en Arweave» carece de sentido hasta que el verificador fija qué wallet.
La rotación es una extensión de gobernanza de §15, no una reutilización (resuelto — §16.15 Q3). La superficie de perfil de §15.2 actualmente enumera solo sig_algs / chains de drand / versiones de wrapper / tipos de contenido, y los disparadores de §15.3 no listan ninguno de estos — LogProfile / anchor_owner todavía no está en la superficie de §15. La gobernanza de rotación debe por tanto construirse: §15.3 se extiende (abajo) para añadir el disparador LogProfile, y una rotación es un bump firmado de LogProfile enviado en una actualización del verificador. Una rotación planificada lleva una cofirma saliente → entrante; una rotación motivada por compromiso no puede (la clave saliente es no fiable/no disponible precisamente entonces) y recurre al bump gobernado por §15, con la comprobación de fork del anclaje previo (abajo) acotando el daño en el ínterin.
Ventana de equivocación (parámetro de confianza de primera clase). Una hoja es resistente a la equivocación solo una vez que su anclaje cobertor está confirmado en Arweave. La ventana es received_at → confirmación del anclaje (≤ cadencia + finalidad de Arweave). Dentro de ella, las únicas garantías son el recibo de sellado fijado (§16.10) y la integridad operativa de qub. Tres artefactos de rendición de cuentas hacen esto honesto en lugar de gesticulado (el modelo de testigos es la resolución de §16.15 Q2):
- Recibo de sellado firmado y fijado — el análogo del SCT devuelto en la respuesta de subida (§16.10), firmado por la clave de recibo fijada y cofirmada por
anchor_owner. Una hoja descartada antes de su anclaje deja a la víctima un recibo no repudiable para publicar, cerrando el agujero de la omisión silenciosa. - Metodología de monitor publicada + recorrido de la cadena prev — la cadena
prevdel anclaje se recorre cabeza→génesis; un fork (dos anclajes en un mismosizeconrootdistinto, o unprevroto) es prueba publicable de mala conducta. La detección de equivocación es un compromiso operativo declarado, no una suposición silenciosa. - Cabeceras dobles autopublicadas — cada nueva cabecera
{sth_hash, tree_size}se publica en un repositorio público de GitHub de solo anexado, propiedad de qub y dedicado (la pata de autopublicación a prueba de manipulaciones que es carga estructural), con una publicación social como corroboración de mejor esfuerzo únicamente. Una publicación fallida DEBE generar una alerta (no fallar en silencio).
Cota de honestidad (restricción vinculante). Como qub controla ambas superficies de publicación, esto es autopublicado, no atestiguado de forma independiente. Ninguna superficie de producto, marketing o legal puede afirmar que el registro está «atestiguado de forma independiente»; la afirmación permitida es que la equivocación es detectable y deja un recibo no repudiable. Un verdadero testigo independiente de terceros queda diferido a un futuro bump de gobernanza de §15.
received_at es afirmado por el operador y ninguna afirmación puede apoyarse en él — nunca se expone como prueba ni como corroboración de disputa en ninguna superficie de producto / legal / API / renderizado de pruebas. El tiempo de bloque del anclaje en Arweave T es la única marca de tiempo sin confianza (una cota superior de «registrado por»). Cualquier comprobación de cordura del monitor sobre received_at DEBE compararse contra T, no contra el campo anchored_at del STH controlado por el operador; tal comprobación es una guarda contra el error de reloj de un operador honesto únicamente, no un control de rendición de cuentas contra un operador malicioso (§16.15 Q5).
16.7 Formato y cadencia de la transacción de anclaje
El AnchorBundle es el cuerpo de transacción de Arweave en CBOR canónico, escrito vía el bundler de §16.8: ver:u8, sth:bstr (bytes del SignedTreeHead canónico), prev_anchor:bstr (id de tx del anclaje previo en bytes crudos; omitido en el génesis), chain_hash:tstr (la chain de drand en vigor — quicknet), y el flujo de leaf-CBOR del lote en orden de seq para que el anclaje sea autocontenido: un monitor re-deriva root a partir del cuerpo sin ninguna dependencia de qub. (Si el flujo de hojas se vuelve grande a alto volumen, una futura revisión podría comprometer solo un rango de hojas por referencia; anotado, no adoptado en v1.)
Los tags de Arweave son intencionalmente enumerables — el registro está pensado para 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. Los tags son pistas no fiables; el cuerpo CBOR es la única autoridad.
Cadencia: diaria por defecto, revisada con el volumen (el disparador de tamaño acorta automáticamente la cadencia efectiva bajo carga); un sellado T3 de pago fuerza un anclaje para que los clientes de pago nunca esperen un día. La wallet de anclaje es dedicada y de baja velocidad, separada de la wallet de subida — DEBE ser su propia JWK (una clave distinta, no un rol lógico sobre la wallet de subida) para que un compromiso de la wallet de subida no pueda falsificar anclajes — con un presupuesto duro de transacciones de anclaje por día (el degradado ARWEAVE_DAILY_CEILING). La postura de custodia se declara con claridad: una clave caliente de alcance estrecho con un disyuntor ajustado y saldo bajo, no «fría» — una wallet que autofirma a diario no puede ser fría, y la especificación no finge lo contrario.
16.8 Bundler ANS-104
Un codificador de DataItem ANS-104 y firmador de deep-hash propios, de aproximadamente 300 líneas, solo Web Crypto, cero dependencias npm (ambos SDKs de Turbo fallan la guardia de cadena de suministro npm ci --ignore-scripts). Disposición de bytes del DataItem:
signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data
La firma es el deepHash de Arweave — un digest SHA-384 recursivo (requisito de cable de Arweave, crypto.subtle.digest("SHA-384")) sobre ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — y luego RSA-PSS sobre el deep hash con la JWK de la wallet vía crypto.subtle; id = base64url(SHA-256(signature)). El SHA-384 aquí está puesto en cuarentena como una primitiva solo-de-cable-de-Arweave, nunca una primitiva de confianza de qub (§15 deja constancia de la valla; el hashing de confianza de qub es SHA3-256 en todo el sistema).
Una única ruta de código sirve a tres consumidores: la permanencia por qub T3 de pago, el fallback de indisponibilidad de Arweave (encolar el DataItem, devolver la confirmación R2-first de todos modos — esto cierra el callejón sin salida actual del 503 ARWEAVE_UNAVAILABLE), y la escritura del AnchorBundle. Esquema de firma (resuelto — §16.15 Q8): v1 firma con RSA-PSS (tipo de firma 1) reutilizando el mecanismo existente de la JWK de la wallet de Arweave (cero custodia nueva de claves de larga vida, sirviendo a la tesis de «un secreto menos»); Ed25519 queda diferido a la ruta de migración PQ de §15.
El deep hash escrito a mano es el código de mayor riesgo y menor cobertura natural de W5, por lo que su gating es no negociable (§16.15 Q8):
- El fixture cross-language
tlog_v1.json(Rust + TS, el patrónwrapper_v1.jsonde §14.5) cubre el deep-hash, los bytes + id del DataItem, los hashes de hojas, una raíz de 5 hojas + audit path, un hash de STH, una prueba de inclusión y una prueba de consistencia — en ambas direcciones, firma y verificación (la dirección de verificación importa porque la comprobación local tx → tx_id de §16.6 mete el deep hash en cada verificador autónomo, no solo en el escritor). - Un round-trip de interoperabilidad de una sola vez a través de un bundler ANS-104 de referencia, consumido como datos de prueba estáticos únicamente — nunca una dependencia npm en tiempo de ejecución (la postura solo-Web-Crypto / sin-scripts-de-instalación se mantiene).
- La ruta deep-hash + RSA-PSS debe hacer round-trip a través de las mismas primitivas
crypto.subtleque usa producción, para que el codificador propio sea compatible byte a byte. - Un monitor de aceptación post-bundle continuo confirma que cada DataItem de anclaje / fallback logra efectivamente la aceptación en Arweave, con una alarma + disyuntor — porque el deep hash sirve también a la cola de fallback de indisponibilidad de Arweave, de modo que una regresión silenciosa llenaría esa cola con ítems rechazados por la red durante el mismísimo apagón que existe para cubrir.
16.9 Pruebas de inclusión y consistencia
Ambas son del RFC 9162, SHA3-256, servidas como CBOR canónico.
InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (el CBOR exacto de la hoja — el verificador recomputa leaf_hash por sí 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 autónoma (sin servidor de 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 de servicio de pruebas DEBE estar indexado por coordenada (resuelto — §16.15 Q7, precondición bloqueante). La generación de pruebas para hojas frías es neutral en cuanto a corrección solo si el material de auditoría en R2 es un almacén persistente de nodos Merkle indexado por coordenada absoluta del árbol (level, index) — no deltas de nodos por batch. Con un almacén indexado por coordenada, cualquier audit path (leaf i, size N) es un conjunto de O(log N) GETs directos a R2 sin recomputación a través de las fronteras de lote; con un almacén indexado por lote no lo es, que es la brecha de disposición de almacenamiento que esta resolución cierra. Los cuerpos de las hojas son asimismo direccionables por contenido mediante seq. Un vector de prueba de W5 DEBE probar una hoja fría de la era génesis contra una raíz muy posterior usando solo R2 + Arweave con el almacenamiento del LogDO borrado, de modo que la afirmación de seguridad de reclamación de §16.13 esté respaldada y no solo afirmada. Los O(log N) GETs secuenciales a R2 pertenecen únicamente al endpoint de pruebas asíncrono — nunca a la ruta caliente de sellado (§16.10) ni a un cron por tick.
16.10 Ordenamiento de la confirmación R2-first
La secuencia de POST /api/v1/upload pasa a ser:
- Guardias de la primera mitad (auth, validación, clave de shard de idempotencia) — sin cambios.
- Síncronamente
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— el suelo de durabilidad; también cierra la carrera de precaché de W1 (antes unctx.waitUntildespués del envío a Arweave). - Síncronamente
await LogDO.append(leaf)— un RPC de DO en la misma colo; el escritor único asignaseq, extiende la cadena de entradas y actualiza la frontera. (RMW sobre estado compartido → DO, nunca KV.) El RPCappendhace únicamente eso — el trabajoO(batch)de cierre de lote Merkle corre fuera de este RPC en la alarma del LogDO, o el p95 deappendse dispara en cadaLOG_BATCH_MAX_LEAVES-ésimo sellado. - Devolver la confirmación ahora — con el recibo de sellado (firmado por la clave de recibo fijada, §16.6) y
{ tx_id, log_seq, anchor_status: "pending" }. El fan-out de varios segundos a Arweave se retira de la ruta crítica. - Un
ctx.waitUntilencola el trabajo diferido: el envío a Arweave por qub (ahora de mejor esfuerzo / de pago; ante un fallo se enruta a la cola de fallback del bundler en lugar de devolver un 503 al usuario) más las escrituras de metadatos provisionales existentes. El cierre de lote y el anclaje corren de forma independiente desde la alarma del LogDO y el cron de anclaje diario. Sinctx.waitUntildentro de un bucle; la clave de shard de idempotencia existente se conserva.
Presupuesto de latencia (resuelto — §16.15 Q7). El objetivo < 300 ms p95 es una guardia de lanzamiento medida, no una suposición. La ruta crítica honesta son las lecturas KV de la primera mitad + un PUT a R2 + dos Durable Objects serializados — el débito de cuota de sellado existente del QuotaDO y el nuevo append del LogDO — por lo que el presupuesto debe contemplar dos viajes de ida y vuelta a DO en la misma colo, no uno. Envía una alarma de latencia del LogDO que refleje la del QuotaDO y trata una regresión del p95 como bloqueante de release.
16.11 Modelo de confianza — la afirmación precisa, acotada por el kind de la hoja
Para kind=0x01 (atestiguado): «Este contenido — un cuerpo que coincide con body_hash, identificado por qub_id — fue comprometido al registro de solo anexado de qub en la posición seq y existía no más tarde que el tiempo de bloque de Arweave T; era criptográficamente ilegible hasta el round de drand R = unlock_round(unlock_at).» Esta es la tríada completa {vinculación de round tlock + inclusión Merkle + raíz anclada}.
Para kind=0x02 (afirmado, el predeterminado): «Un ciphertext opaco con dirección de contenido chash, que reclama qub_id y unlock_at, fue comprometido al registro de solo anexado en la posición seq y existía no más tarde que el tiempo de bloque de Arweave T.» Las patas de round y cuerpo las suministra la verificación existente del bundle .qub de §11 (qub_core::unlock), no el registro; lo que el registro añade sobre una transacción por qub a secas es ordenamiento a prueba de manipulaciones, un tiempo de compromiso de cota superior sin confianza y resistencia a la equivocación.
Ambas afirmaciones excluyen, según §11: autoría sin sig_alg ≥ 0x01, intención y temporalidad de granularidad sub-anclaje. Ninguna deja que afirmación alguna se apoye en received_at.
Techo de afirmación (restricción vinculante de lanzamiento — resuelto §16.15 Q1). Para los qubs gratuitos / por defecto (kind=0x02), la afirmación kind=0x02 acotada de arriba es el techo de lo que cualquier superficie de producto, marketing, términos o renderizado de pruebas puede aseverar. Ninguna superficie puede declarar ni implicar que el registro prueba el contenido o el round de unlock de un qub por defecto — el registro prueba ordenamiento + un tiempo de compromiso de cota superior sin confianza de un ciphertext opaco. La prueba de contenido y round proviene exclusivamente de la verificación existente del bundle .qub de §11, que es independiente del registro. Esto es un bloqueante duro de lanzamiento sobre la copy, no una preferencia estilística; es la resolución que mantiene honesta la ruta por defecto ciega a los bytes.
16.12 Versionado y coordinación con W3
No hay ningún bump de cable de SealedQub y por tanto ningún bump de versión de protocolo (§12.1): el registro es un sidecar que se compromete a campos y bytes existentes, por lo que no entra en el historial de versiones de protocolo de §12.2. El drand_chain_version opcional de W3 queda intacto y sigue siendo el único campo opcional de SealedQub. El registro introduce en su lugar sus propios espacios de versión independientes — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — reflejando la independencia de versión del wrapper de §12.4 (el wrapper lleva un byte de versión independiente de la versión de protocolo, y las versiones del registro siguen la misma separación).
La entrega de la prueba se obtiene por defecto, con un ride-along opcional. Una prueba no puede existir en el momento del sellado (el anclaje aún no se ha escrito), por lo que el bundle .qub en el momento del sellado permanece sin prueba. El verificador de W7 obtiene GET …/proof una vez, o en modo totalmente offline reconstruye la prueba a partir del AnchorBundle público mediante una consulta de Arweave sobre Log-Id. El bundle .qub (W7) reserva un miembro inclusion_proof opcional — ausente en el sellado, poblado por una re-exportación post-anclaje para archivado en frío — siguiendo el mismo patrón «opcional, omitido por defecto, aditivo» que el drand_chain_version de W3.
16.13 Retención
Las ventanas de retención para la cola abierta del LogDO, el sustrato de servicio de pruebas en R2, los contadores del disyuntor de anclaje y la cola de fallback del bundler se especifican en docs/DATA-RETENTION.md. Principio: el almacenamiento caliente por entrada del registro (LogDO) es reclamable tras el anclaje; su material de auditoría — el almacén de nodos Merkle indexado por coordenada (level, index) + los cuerpos de hojas direccionados por seq (§16.9) + los anclajes de Arweave — es permanente. Reclamar una hoja fría del DO nunca invalida una prueba emitida, porque una prueba se resuelve contra ese almacén de nodos permanente en R2 y el anclaje de Arweave, no contra el DO (y el vector de prueba de DO borrado de §16.9 lo demuestra).
16.14 Vectores de prueba
W5 envía el fixture cross-language tlog_v1.json (§16.8) más vectores trabajados: una hoja kind=0x01 y una 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 viven junto a los vectores del wrapper externo de §14.5 y son ejercitados tanto por la implementación de Rust (qub-core) como por la de TypeScript (Worker).
16.15 Decisiones de revisión (W5 — resueltas)
La revisión externa W5 (una pasada de diseño adversarial + sign-off del owner) está completada. Cada decisión de abajo está asentada y reflejada en el texto de §16 de arriba; las restricciones vinculantes de lanzamiento se reafirman al final. La implementación puede proceder bajo ellas.
- Honestidad de la hoja de ruta por defecto (
kind=0x02) — RESUELTO. Enviar la división de dos kinds de hoja según lo especificado:kind=0x02no compromete nibody_hashnidrand_round. Sin campo*_body_hashen la ruta ciega a los bytes (sería la señal falsa de «verificado» más legible para los integradores y es una conveniencia que §11 ya provee desde el bundle). No requerir sellado en servidor para los qubs registrados (eso forzaría plaintext a través del Worker y destruiría el foso del crypto-shredding). Cualquier atajo autodescriptivo pertenece al bundle.qub/ sobre de prueba como un campo recomputado por el verificador, nunca un campo de la hoja. Techo de afirmación confirmado por el owner: §16.11. - Rendición de cuentas de equivocación / omisión — RESUELTO. La clave de recibo de sellado está fijada en
LogProfile+ cofirmada poranchor_owner(cerrando la contradicción previa de «sin clave de firma»; §16.6). Modelo de testigos de lanzamiento: recibo fijado + metodología de monitor + recorrido de la cadena prev + cabeceras dobles autopublicadas (repositorio público de GitHub propiedad de qub, social de mejor esfuerzo), comercializado como detectable + con recibo, nunca atestiguado de forma independiente. Un verdadero testigo de terceros queda diferido a un bump de gobernanza de §15. - Raíz de confianza de anchor-owner fijada + rotación — RESUELTO. Adoptar el pin de
LogProfile(§16.6); el verificador compruebaanchor_tx.owner == anchor_ownery verifica localmente la vinculación datos → tx_id de la tx. La gobernanza de rotación es una extensión a construir de §15 (disparador de §15.3 añadido), no una reutilización; las rotaciones planificadas cofirman, las rotaciones motivadas por compromiso recurren al bump de §15 con la comprobación de fork acotando el daño. - Cegado de la hoja de qubs privados — RESUELTO. Mantener el cegado para los qubs privados (
ref = SHA3-256(qub_id ‖ log_blind_secret)),qub_idcrudo para los qubs públicos (ya §16.2.1),chashcomo el vínculo autónomo.log_blind_secretes un secreto de grado correlación/Sybil, rotación solo hacia adelante (§16.2.1). received_at— RESUELTO. Mantenerlo en la hoja, comprometido pero explícitamente no evidenciario; nunca expuesto como prueba ni como corroboración de disputa en ninguna superficie. Cualquier comprobación de cordura del monitor se compara contra el tiempo de bloque de ArweaveT, no contra elanchored_atcontrolado por el operador (§16.6).- Temporalidad demostrable de nivel gratuito — RESUELTO (sign-off del owner). La durabilidad no regresa; solo el tiempo de compromiso de cota superior demostrable se vuelve más grueso hasta el tiempo de bloque del anclaje. La copy de nivel gratuito no usa ningún SLA numérico («…añadida en el siguiente anclaje del registro, típicamente a diario»); la prueba de hora exacta es una propiedad de pago de T3, divulgada en la superficie de comparación de niveles + en los términos (§16.1).
- Árbol acumulativo en Workers — RESUELTO. Un único árbol acumulativo del RFC 9162 + LogDO de escritor único con frontera cacheada (margen cómodo frente al techo de ~1k escrituras/seg del DO; diferir el sharding de Merkle-de-raíces-de-shard hasta acercarse a él). Precondición bloqueante: almacén de nodos R2 indexado por coordenada
(level, index)+ el vector de prueba de hoja fría con DO borrado (§16.9);< 300 mses una guardia de lanzamiento medida sobre dos DOs serializados (§16.10). - Esquema de firma ANS-104 + deep-hash — RESUELTO. RSA-PSS (tipo de firma 1, reutilizando la JWK de la wallet de anclaje dedicada); Ed25519 diferido a la ruta PQ de §15. El deep hash SHA-384 escrito a mano está sujeto al fixture cross-impl en ambas direcciones, una comprobación de interoperabilidad solo-estática con bundler de referencia, el round-trip de
crypto.subtlecompartido y el monitor de aceptación en Arweave post-bundle (§16.8).
Restricciones vinculantes de lanzamiento (trasladar a la implementación + revisión de producto/legal):
- Techo de afirmación (Q1/Q6). Ninguna superficie puede decir que el registro prueba el contenido o el round de unlock de un qub por defecto; la afirmación permitida es ordenado, de forma a prueba de manipulaciones, con un tiempo de compromiso de cota superior sin confianza. La copy de marca de tiempo de nivel gratuito no lleva latencia numérica; la prueba de hora exacta es solo de T3 de pago.
- Honestidad de testigos (Q2). Comercializar la equivocación como detectable + con recibo, nunca atestiguada de forma independiente.
- Claves de recibo + anclaje (Q2/Q8). La clave de recibo está fijada + cofirmada; la wallet de anclaje es su propia JWK distinta de la wallet de subida.
- Guardia del deep-hash (Q8). Ningún anclaje ni tx T3 se envía hasta que el fixture en ambas direcciones + la comprobación de interoperabilidad pasen; el monitor de aceptación genera alerta ante un fallo.
- Precondición de almacenamiento (Q7). El almacén de nodos indexado por coordenada + el vector de hoja fría con DO borrado son prerrequisitos para la garantía de «la reclamación nunca invalida una prueba».