Especificação do protocolo qub
qub é um protocolo para compromissos temporais criptográficos: um sistema para selar palavras para uma data futura e posteriormente verificar exatamente o que foi selado, cujo lançamento foi controlado em rondas pelo drand e—quando uma transação de armazenamento ou prova de registo de transparência está disponível—um limite superior carimbado independentemente sobre quando o texto cifrado foi comprometido.
Três primitivos fazem com que funcione. drand é um farol de aleatoriedade descentralizado — a data de revelação é aplicada criptograficamente, em vez de depender da boa vontade da qub. Armazenamento durável preserva bytes selados reconhecidos enquanto os caminhos de publicação atuais agendam transacções individuais de armazenamento permanente; os acréscimos de registos de carregamento geral bem-sucedidos podem adicionalmente juntar-se a compromissos agrupados e ancorados. ML-DSA-65 é uma assinatura digital pós-quântica—quando a autoria está ativada, o qub está ligado a um par de chaves cujo segredo nunca sai do dispositivo do autor.
Juntos, estes primitivos constituem uma declaração que é bloqueada no tempo e resistente a alterações, opcionalmente atribuível e independente de carimbo temporal—um recibo cujo valor aumenta à medida que a capacidade do mundo de fabricar o passado melhora.
O restante deste documento é a especificação normativa necessária para implementações interoperáveis.
Especificação do protocolo qub
| Campo | Valor |
|---|---|
| Lançamento do documento | 1.0.0 (protocol-v1.0.0) |
| Protocolo de ligação | 0x01 |
| Envoltório exterior | 0x01 |
| Data de vigência | 23-09-2026 |
| Estado | Atual |
| Revisado através | 23-09-2026 |
Este documento é a especificação normativa do protocolo para o sistema de compromisso temporal qub. Define as estruturas de dados, as regras de serialização, as fórmulas de derivação e os procedimentos de verificação exigidos para implementações interoperáveis.
Âmbito: a camada do protocolo é intencionalmente neutra em relação ao idioma — o corpo do qub é texto puro / markdown / bytes de pacto opacos, e a renderização localizada é responsabilidade do leitor (aplicação web qub.social, iframe <qub-embed>, clientes MCP, etc.).
1. Notação e convenções
| Notação | Significado |
|---|---|
u8, u64, i64 |
Inteiros sem sinal / com sinal da largura de bits indicada |
[u8; N] |
Arranjo de bytes de comprimento fixo de N bytes |
Vec<u8> |
Arranjo de bytes de comprimento variável |
Option<T> |
Valor do tipo T, ou ausente |
String |
Cadeia de texto UTF-8, normalizada em NFC |
| ` | |
SHA3-256(x) |
Hash NIST SHA3-256 da cadeia de bytes x (FIPS 202) |
ceil(x) |
Função teto: o menor inteiro ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Byte mais significativo primeiro |
Todos os inteiros nas construções de pré-imagem são codificados como arranjos de bytes big-endian de largura fixa (i64 → 8 bytes, u8 → 1 byte), salvo indicação em contrário.
Todos os timestamps são segundos Unix em UTC.
2. Estruturas de dados
2.1 ComposeQub (estado em memória do criador)
Não serializado em CBOR. Não gravado no armazenamento permanente. Local à aplicação do criador.
ComposeQub {
draft_id: [u8; 16], // Random, generated locally
created_at: i64, // Unix seconds UTC
unlock_at: Option<i64>, // Unix seconds UTC; None while composing
visibility: u8, // 0x00 = private; 0x01 = public
content_type: u8, // 0x01 text; 0x03 pact; 0x04 verdict
plaintext: Vec<u8>, // Raw body bytes (UTF-8 for text)
sender_label: Option<String>, // Display name; V2-signed when authorship is enabled
title: Option<String>, // Plaintext countdown title; bound via title_hash
reply_to: Option<[u8; 32]>,// Parent qub_id; V2-signed when authorship is enabled
outcome_at: Option<i64>, // Optional future judgment time; bound to qub_id
status: DraftStatus, // Composing | Sealed | Uploaded | Failed
}
2.2 QubEnvelope (carga útil decifrada)
Serializado usando CBOR canónico (§3). Cifrado dentro do SealedQub. É a estrutura que prova a integridade do conteúdo após a decifragem.
QubEnvelope {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Derived (see §4.1)
content_type: u8, // Content type registry (see §6)
created_at: i64, // Unix seconds UTC
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // When reality renders judgment; bound to qub_id
sender_label: Option<String>, // Not in qub_id; V2-signed when authorship is enabled
reply_to: Option<[u8; 32]>,// Parent qub_id; not in qub_id; V2-signed when present
body: Vec<u8>, // UTF-8 text or canonical CBOR pact/verdict body
body_hash: [u8; 32], // SHA3-256(body) (see §4.2)
sig_alg: u8, // Signature algorithm (see §9.2)
author_signature: Option<Vec<u8>>, // Set when sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Set when sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
cosigner_signature: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}
Linha de base (texto qub não assinado): version = 0x01, content_type = 0x01, sig_alg = 0x00; os campos de assinatura e de co-signatário estão ausentes. Outros campos opcionais de metadados podem estar presentes.
Outras configurações v1: content_type = 0x03 (corpo de pacto, ver §6.1); sig_alg = 0x01 (ML-DSA-65) com author_signature e author_pubkey presentes (ver §9.3); cosigner_pubkey e cosigner_signature presentes juntos para pactos co-assinados (ver §9.7); reply_to definido com o qub_id do qub pai para qubs de cadeia de respostas (ver §9.3 para as implicações no âmbito da assinatura).
2.3 SealedQub (formato canónico de wire)
Serializado usando CBOR canónico (§3). Este é o artefacto interno do fio: a entrega pública armazena estes bytes puros, enquanto a entrega privada envolve-os em OuterWrapper antes do armazenamento (§13).
SealedQub {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Same as QubEnvelope.qub_id
visibility: u8, // 0x00 = private/wrapped; 0x01 = public/bare
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // Surfaced on the verdict-watch CTA
// before reveal; mirrors QubEnvelope.outcome_at;
// bound to qub_id via the §4.1 preimage.
drand_chain_id: String, // drand chain hash (hex string)
drand_round: u64, // Target drand round number
drand_chain_version: Option<u8>, // W3 — chain-migration version. Absent / 0 = quicknet
// (the only chain today). Lets a future chain swap
// be expressed on the wire without a breaking format
// change. NOT part of the §4.1 qub_id preimage, so its
// addition never alters an existing qub's identity.
tlock_ciphertext: Vec<u8>, // tlock-encrypted QubEnvelope CBOR bytes
recipient_pubkey: Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
// but not interpreted by the v1 reference viewer
title: Option<String>, // Plaintext title surfaced on the viewer
// countdown before reveal. Bound to qub_id
// via title_hash (§4.1). 1..=100 NFC code
// points, no hostile/control code points.
}
2.4 RevealedQub (estado da aplicação do leitor)
Não serializado em CBOR. Local à aplicação do leitor. Construído após decifragem e verificação bem-sucedidas.
RevealedQub {
qub_id: [u8; 32],
arweave_tx_id: String,
visibility: u8,
content_type: u8,
created_at: i64,
unlock_at: i64,
outcome_at: Option<i64>, // Carried from both wire layers; drives the verdict-watch block
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Carried forward from SealedQub.title
reply_to: Option<[u8; 32]>,
body: Vec<u8>,
body_hash: [u8; 32],
body_hash_verified: bool,
author_signature: Option<Vec<u8>>,
author_pubkey: Option<Vec<u8>>,
signature_verified: Option<bool>,
cosigner_pubkey: Option<Vec<u8>>,
cosigner_signature: Option<Vec<u8>>,
cosigner_verified: Option<bool>,
}
3. Perfil canónico de CBOR
Toda serialização de SealedQub e QubEnvelope DEVE estar em conformidade com este perfil. Duas implementações, dada a mesma estrutura lógica, DEVEM produzir bytes idênticos.
3.1 Regras de codificação
| Regra | Especificação |
|---|---|
| Padrão | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Ordenação de chaves de mapa | Ordenado primeiro por comprimento codificado em bytes (mais curto antes do mais longo), depois lexicograficamente (byte a byte para codificações de mesmo comprimento) |
| Codificação de inteiros | Forma mais curta: 0–23 no byte inicial; 24–255 em 2 bytes; 256–65535 em 3 bytes; etc. |
| Codificação de comprimento | Apenas comprimentos definidos. Nada de arrays, mapas, byte strings ou text strings com comprimento indefinido (additional info = 31 é proibido). |
| Tags | Sem tags CBOR (major type 6 é proibido). |
| Ponto flutuante | Sem floats (major type 7 valores 0xF9–0xFB são proibidos). |
| Text strings | Codificadas em UTF-8, normalizadas em NFC (Unicode Normalization Form C). |
| Byte strings | Bytes brutos. Nenhuma codificação base64 na camada CBOR. |
| Chaves duplicadas | Rejeitar com erro. Os parsers NÃO DEVEM aceitar silenciosamente chaves duplicadas em mapas. |
| Chaves desconhecidas | Rejeitar com erro. Os parsers NÃO DEVEM tolerar chaves de mapa fora do conjunto canónico de chaves do tipo — duas byte strings canónicas distintas nunca podem descodificar para o mesmo valor (encode(decode(x)) == x), e, em cargas úteis assinadas, uma chave extra seria conteúdo oculto ao qual ambas as assinaturas ficam vinculadas. A evolução do esquema passa por version, nunca por chaves extra. |
| Valores simples | Apenas true (0xF5), false (0xF4) e null (0xF6) são permitidos. |
| Campos opcionais | Campos opcionais ausentes são omitidos inteiramente do mapa CBOR (não codificados como null). Os campos opcionais presentes são incluídos na ordem de chave ordenada. |
3.2 Ordens canónicas verificadas de chaves
Estas ordens de chaves são normativas. As implementações DEVEM emitir as chaves exatamente nesta ordem. Asserções de depuração DEVERIAM verificar a ordenação em builds não-release.
QubEnvelope (versão 0x01, sem assinatura, todos os campos opcionais ausentes):
"body" (5 encoded bytes)
"qub_id" (7 encoded bytes)
"sig_alg" (8 encoded bytes)
"version" (8 encoded bytes)
"reply_to" (9 encoded bytes) ← only if present (reply chains)
"body_hash" (10 encoded bytes)
"unlock_at" (10 encoded bytes)
"created_at" (11 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (verdict mechanic)
"content_type" (13 encoded bytes)
"sender_label" (13 encoded bytes) ← only if present
"author_pubkey" (14 encoded bytes) ← only if present
"cosigner_pubkey" (16 encoded bytes) ← only if present (pact cosign)
"author_signature" (17 encoded bytes) ← only if present
"cosigner_signature" (19 encoded bytes) ← only if present (pact cosign)
Derivação da ordem de chaves do QubEnvelope: cada chave é uma text string CBOR. Comprimento codificado = 1 byte de cabeçalho + comprimento da string (para strings com menos de 24 bytes). Ordene primeiro por comprimento codificado total e depois lexicograficamente para chaves de mesmo comprimento.
SealedQub (versão 0x01, público, sem destinatário):
"title" (6 encoded bytes) ← only if present
"qub_id" (7 encoded bytes)
"version" (8 encoded bytes)
"unlock_at" (10 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (verdict mechanic)
"visibility" (11 encoded bytes)
"drand_round" (12 encoded bytes)
"drand_chain_id" (15 encoded bytes)
"recipient_pubkey" (17 encoded bytes) ← only if present
"tlock_ciphertext" (17 encoded bytes)
"drand_chain_version" (20 encoded bytes) ← only if present (W3; absent = quicknet)
PactTerms (corpo de pacto, content_type 0x03):
"notes" (6 encoded bytes) ← only if present
"terms" (6 encoded bytes)
"title" (6 encoded bytes)
"party_a" (8 encoded bytes)
"party_b" (8 encoded bytes)
"pact_version" (13 encoded bytes)
PactTerm (linha do array terms):
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier (mapa party_a / party_b):
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 Referência de codificação de bytes
| Tipo | Codificação CBOR | Exemplo |
|---|---|---|
| Hash SHA3-256 (32 bytes) | 0x58 0x20 + 32 bytes |
body_hash, qub_id |
| Timestamps (i64) | Major type 0 (positivo) ou 1 (negativo), codificação mais curta | Segundos Unix |
| Versão (u8, valor 1) | 0x01 (byte único) |
|
| Tipo de conteúdo (u8, valor 1) | 0x01 (byte único) |
|
| sig_alg (u8, valor 0) | 0x00 (byte único) |
|
| Assinatura ML-DSA-65 (3.309 bytes) | 0x59 0x0C 0xED + 3.309 bytes |
author_signature, cosigner_signature |
| Chave pública ML-DSA-65 (1.952 bytes) | 0x59 0x07 0xA0 + 1.952 bytes |
author_pubkey, cosigner_pubkey |
4. Derivações normativas
4.1 qub_id
O qub_id identifica de forma única um qub e vincula o QubEnvelope ao SealedQub. É derivado deterministicamente do conteúdo do envelope.
qub_id = SHA3-256(
"QUB_ID_V2" || // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
version || // u8 (1 byte)
content_type || // u8 (1 byte)
created_at || // i64 big-endian (8 bytes)
unlock_at || // i64 big-endian (8 bytes)
outcome_at_or_zero || // i64 big-endian (8 bytes; 0 when outcome_at is absent)
drand_round || // u64 big-endian (8 bytes)
body_hash || // [u8; 32] (32 bytes)
title_hash // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output
Codificação do separador de domínio: A string "QUB_ID_V2" corresponde a 9 bytes ASCII. Um único byte 0x00 de preenchimento é acrescentado para totalizar 10 bytes para alinhamento. As implementações DEVEM usar exatamente estes 10 bytes: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
outcome_at codificação: Uma revisão de implementação pré-lançamento estendeu a pré-imagem de 92 para 100 bytes para dobrar o opcional outcome_at campo na ligação. Ausente outcome_at é codificado como 8 bytes zero; os validadores do protocolo rejeitam outcome_at <= 0 em todo o lado para que este sentinela não colida com um valor legítimo. Veja §3.2 (formato de fio) e o in-tree tasks/verdict-uplift-plan.md para o mecânico de veredicto que motiva este campo.
drand_round codificação: Uma revisão posterior da implementação pré-lançamento estendeu a pré-imagem de 100 para 108 bytes para dobrar drand_round (a ronda drand alvo, §4.3) na ligação, e aumentou o separador de domínio para QUB_ID_V2. Isto liga a ronda de timelock à identidade qub: um gateway não pode voltar a ligar o texto cifrado a uma ronda diferente (por exemplo, já passada) da que está exibida unlock_at implica. O procedimento de desbloqueio (§8) verifica adicionalmente se a ronda incorporada no estrofe de cifra tlock corresponde unlock_round(unlock_at), portanto o tempo de desbloqueio exibido é comprovadamente a ronda que permite a descodificação.
Propriedades:
- Alterar qualquer campo ligado à pré-imagem—
version,content_type,created_at,unlock_at,outcome_at,drand_round, crubodybytes (atravésbody_hash), outitle(atravéstitle_hash)—produz um diferentequb_id. - O qub_id é calculado antes da encriptação. Tanto o QubEnvelope como o SealedQub transportam o mesmo qub_id. O visualizador verifica se correspondem após a desencriptação.
qub_idnão depende desender_label,reply_to, bytes de assinatura ou chaves públicas de assinatura. No entanto, na construção de assinatura V2 atual,sender_labelereply_tosão autenticados diretamente porsender_label_hashereply_to_or_zero(§9.3) sempre que houver assinaturas presentes.- Alterando o SealedQub
title(com tudo o resto corrigido) mudançasqub_idviatitle_hash. Um gateway, portanto, não pode trocar o título em texto plano exibido na contagem decrescente sem invalidar a identidade qub. - Alterando o SealedQub
outcome_at(com tudo o resto corrigido) alteraçõesqub_idatravés da pré-imagem. Um gateway não pode trocar a decisão pré-divulgação na data exibida na contagem decrescente sem invalidar a identidade qub. - A mudar
drand_round(com tudo o resto corrigido) alteraçõesqub_idatravés da pré-imagem. Um gateway não pode reassociar o texto cifrado de timelock a uma ronda diferente sem invalidar a identidade qub; combinado com a verificação da ronda de tempo de desbloqueio §8, o apresentadounlock_até a ronda que efetivamente controla a descodificação.
4.2 body_hash
body_hash = SHA3-256(body)
Onde body é a carga útil de conteúdo Vec<u8> bruta. Para qubs de texto, é o corpo do qub codificado em UTF-8.
4.2.1 title_hash
title_hash = SHA3-256(NFC(title).utf8_bytes) if title is present
title_hash = [0u8; 32] if title is absent
Onde title é o título opcional em texto puro exibido na contagem decrescente do leitor antes da revelação (ver §3.2). A normalização NFC é executada no momento do hash para que o digest seja estável entre sequências de code points visualmente equivalentes. O sentinela todo-zeros é reservado para o caso ausente; uma string vazia é rejeitada na fronteira do CBOR canónico como uma codificação não canónica de "ausente" (a codificação canónica omite o campo por completo).
4.3 Mapeamento de Desbloqueio-Ronda
drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
| Parâmetro | Fonte | Exemplo |
|---|---|---|
unlock_at |
Segundos Unix UTC escolhidos pelo utilizador | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
informações da cadeia drand (genesis_time) |
1595431050 |
chain_period_seconds |
informações da cadeia drand (period) |
30 |
Este é o mapeamento de bloqueio de referência (drand's CurrentRound). drand publica rodada N em chain_genesis_time + (N - 1) * chain_period_seconds, por isso a fórmula seleciona a corrente circular em unlock_at — a ronda cuja assinatura é a primeira que um espectador que chega unlock_at pode usar.
Propriedade de alinhamento (o caso que importa na prática): quando (unlock_at - chain_genesis_time) é exatamente divisível por chain_period_seconds, a assinatura da ronda selecionada é publicada exactamente às unlock_at, nunca antes. Isto aplica-se sempre ao deployment de referência: o tempo de génese da quicknet (1692803367) é divisível pelo seu período de 3 segundos, e as aplicações de referência fixam os tempos de desbloqueio do pin em minutos inteiros. Para um não alinhado unlock_at, a assinatura da ronda selecionada publica estritamente menos de um período antes unlock_at — a precisão temporal do compromisso é de um período de farol.
Mapeamento pré-lançamento legado e tolerância do lado de desbloqueio: o mapeamento original era ceil((unlock_at - chain_genesis_time) / chain_period_seconds), que—para o caso alinhado com o período acima—selecionou o publicado arredondado de um período completo antes unlock_at, tornando o texto cifrado decifrável cedo por exatamente um período. Os dois mapeamentos diferem exatamente por +1 quando o delta divide o período, e concordam de outra forma. Porque drand_round está incorporado no imutável qub_id pré-imagem (§4.1), artefactos selados sob o mapeamento legado não podem ser re-derivados; os verificadores que realizam a verificação cruzada da rodada do passo 6a do §8 DEVEM, portanto, aceitar um armazenado drand_round igual a ou a ronda derivada ou o derivado arredondado menos um (e DEVE exigir que o ciclo da estrofe tlock seja exatamente igual ao ciclo armazenado). A tolerância alarga a assinatura de desbloqueio mais cedo no máximo por um período. O serviço de preparação de pactos aplica a mesma tolerância quando re-deriva um pacto preparado qub_id (na fase e na co-assinatura): se a ronda do mapeamento atual não reproduzir o comprometido qub_id e o delta divide o período, ele tenta novamente com a rodada menos um, e sela o pacto finalizado para qualquer que seja a rodada a qub_id realmente se liga—nunca cegamente à rodada recalculada, o que tornaria o artefacto permanentemente indedutível.
Validação: unlock_at DEVE ser no futuro na hora do selo. unlock_at NÃO deve ter mais de 10 anos a partir de created_at (para limitar o risco de dependência de drand a longo prazo; a interface DEVE alertar para datas de desbloqueio superiores a 2 anos).
5. Newtypes do formato de wire
Os newtypes do formato de wire fornecem segurança em tempo de compilação contra a confusão de bytes CBOR com JSON, texto puro bruto ou outras codificações de bytes.
| Tipo | Contém | Produzido Por | Consumido Por |
|---|---|---|---|
SealedQubCbor |
CBOR Canónico de SealedQub | serialize_sealed_qub() |
Artefacto de fio interno; guardado nu para entrega pública ou envolto para entrega privada, depois recuperado pelo espetador |
QubEnvelopeCbor |
CBOR Canónico de QubEnvelope | serialize_qub_envelope() |
tlock encriptar entrada, tlock desencriptar saída |
5.1 Regras de construção
// 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 Validação na construção
from_encoded() DEVERIA validar que a entrada começa com um cabeçalho CBOR de mapa válido. A validação estrutural completa ocorre no momento do parse, não no momento da construção, para evitar parse duplicado.
6. Registo de tipos de conteúdo
| Valor | Tipo | Tamanho máximo do corpo | Notas |
|---|---|---|---|
0x00 |
Reservado (inválido) | — | NÃO DEVE ser usado |
0x01 |
Texto puro (UTF-8, Markdown restrito) | 50 KB pago / 10 KB grátis | Ver §10 para regras de renderização. A divisão grátis / pago é imposta pelo serviço de upload; o teto rígido na camada do protocolo é 50 KB. |
0x02 |
Reservado (futuro) | — | Alocado para um tipo de conteúdo futuro; inválido na v1. Os leitores DEVEM rejeitar conforme a regra abaixo. |
0x03 |
Pacto (acordo bilateral, corpo CBOR) | 100 KB | O corpo é PactTerms em CBOR canónico (§6.1). Co-assinatura por §9.7. |
0x04 |
Veredito (autoavaliação do criador, corpo CBOR) | 8 KB | O corpo é VerdictBody em CBOR canónico (§6.2). Emitido apenas pela intenção do lado do sistema verdict. A relação com o pai está na tag Arweave Parent-Tx-Id, não no corpo. Ver verdict-uplift-plan §3.4. |
Os leitores DEVEM rejeitar tipos de conteúdo desconhecidos com um erro claro visível ao utilizador. Os leitores NÃO DEVEM tentar renderizar tipos desconhecidos como texto.
6.1 Corpo de pacto (content_type = 0x03)
Um corpo de pacto é a codificação em CBOR canónico de um valor PactTerms:
PactTerms {
pact_version: u8, // 0x01 for structured/v1
title: String, // ≤ 200 bytes, NFC
terms: Vec<PactTerm>, // ≤ 20 rows
party_a: PartyIdentifier, // initiator
party_b: PartyIdentifier, // counter-signer
notes: Option<String>, // ≤ 5,000 bytes, NFC; absent key if none
}
PactTerm { key: String (≤ 100 bytes), value: String (≤ 2,000 bytes) } // NFC
PartyIdentifier{ label: String (≤ 100 bytes), contact: Option<String (≤ 320 bytes)> }
As ordens canónicas de chaves CBOR para os três mapas estão dadas em §3.2. O CBOR de pacto serializado total NÃO DEVE exceder 100 KB (corresponde a §6).
Discriminador de esquema. A primeira linha em terms para um pacto structured/v1 DEVE ser { key: "pact_schema", value: "structured/v1" }. Linhas sem esse marcador são pactos "custom" e não recebem validação estruturada nem renderização ciente do esquema.
Slots de reconhecimento congelados. Os pactos structured/v1 carregam exatamente quatro linhas de reconhecimento sob estas chaves:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
O value para cada uma é uma de oito strings em inglês congeladas, escolhidas pelo par (role, kind), onde role ∈ { seller, buyer, provider, client } e kind ∈ { standard, capacity }. As strings em si são dados normativos do protocolo — as assinaturas ML-DSA-65 de ambas as partes se comprometem com os bytes exatos via body_hash. Elas NÃO são localizadas; o corpo assinado é neutro em relação ao idioma. Qualquer alteração de redação requer uma nova versão de esquema (structured/v2).
As oito strings, sua busca (acknowledgement_for(role, kind)) e a justificativa de cada uma são fixadas pela implementação de referência. Implementações conformes DEVEM emitir valores de reconhecimento idênticos byte a byte; testes de body-hash SHA3-256 com fixtures-padrão cobrindo as quatro combinações de papéis detetam qualquer drift.
Ordem de exibição no leitor. As strings de reconhecimento contêm frases como "described above", o que pressupõe que as linhas de descrição / âmbito sejam renderizadas antes dos reconhecimentos. Os leitores DEVEM renderizar o array terms na ordem CBOR; reordenar quebra a semântica do texto.
Contacto da contraparte. Quando o contact da Parte B é um endereço de e-mail válido, o serviço de upload do qub despacha automaticamente um e-mail de convite para revisão / co-assinatura no momento do staging e vincula a co-assinatura subsequente à verificação desse mesmo endereço (§9.7). Pactos cujo contacto da Parte B é ausente ainda podem ser co-assinados, mas apenas por um canal fora de banda — o serviço recusa pedidos de co-assinatura que não consigam produzir um marcador de verificação de e-mail correspondente de 15 minutos.
6.2 Corpo de veredito (content_type = 0x04)
Um corpo de veredito é a codificação em CBOR canónico de um valor VerdictBody:
VerdictBody {
verdict_version: u8, // 0x01 for structured/v1
outcome: u8, // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
reflection: Option<String>, // ≤ 2,000 bytes NFC; "what changed, what did you learn"
evidence_url: Option<String>, // ≤ 2,048 bytes; HTTPS only; absent key when omitted
}
Ordem canónica das chaves CBOR:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
O CBOR de veredito serializado total NÃO DEVE exceder 8 KB (corresponde à linha do registo acima).
Enumeração do desfecho. O byte na wire é neutro em relação à intenção; as quatro categorias Right / Partial / Wrong / Unfalsifiable cobrem o espaço de desfecho de cada intenção que comporta veredito. Rótulos por intenção («Acertei» / «Cumpri» / «Lançado» / «Confirmada» para Right, etc.) são uma preocupação de renderização do lado do leitor, resolvida em função da intenção do qub pai — a wire mantém-se neutra em relação ao idioma e à intenção. Valores fora de 1..=4 DEVEM ser rejeitados na descodificação.
Vinculação ao pai. Um qub de veredito NÃO carrega a referência ao pai no seu corpo. O id de transação Arweave do qub pai é emitido como a tag de armazenamento Parent-Tx-Id no momento do upload (camada de tags de armazenamento, §7). Isto mantém o corpo como uma declaração assinada autocontida de autoavaliação; a cadeia de auditoria («certo sobre quê?») estabelece-se via a consulta da tag Arweave.
Segurança do URL de evidência (normativo). Quando evidence_url está presente, os validadores (lado da composição, lado da wire, edge do Worker) DEVEM impor:
- Apenas HTTPS. A string DEVE começar pela sequência de bytes
https://. Qualquer outro esquema —http,ftp,javascript,data,file, etc. — é rejeitado. - Limite de comprimento. ≤ 2 048 bytes (limite prático de URL nos navegadores).
- Verificação de NFC + codepoints hostis. Mesma regra que
titleereflection— codepoints de bidi-override / largura zero / tag-block / BOM / C0 / C1 são rejeitados. A definição corresponde à do Rustcrate::handle::contains_hostile_text_codepointe à do TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(manter sincronizados). - Sem espaços em branco, sem controlos ASCII. Espaços em branco / DEL / bytes abaixo de
0x20em qualquer ponto do URL são rejeitados — fecha o vetor de injeção de\n/\tque a regra de bidi não cobre. - Segmento de anfitrião não vazio. Tudo entre
https://e o primeiro/,?ou#DEVE ser não vazio.
Sem busca do lado do servidor. O Worker NÃO DEVE proxar, buscar nem pré-visualizar o URL. O protocolo armazena uma string; a renderização acontece do lado do leitor com rel="nofollow noopener noreferrer" target="_blank" e um anfitrião visível mostrado ao lado do texto do link.
Reflexão. Texto opcional de reflexão escrito pelo criador («o que mudou, o que aprendeu»). Mesma validação de NFC + codepoints hostis que title. Entrada vazia / só com espaços em branco recolhe-se para ausente no momento da construção.
Versão de esquema. A v1 suporta apenas verdict_version = 0x01. Revisões futuras de esquema incrementam este byte e chegam acompanhadas de uma nova versão de protocolo, conforme §12.
7. Protocolo de selamento
A sequência completa de selamento. Cada etapa é normativa.
1. User composes plaintext and metadata in ComposeQub.
2. Validate:
a. body is non-empty.
b. body size ≤ max for content_type and user tier (see §6).
c. unlock_at is in the future.
d. unlock_at ≤ created_at + 10 years.
e. content_type is a known, supported value.
f. visibility is 0x00 (private) or 0x01 (public).
3. Compute body_hash = SHA3-256(body).
4. Set created_at = current Unix seconds UTC.
5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
compute drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
(§4.3). (Computed here, before qub_id, because drand_round is bound into the
qub_id preimage—§4.1.)
6. Compute qub_id (see §4.1), folding in drand_round from step 5.
7. Construct QubEnvelope with all fields.
8. Serialise QubEnvelope using canonical CBOR → bytes B.
Assert: serialised output matches canonical profile (§3).
9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C and matching qub_id, version,
visibility, unlock_at, drand_chain_id, and drand_round.
11. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12. Select the delivery shape from visibility:
a. Private (0x00): generate K = 32 random bytes and N = 12 random bytes
using a CSPRNG. Compute W = wrap_sealed_qub(SealedQubCbor,
qub_id=qub_id, key=K, nonce=N) per §13. Upload payload = W.
b. Public (0x01): upload payload = bare SealedQubCbor; do not generate K.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit the selected upload payload to the qub upload service. For a private
browser seal, the service is byte-blind to the inner SealedQubCbor and never
receives K. The Builder `/api/v1/seal` route is an explicit exception: it
receives plaintext and caller-supplied K in memory, then persists neither.
16. Receive arweave_tx_id from the service. For private delivery, construct
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (or the equivalent short-code
path). For public delivery, omit the fragment. Browsers do not transmit URL
fragments to servers, so K from the browser-seal path is not observed by
qub.social or any storage gateway.
Camada de etiquetas de armazenamento (fora de banda). O serviço de upload qub anexa um conjunto deliberadamente pequeno de etiquetas de transação de armazenamento juntamente com o payload de upload selecionado. Content-Type=application/octet-stream é normativamente exigido. O serviço de referência anexa adicionalmente três etiquetas opcionais quando o criador opta por apresentá-las: Intent (intenção de composição validada em lista de permissões—announcement, thesis, prediction, letter, secret, commitment, proof, ou emitido pelo sistema verdict), Author (impressão digital da chave pública §9.3 do criador em hexadecimal minúsculo de 64 caracteres), e Parent-Tx-Id (ID de transação de armazenamento do qub pai para cadeias de resposta, 43 caracteres em base64url).
O Author etiqueta é opt-in por qub: a aplicação criadora de referências anexa-o apenas quando o utilizador ativa explicitamente a atribuição pública no momento da selagem. Quando o interruptor está desligado — o padrão — não Author a etiqueta é escrita e o qub não está atribuído na cadeia: nada no armazenamento permanente liga o upload ao identificador de um criador, e-mail ou outros qubs. Quando o interruptor está ligado, o Author a impressão digital resolve-se para o escolhido do criador @handle através da cadeia de atestação §9.5. Relações de cadeia de respostas e Intent não são identificáveis. Para entrega privada, a embalagem exterior (§13) encripta o conteúdo interno reconhecível SealedQub artefacto, por isso a colheita de invólucros armazenados e a obtenção de assinaturas públicas do drand ainda é insuficiente para recuperar o corpo sem K; as tags de armazenamento permanecem deliberadamente como metadados públicos.
O serviço de referência intencionalmente NÃO anexa as tags App-Name, App-Version ou Type: qualquer filtro de valor único desse tipo retornaria todo o corpus de qubs a uma consulta GraphQL, o que é inconsistente com o âmbito de confidencialidade somente do corpo do wrapper.
Um verificador conforme NÃO DEVE depender de qualquer tag de armazenamento para a verificação de terceiros do §11; o body hash / qub_id / assinatura comprometem-se apenas com o CBOR interno, nunca com o conjunto de tags.
8. Protocolo de desbloqueio
A sequência completa de desbloqueio. Cada etapa é normativa.
1. Viewer opens delivery URL. Extract arweave_tx_id from the path and retain
the optional URL fragment. Do not assume a missing fragment is an error:
public/bare delivery intentionally has no K.
2. Check denylist. If tx_id is denylisted → display block message. Stop.
3. Fetch the stored bytes (with multi-gateway fallback).
3a. Resolve the delivery shape structurally:
a. If the bytes parse as OuterWrapper, require a well-formed 32-byte K in
the URL fragment, require wrapper version 0x01, and unwrap per §13.
Any missing/malformed K or AEAD failure is a terminal error.
b. Otherwise require the bytes to parse as bare SealedQubCbor; no K is
required. If neither shape parses, report an integrity error.
4. Parse SealedQubCbor → SealedQub.
5. Validate: SealedQub.version is known (0x01), visibility is known, and the
delivery shape matches it (wrapped = 0x00 private; bare = 0x01 public).
Reject any mismatch or unknown value.
6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
6a. Round-binding check. Recompute expected_round from
SealedQub.unlock_at per §4.3. Reject unless SealedQub.drand_round ==
expected_round OR SealedQub.drand_round == expected_round - 1 (the
legacy pre-release mapping—see §4.3), AND the round baked into the tlock
ciphertext stanza (read via the age/tlock header, no signature required)
== SealedQub.drand_round exactly. The stanza round is the one that
actually gates decryption; without this check a malicious creator could
bind the ciphertext to an already-past round while displaying a future
countdown, so anyone reading the stored bytes could decrypt before
unlock_at. Implementations with no chain identity (test mocks) skip this
check.
7. Once current time ≥ SealedQub.unlock_at:
a. Fetch drand round signature for SealedQub.drand_round from drand network.
b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
8. Parse B → QubEnvelope.
9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
Fail → integrity error.
12a. Verify: QubEnvelope.outcome_at == SealedQub.outcome_at (both absent, or
both present and equal). Fail → integrity error.
12b. Content re-derivation. Recompute qub_id per §4.1 from the decrypted
fields — (QubEnvelope.version, content_type, created_at, unlock_at,
outcome_at, SealedQub.drand_round, QubEnvelope.body_hash,
title_hash(SealedQub.title)) — and verify it equals SealedQub.qub_id.
Fail → integrity error. The pairwise checks in steps 10-12a only prove
the two layers agree with EACH OTHER; a forger who rewrites a bound
field consistently on both surfaces (a pre-reveal title swap, or a
post-round body swap with a recomputed body_hash re-encrypted to the
same round under the same qub_id) passes them all. Only re-deriving
the identity from content closes this.
13. Verify: QubEnvelope.content_type is known and renderable.
Known values: 0x01 (text), 0x03 (pact), 0x04 (verdict).
Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using the appropriate renderer (see §10 for text and §6 for pact/verdict).
17. Construct RevealedQub for display.
9. Assinatura de autoria
9.1 Justificativa
Os qubs são armazenados num armazenamento permanente. As assinaturas de autoria devem permanecer infalsificáveis indefinidamente, razão pela qual a v1.0 usa o esquema pós-quântico ML-DSA-65 (FIPS 204) em vez de um esquema clássico cuja segurança pode degradar ao longo da vida útil permanente do qub.
9.2 Registo de algoritmos
sig_alg |
Esquema | Tamanho da Chave | Tamanho da Assinatura | Estado |
|---|---|---|---|---|
0x00 |
Sem assinatura (não assinado) | — | — | Ativo |
0x01 |
ML-DSA-65 (FIPS 204) | 1.952 bytes | 3.309 bytes | Ativo |
0x02 |
Ed25519 | 32 bytes | 64 bytes | Constante reservada; não suportada no protocolo v1 |
Os visualizadores do Protocolo-v1 DEVEM rejeitar todos os valores fora {0x00, 0x01}, incluindo
o reservado 0x02 valor. A reserva previne a reutilização acidental; não é
activação. Ativá-la requer a mudança governada descrita no §15.
9.3 Construção da pré-imagem assinada
Existiram duas versões da pré-imagem. Todas as assinaturas DEVEM usar a V2, e os verificadores DEVEM aceitar apenas a V2. A pré-imagem V1 legada (documentada abaixo para referência histórica) foi aceite como recurso de verificação apenas durante a migração para a V2; esse recurso foi descontinuado e uma assinatura só-V1 é agora rejeitada.
V2 (atual — produzida por toda a nova assinatura de autoria e por ambas as assinaturas do fluxo de staging / co-assinatura de pactos):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V2" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 || // u8 (1 byte): MUST be 0x00 in v1.x
sender_label_hash || // [u8; 32]: SHA3-256(NFC(sender_label)),
// or 32 zero bytes when absent
reply_to_or_zero // [u8; 32]: parent qub_id, or 32 zero
// bytes when absent
)
// Total preimage: 155 bytes → 32-byte hash
signature = Sign(author_secret_key, sig_input)
sender_label_hash segue a mesma convenção de sentinela-de-ausência que title_hash (§4.2.1): 32 bytes zero não são uma saída SHA3-256 válida, pelo que «ausente» nunca pode colidir com um rótulo presente. Todos os campos têm largura fixa, pelo que a pré-imagem é inequívoca sem prefixos de comprimento.
V1 (legada — DESCONTINUADA; já não é produzida nem aceite na verificação):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V1" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 // u8 (1 byte): MUST be 0x00 in v1.0
)
// Total preimage: 91 bytes → 32-byte hash
A pré-imagem V1 omitia sender_label e reply_to. Foi aceite como recurso de verificação apenas durante a migração para a V2; esse recurso foi entretanto descontinuado — os verificadores DEVEM aceitar apenas a pré-imagem V2. A definição é aqui mantida para referência histórica e para explicar o separador de domínio abaixo. Uma assinatura que apenas se verifique contra a V1 DEVE ser tratada como uma falha de verificação.
Separadores de domínio: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" correspondem a 17 bytes ASCII cada ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Sem padding. O separador distinto separa por domínio as duas construções, pelo que uma assinatura sobre uma pré-imagem nunca pode verificar-se como a outra.
Byte org_id_present: o byte que se segue a unlock_at DEVE ser 0x00. A implementação de referência expõe-no como a constante ORG_ID_PRESENT_INDIVIDUAL = 0x00 em crates/qub-core/src/signing.rs; os leitores que reconstroem sig_input para verificação DEVEM emitir o mesmo byte.
Âmbito da assinatura — o que está e o que não está coberto. O sig_input V2 compromete-se diretamente com version, qub_id, body_hash, unlock_at, sender_label e reply_to (mais o separador de domínio fixo e o byte org_id_present). O próprio qub_id é derivado de version, content_type, created_at, unlock_at, outcome_at, drand_round e body_hash via a pré-imagem do §4.1, pelo que qualquer alteração nesses campos produz um qub_id diferente e invalida a assinatura transitivamente. A superfície autenticada é, portanto:
| Campo | Autenticado por assinatura | Como |
|---|---|---|
version |
✓ | Entrada direta para sig_input |
qub_id |
✓ | Entrada direta |
body_hash |
✓ | Entrada direta |
unlock_at |
✓ | Entrada direta |
sender_label |
✓ | Entrada direta via sender_label_hash (Pré-imagem V2 — a única forma aceite) |
reply_to |
✓ | Entrada direta via reply_to_or_zero (Pré-imagem V2 — a única forma aceite) |
content_type |
✓ | Transitivamente, via qub_id pré-imagem |
created_at |
✓ | Transitivamente, via qub_id pré-imagem |
outcome_at |
✓ | Transitivamente, via qub_id pré-imagem |
drand_round |
✓ | Transitivamente, via qub_id pré-imagem |
body |
✓ | Transitivamente, via body_hash = SHA3-256(body) |
author_pubkey |
— (implícito) | A chave que verificou a assinatura é do autor, por definição |
cosigner_pubkey / cosigner_signature |
— | Assinado de forma independente sobre o mesmo sig_input (ver §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Exterior SealedQub campos, não dentro do envelope — cobertos pelos seus próprios invariantes estruturais (consistência de ronda / cadeia) mas não pela assinatura do autor. (drand_round está agora ligado transitivamente através do qub_id preimage — veja acima.) |
Porque a V2 é a única pré-imagem aceite.
- Sob a pré-imagem V1 descontinuada, uma parte com acesso de escrita aos bytes armazenados poderia trocar
sender_label(«Alice» → «Mallory») ou reparentarreply_to— e voltar a cifrar após o round — sem invalidar a assinatura do autor, porque nenhum dos campos estava na pré-imagem assinada. A V2 cobre ambos, pelo que qualquer alteração a qualquer um dos campos faz a verificação passar a «falhada». Como os verificadores aceitam agora apenas a V2, esta troca fica fechada para todas as assinaturas: uma assinatura que não vincule nenhum dos campos (ou seja, que apenas se verifique contra a V1) é rejeitada por completo, em vez de se recorrer a ela. - O
author_pubkeydentro do envelope permanece a verdadeira âncora de identidade — os leitores DEVEM derivar a identidade exibida a partir deauthor_pubkey(via a camada de atestação do §9.5) em vez de confiar emsender_label.
As implementações que exibem sender_label ou reply_to a utilizadores finais DEVEM expor a identidade autenticada (impressão digital da pubkey, atestação) como sinal primário de identidade, não o rótulo.
9.4 Procedimento de verificação
1. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
only accepted form — the legacy V1 fallback is retired (§9.3), so a
signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
A verificação de assinatura é a operação mais cara (especialmente ML-DSA-65). DEVERIA ser realizada após todas as verificações mais baratas (hash, qub_id, unlock_at) terem passado.
9.5 Atestações de identidade
As atestações de identidade — o mapeamento de author_pubkey para reivindicações de identidade reconhecíveis por humanos, como um handle qub, endereço de e-mail, handle social ou credencial passkey — são um aprimoramento progressivo do lado do leitor e não são exigidas para a verificação da assinatura. Os leitores que resolvem atestações para uma identidade exibida DEVEM aplicar a precedência:
handle > email > social > fingerprint
O fallback de fingerprint é o hex minúsculo de SHA3-256(author_pubkey); está sempre disponível para qualquer qub assinado. Os leitores PODEM abreviá-lo para exibição — o leitor de referência renderiza qub: seguido dos primeiros e últimos quatro bytes (qub:<8 hex>…<8 hex>).
Um verificador conforme pode concluir todas as verificações em §9.4 sem contatar a API qub, sem qualquer rede além do armazenamento permanente e do drand, e sem qualquer consulta do lado do servidor. A resolução de atestação é uma etapa separada de melhor esforço, realizada apenas após o sucesso da verificação da assinatura.
9.6 Impacto de tamanho
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Assinatura | 64 bytes | 3.309 bytes |
| Chave pública | 32 bytes | 1.952 bytes |
| Total por qub | 96 bytes | 5.261 bytes |
| Delta de custo de armazenamento (a ~$5/MB) | ~$0,0005 | ~$0,026 |
Para um qub de texto de 500–2.000 bytes, ML-DSA-65 aproximadamente triplica o tamanho armazenado. O custo absoluto é desprezível.
9.7 Verificação de co-assinante (acordos bilaterais de pacto)
Para acordos bilaterais (content_type = 0x03), uma segunda camada de assinatura prova que ambas as partes consentiram com os mesmos termos.
Campos do envelope:
cosigner_pubkey: chave pública ML-DSA-65 do co-assinante (Parte B).cosigner_signature: assinatura sobre o mesmosig_inputque o autor (§9.3).
Ambos os campos DEVEM estar presentes juntos ou ambos ausentes. Se exatamente um estiver presente, os leitores DEVEM relatar um erro de integridade.
Procedimento de verificação:
1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3 (V2 only — the
legacy V1 fallback is retired; all pact clients produce V2 signatures).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."
Propriedades:
- O co-assinante assina o mesmo
sig_inputque o autor — ambas as partes comprometem-se com o mesmoqub_id,body_hasheunlock_at(e, sob a V2, o mesmosender_label_hashereply_to_or_zero). - Para permitir que a contraparte reconstrua a pré-imagem V2 sem acesso aos bytes brutos do envelope, o serviço de staging impõe, no momento do staging, que o
sender_labelde um envelope de pacto seja igual apact_terms.party_a.labele quereply_toesteja ausente. Ambas as condições se verificam para todos os pactos do cliente de referência; os envelopes que as violem são rejeitados no staging. - A derivação de
qub_id(§4.1) NÃO inclui campos de co-assinante. Adicionar um co-assinante a um envelope existente não altera oqub_id. - Um pacto pode estar assinado apenas pelo autor (compromisso unilateral), apenas pelo co-assinante (incomum), ou por ambos (prova bilateral completa).
Gate de vinculação de e-mail (operacional). Quando um pacto em staging carrega um contacto de e-mail de Parte B (§6.1), o serviço de upload do qub DEVE recusar a solicitação de co-assinatura, a menos que exista um marcador de verificação de e-mail de curta duração que combine tanto o id de staging como o hash de e-mail normalizado desse contacto. O marcador é gravado por /api/v1/auth/verify quando o token do magic-link carrega um staging_id e o endereço verificado coincide com SHA-256(normalise_email(party_b.contact)) — onde normalise_email(addr) preserva a caixa da parte local e converte para minúsculas apenas a parte do domínio (conforme RFC 5321 §2.3.11), e SHA-256 aqui é o hash NIST FIPS 180-4 (distinto do SHA3-256 usado nas derivações do §4) — e expira 900 segundos (15 minutos) após a emissão. Este é um gate operacional anti-impersonação, NÃO parte da prova on-chain do qub — um verificador de terceiros que reproduz o §11 precisa apenas do armazenamento permanente e do drand, sem qualquer consulta do lado do servidor. O marcador existe apenas no servidor e nunca faz parte do corpo assinado.
Impacto de tamanho (autor + co-assinante ML-DSA-65):
| Componente | Tamanho |
|---|---|
| Assinatura do autor | 3.309 bytes |
| Chave pública do autor | 1.952 bytes |
| Assinatura do co-assinante | 3.309 bytes |
| Chave pública do co-assinante | 1.952 bytes |
| Sobrecarga cripto total | 10.522 bytes |
| Delta de custo de armazenamento | ~$0,05 |
10. Renderização e sanitização de Markdown
Esta seção é crítica para a segurança. O leitor renderiza qubs de texto (content_type = 0x01) usando um subconjunto restrito de Markdown.
10.1 Elementos permitidos
- Cabeçalhos:
#até####(sem#####nem######) - Ênfase: negrito (
**), itálico (*), tachado (~~) - Listas: ordenadas (
1.) e não ordenadas (-,*) - Citações em bloco (
>) - Código: spans inline (```) e blocos cercados (`````)
- Linhas horizontais (
---) - Quebras de linha (dois espaços ao final ou linha em branco)
- Parágrafos
10.2 Elementos proibidos
| Elemento | Tratamento |
|---|---|
HTML bruto (<div>, <script>, etc.) |
Removido por completo. Nenhum HTML passa adiante. |
Imagens () |
Removidas. A sintaxe de imagem é eliminada da saída. |
Links ([text](url)) |
A URL é renderizada como texto puro visível. Não auto-vinculada. Não clicável sem ação explícita do utilizador. |
| Esquemas de URL perigosos | javascript:, data:, vbscript:, file: — removidos. |
| Iframes, embeds, objetos | Removidos. |
| Entidades HTML | Decodificadas para caracteres exibidos apenas se forem seguras. |
10.3 Implementação
As implementações DEVEM usar um parser de allowlist estrito, não uma blocklist. A abordagem recomendada:
- Fazer o parse do Markdown usando
pulldown-cmark(ou equivalente). - Percorrer a AST e descartar qualquer nó fora da allowlist (§10.1).
- Para nós de link: emitir a URL como texto visível, não como elemento
<a>clicável. - Converter a AST filtrada numa representação intermediária tipada (por exemplo, um enum
MarkdownNodecom apenas variantes seguras). O HTML bruto é estruturalmente irrepresentável nesta IR. - Renderizar a partir da IR tipada para a camada de view de destino (por exemplo, componentes de view reativos, nós DOM). Nunca há concatenação de strings HTML ou
innerHTMLem ponto algum.
Abordagens de blocklist são frágeis porque novas extensões de Markdown ou peculiaridades de parser podem introduzir elementos não filtrados. A abordagem de AST tipada torna o XSS estruturalmente impossível — não há variante que possa carregar HTML arbitrário.
10.4 Limites de tamanho e estrutura
- Profundidade máxima de cabeçalho renderizada:
####(H4).#####e mais profundos são renderizados como texto em negrito. - Sem limite na quantidade de parágrafos (os limites de tamanho de corpo em §6 são a restrição).
- Blocos de código cercados: sem realce de sintaxe no MVP. Renderizados como texto pré-formatado monoespaçado.
11. Verificação por Terceiros
Qualquer terceiro que detenha os bytes armazenados (e K para um qub privado/envelopado) pode verificar o artefacto criptográfico sem a cooperação do qub. De forma independente com carimbo de data/hora existência a reivindicação adicional requer igualmente verificação inclusão em armazenamento permanente per-qub ou uma prova verificada §16 num registo de transparência.
1. Obtain the stored bytes. For a private delivery, also obtain K from the
delivery-link fragment.
2. Resolve the delivery shape (§8 step 3a): unwrap OuterWrapper with K, or
accept bare SealedQubCbor. Require shape/visibility agreement.
3. Parse SealedQubCbor → SealedQub; validate protocol version, visibility,
content type, chain identity, and structural bounds.
4. Recompute expected_round from unlock_at (§4.3); require the stored round
(allowing the documented legacy minus-one case) and ciphertext-stanza round
to agree exactly as §8 step 6a specifies.
5. Obtain the drand signature for SealedQub.drand_round and verify its BLS
signature against the pinned chain public key.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify envelope/sealed equality for qub_id, unlock_at, and outcome_at.
10. Recompute qub_id from the decrypted fields, sealed drand_round, and title;
require it to equal the carried qub_id (§4.1).
11. If sig_alg != 0x00, verify the V2 author signature (§9.4). If cosigner
fields are present, verify their pairing, key separation, and signature
(§9.7).
12. For an existence-time claim, independently verify either:
a. the permanent-storage transaction's data-to-id binding, owner, block
inclusion, and block timestamp; or
b. a §16 inclusion proof through its pinned anchor and anchor block time.
13. Report each verified claim separately; do not collapse an absent storage
or anchor proof into a successful timing verdict.
O que a verificação prova:
| Prova de entrada | O que estabelece |
|---|---|
| Pacote válido / artefacto selado + assinatura drand | O corpo recuperado corresponde body_hash; os metadados incorporados qub_id está intacto; o texto cifrado está ligado à ronda drand declarada; e essa ronda já decorreu. Isto faz não determinar quando o texto cifrado foi criado. |
| Assinatura válida do autor/co-assinante V2 | O(s) titular(es) da(s) chave(s) secreta(s) correspondente(s) autenticou(aram) a superfície assinada na §9.3. |
| Transação de armazenamento por-qub verificada de forma independente | O texto cifrado armazenado exato existia no mais tardar na sua marca temporal de bloco. |
| Prova de transparência com âncora válida | A reivindicação específica do tipo de folha no §16.11, incluindo um tempo de compromisso de limite superior a partir do bloco âncora. |
O que a verificação NÃO prova:
| Não à prova | Porquê |
|---|---|
| Autoria | O sender_label é decorativo. Sem sig_alg ≥ 0x01, qualquer pessoa poderia ter selado este conteúdo. |
| Intenção | O artefacto prova bytes e relações criptográficas, não o que o criador subjectivamente quis dizer. |
Compromisso pré-existente de .qub sozinho |
Um criador pode montar um pacote válido depois de ter passado o período limitado. A assinatura drand incorporada prova que o período passou, não que o texto cifrado existia antes dele. |
| Tempo exacto do botão de selo | Um carimbo temporal de bloco de armazenamento ou âncora é um limite superior verificável de forma independente, e pode atrasar em relação à ação local do utilizador. sealed_at / received_at as afirmações não são evidenciais. |
O registo de transparência implementado (§16) estende a verificação através dos qubs com
à prova de violação encomenda e sem confiança tempo de compromisso de limite superior (o
tempo do bloco de âncora), limitado pelo tipo de folha (§16.11). Não adiciona autoria ou
intenção; para o caminho de upload padrão cego a bytes, não prova por si só
body_hash ou drand_round, que continuam a vir das verificações de artefactos.
12. Controlo de Versão e Lançamento
As versões do documento, o protocolo do fio interno e o invólucro externo são separados espaços de versão. Uma clarificação apenas em documento, portanto, não silenciosamente mudar bytes, e uma futura migração por cabo não pode disfarçar-se de editorial revisão.
12.1 Versão de Lançamento do Documento
Esta especificação utiliza lançamentos semânticos de documentos (MAJOR.MINOR.PATCH) e
uma tag Git imutável chamada protocol-v<release>.
- PATCH: precisão ou correção editorial que não altera bytes conformes ou o comportamento exigido.
- MENOR: adição normativa compatível com versões anteriores, nova entrada de registo ou novo formato sidecar versionado independentemente.
- MAIOR: alteração normativa incompatível, incluindo uma nova interpretação obrigatória do fio.
O estado de lançamento é um dos Rascunho (ainda não normativo), Atual (o único
alvo de implementação recomendado), ou Substituído (retido por motivos históricos
verificação). O não versionado /protocol a rota mostra a versão atual;
a etiqueta de lançamento preserva a sua fonte exacta e cada local publicado com ela.
Alterar o estado ou o número de versão requer a atualização desta tabela e da versão
história na mesma alteração revista.
| Lançamento do documento | Data de vigência | Estado | Protocolo de ligação | Embalagem | Fonte |
|---|---|---|---|---|---|
| 1.0.0 | 23-09-2026 | Atual | 0x01 |
0x01 |
protocol-v1.0.0 |
12.2 Versão do Protocolo
O version campo (u8) em ambos SealedQub e QubEnvelope identifica a versão principal do protocolo.
- Os visualizadores DEVEM rejeitar versões principais desconhecidas com um erro claro.
- Dentro de uma versão principal conhecida, os descodificadores DEVEM rejeitar chaves de mapa desconhecidas (§3.1) — a evolução do esquema acontece pela introdução de uma nova
version, não adicionando chaves que os decodificadores existentes ignorariam. (Revisões anteriores desta especificação permitiam tolerar campos opcionais desconhecidos; essa cláusula foi retirada — issoencode(decode(x))não-injetivo e abriu um vetor de conteúdo assinado oculto nos payloads do pacto.) - Tipos de conteúdo (
content_type) e esquemas de assinatura (sig_alg) são controlados por versão: novos valores só podem ser introduzidos juntamente com uma nova versão do protocolo ou uma atualização explícita do registo.
12.3 Histórico da Versão do Protocolo
| Versão | Valor | Descrição |
|---|---|---|
| v1 | 0x01 |
Entrega privada/envolvida e pública/nua; texto (0x01), pacto (0x03), e veredicto (0x04) corpos; autor/cossignatário ML-DSA-65 V2 a assinar; drand quicknet tlock; SHA3-256. |
12.4 Compatibilidade Futura
Um visualizador v1 que se deparar com um QubEnvelope com chaves de mapa CBOR desconhecidas (chaves que não estão na ordem canónica §3.2) DEVE rejeitá-lo com um erro de decodificação (§3.1). A compatibilidade futura depende de version campo, não sobre a tolerância da chave: futuras adições — mesmo metadados menores — são incluídas sob uma nova version valor, que um visualizador v1 rejeita com um erro claro de "protocolo mais recente" em vez de ignorar silenciosamente o conteúdo ao qual as assinaturas se comprometem.
Um espectador da v1 a encontrar sig_alg = 0x01 (ML-DSA-65) mas sem suporte de verificação ML-DSA-65 DEVE exibir o conteúdo qub com um aviso 'assinatura presente mas não verificável', não rejeitar o qub completamente. A implementação de referência atual rejeita todos sig_alg valor diferente de 0x00 e 0x01 porque o registo v1 não contém nenhum outro algoritmo válido — a rejeição rigorosa e a falha suave são observacionalmente idênticas até que um terceiro algoritmo seja registado. O comportamento de falha suave acima torna-se funcional uma vez que o §9.2 admita uma nova entrada, e o visualizador de referência será atualizado para falha suave nesse ponto.
12.5 Versão do Envoltório Externo
O OuterWrapper descrito na §13 transporta o seu próprio version byte, independente de SealedQub.version e QubEnvelope.version. Os dois espaços de versões evoluem separadamente: uma futura substituição simétrica segura contra computadores quânticos atualiza o byte do invólucro sem tocar na versão interna do protocolo, e uma futura adição a nível de protocolo (por exemplo, um novo campo no envelope) atualiza a versão interna sem tocar no byte do invólucro.
OUTER_WRAPPER_VERSION_* |
Valor | Algoritmo | Estado |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM com nonce de 12 bytes, etiqueta de autenticação de 16 bytes, AAD ligado a qub_id |
Ativo para entrega privada |
| — | 0x02–0xFF |
Reservado | Futuro |
Os espectadores DEVEM rejeitar versões de empacotamento desconhecidas com um erro claro. O protocolo mantém intencionalmente o espaço de versões de empacotamento limitado até surgir um motor de migração concreto (por exemplo, orientações do NIST a favorecer um AEAD diferente); a 0x02 O slot será atribuído na mesma revisão que introduz o algoritmo.
13. Wrapper externo de cifragem
13.1 Justificativa
As camadas do protocolo (QubEnvelope → tlock → SealedQub) tornam um qub selado com bloqueio temporal: o corpo é ilegível até unlock_at e até que a assinatura do round drand tenha sido publicada. Após o desbloqueio, no entanto, a assinatura do round é pública e o formato canónico CBOR de SealedQub é reconhecível, então um harvester que indexasse transações de armazenamento poderia decifrar em massa todo o corpus de qubs.
Para entrega privada, o invólucro externo de encriptação fecha esse canal interpondo uma camada AEAD simétrica adicional entre o canónico SealedQubCbor e os bytes armazenados. No caminho browser-seal, a chave de 256 bits K vidas apenas no fragmento da URL do URL de entrega e nos dispositivos dos utilizadores; os navegadores não transmitem fragmentos de URL para os servidores, por isso qub.social, cada gateway de armazenamento e cada CDN à frente de qualquer um deles estão observacionalmente cegos a K. A representação armazenada de um qub privado é, portanto, um texto cifrado opaco cujo texto simples é irrecuperável sem a URL que o criador escolheu partilhar. A entrega pública omite deliberadamente esta camada (§13.8).
Efeito líquido:
- Resistência à enumeração para entrega privada.
OuterWrapperpermanece como CBOR estruturado reconhecível—não é literalmente indistinguível de bytes aleatórios—mas o seu campo de texto cifrado oculta o interior reconhecívelSealedQubforma. A estratégia documentada de recolha de 'GraphQL-consulta para carregamentos com forma de qub simples, decifrar em massa com assinaturas públicas drand' não termina com texto simples sem K. - Postura de privacidade de eliminação encriptada para o fluxo padrão do navegador privado. qub.social não consegue decifrar aqueles artefactos armazenados a partir dos seus dados no servidor por defeito. A recuperação explícita, a entrega pública e a selagem confiável no servidor têm diferentes limites de confiança divulgados.
- Escada de confidencialidade de dois níveis. Padrão = acesso controlado por link (esta secção). Qubits privados encriptados pelo destinatário (uma funcionalidade reservada da Fase 2, ainda não especificada) sobrepõem-se como o segundo nível.
13.2 Camadas
plaintext body ← QubEnvelope.body (§2.2)
↓ canonical CBOR (§3)
envelope CBOR
↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
↓ canonical CBOR (§3)
SealedQubCbor bytes ← inner wire artifact
├─ public (visibility=0x01) ───────────────▶ stored directly (§13.8)
└─ private (visibility=0x00)
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12, this section)
OuterWrapper CBOR bytes ← stored private payload
Selamento e desbloqueio na camada do protocolo (§7, §8) permanecem inalterados abaixo da fronteira do wrapper; o wrapper se conecta no ponto de chamada de seal() e se desconecta no ponto de chamada de unlock().
13.3 Estrutura de dados do OuterWrapper
struct OuterWrapper {
version: u8, // 0x01, see §12.5
qub_id: [u8; 32], // copied from inner SealedQub; AEAD AAD
nonce: [u8; 12], // 96-bit AEAD nonce
ciphertext: Vec<u8>, // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}
Invariantes dos campos.
versionDEVE ser igual0x01para bytes do wrapper v1.0.qub_idDEVE ser igual aqub_idcampo do SealedQub recuperado após a desembrulhar. Ambas as referênciaswrap_sealed_qubeunwrap_sealed_qubanalisar o CBOR interno e impor esta igualdade diretamente; a ligação AAD separadamente torna a adulteração pós-envolvimento do exteriorqub_idfalhar na autenticação.nonceDEVE ter 96 bits (12 bytes), gerados de forma nova por um CSPRNG para cada operação de encapsulamento. Reutilizar um nonce com a mesma chave permite ataques de reutilização de nonce AEAD que recuperam o texto plano; os produtores DEVEM tratar (key,nonce) pares como treino único.ciphertexté a saída do AES-256-GCM: bytes do texto cifrado concatenados com a etiqueta de autenticação de 16 bytes.ciphertext.len() == SealedQubCbor.len() + 16exatamente.
Codificação CBOR. CBOR canónico conforme §3, com a mesma regra de ordenação de chaves (ordenado por comprimento codificado em bytes ascendente, depois lexicograficamente). As quatro chaves são:
| Chave | Bytes codificados | Ordem |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
O primeiro byte do CBOR do OuterWrapper é, portanto, o cabeçalho de mapa de comprimento definido para um mapa de 4 entradas (0xA4).
13.4 Vinculação AAD ao qub_id
O wrapper vincula qub_id como dado autenticado adicional do AEAD. Esta é a defesa estrutural carregadora contra três classes de ataque:
| Ataque | Defesa |
|---|---|
Mover o texto cifrado para outro qub_id campo no invólucro |
Incompatibilidade AAD → falha na autenticação AEAD |
| Misture o fragmento de URL do qub A com os bytes armazenados do qub B | Chave errada (e AAD ligada independentemente) → falha na autenticação AEAD |
Mexer com o qub_id campo do invólucro após o envio |
Incompatibilidade AAD → falha na autenticação AEAD |
Carregar qub_id no plaintext do wrapper não enfraquece a imunidade à enumeração de forma significativa — qub_id é em si um hash SHA3-256 da pré-imagem do §4.1 sem pré-imagem recuperável a partir do digest, e um enumerador que já tenha colhido os bytes do wrapper não aprende nada com o qub_id visível que não pudesse inferir da própria existência do upload.
13.5 Algoritmos de wrap e unwrap
wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
require K.len() == 32 and N.len() == 12 and Q.len() == 32
I := canonical_cbor_decode(S) as SealedQub
require I.qub_id == Q // reject mismatched caller AAD
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C includes the 16-byte authentication tag at the end
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
require K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
require O.version == 0x01 // §12.5
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
S := canonical_cbor_decode(P) as SealedQub
require S.qub_id == O.qub_id // explicit inner/outer cross-check
return P // P is the validated SealedQubCbor
Colapso de modos de falha. K errado, nonce errado, incompatibilidade de AAD e ciphertext adulterado produzem todos o mesmo erro DECRYPT_FAILED. Esta é uma propriedade AEAD deliberada: distinguir o modo de falha criaria um canal lateral que um atacante remoto poderia sondar enviando wrappers malformados e cronometrando a resposta. Implementações de referência DEVEM colapsar todas as falhas AEAD num único formato de erro.
13.6 Material de chave e distribuição
A chave de empacotamento K é um valor uniforme aleatório de 256 bits gerado por qub por um CSPRNG. As implementações de referência o obtêm de:
- Criador WASM:
getrandom(WebCrypto sob o backendwasm_js). - Chamador da API de selamento server-side: o seu CSPRNG local; o chamador fornece e retém
Kcomowrapper_key_b64url. O Worker usaKem memória para o wrapper, mas NÃO DEVE persisti-la. Isto permite que uma nova tentativa idempotente recupere uma resposta expurgada usando a capacidade retida pelo chamador, em vez de depender de um segredo de uso único gerado pelo servidor.
Distribuição: K DEVE ser codificado como base64 URL-safe (RFC 4648 §5, sem padding) e anexado à URL de entrega como o componente de fragmento:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
O fragmento nunca é transmitido a qualquer servidor por um navegador conforme. Canais de recuperação (índice de histórico do servidor, envio automático opt-in por e-mail) que persistem o link de entrega completo — incluindo o fragmento — além do dispositivo do utilizador são uma troca explícita contra a postura padrão de crypto-shredding e DEVEM ser condicionados a consentimento explícito do utilizador.
Perda de fragmento. Se um utilizador perder o fragmento da URL e não tiver canal de recuperação, o qub fica ilegível. Esta é a troca carregadora do design e DEVE ser divulgada ao utilizador no momento do selamento. O MVP reforça a divulgação no momento do selamento com uma cópia explícita "guarde esta URL" e um canal de recuperação por e-mail verificado para utilizadores que optarem por participar.
13.7 Fora do âmbito desta seção
- A assinatura de autoria (§9) permanece inalterada: as assinaturas são calculadas no interior do interno
QubEnvelopee são recuperados após descompactar → descriptografar tlock → analisar CBOR. - Criptografia com chave pública do destinatário (o reservado
recipient_pubkeycampo) é uma funcionalidade futura distinta do modo atual de invólucro, privado e limitado por capacidade de ligação. - O fluxo actual de coassinatura do pacto do lado do servidor emite público/desnudo
SealedQubCborcom visibilidade0x01; não pode satisfazer o modelo de K-secrecy apenas para navegadores porque o selamento final ocorre após a coassinatura mediada pelo servidor. Um futuro produtor de pacto privado pode usar o mesmo invólucro, que é cego aos bytes do tipo de conteúdo interno.
13.8 qubs públicos (omissão do wrapper)
O invólucro exterior é opcional na camada de entrega. Um criador pode selar um qub como público, caso em que o canónico SealedQubCbor entra no pipeline de armazenamento diretamente, sem OuterWrapper camada e nenhuma tecla K:
SealedQubCbor bytes ──(public)──▶ stored as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ stored
Um bar público é bloqueado por tempo mas não restrito por link: permanece ilegível até que a sua ronda de distribuição seja publicada (a camada tlock permanece inalterada), mas após a desbloqueio qualquer pessoa que tenha o ID da transação de armazenamento pode decifrá-lo — nenhum fragmento de URL é necessário, porque não há K. Esta é a troca deliberada para superfícies que o servidor deve gerir: emails de notificação de revelação, links oEmbed/auto-embed sem fragmentos, e SEO mais rico após a revelação de publicações, todos precisam de um link que funcione sem um segredo que o servidor nunca possui (§13.6). Um qub privado ainda pode usar o explícito <qub-embed src="full_delivery_url"> forma quando o editor fornece a sua capacidade completa de transporte de fragmentos.
Consequências que um produtor DEVE considerar:
- Sem imunidade de enumeração. Os qubs públicos renunciam à propriedade de imunidade à enumeração §13.1 por construção. O serviço de upload de referência carimba um
Visibility: publicetiqueta de armazenamento permanente neles (e apenas neles) para que sejam intencionalmente descobríveis; os qubs privados não possuem tal etiqueta e mantêm a sua indistinção a nível de bytes. - Título em texto simples exposto no momento da selagem. O §3.2
titleo campo está em texto simples por dentroSealedQubCbor. Sob o invólucro está escondido até que um espectador forneçaK; sem o invólucro, é legível mundialmente no armazenamento permanente a partir do momento de upload, antes de desbloquear. As aplicações do criador em conformidade DEVEM divulgar isto no momento da selagem. - A deteção é estrutural e verificada cruzadamente. Um visualizador/incorporador conforme distingue as duas formas armazenadas pela análise: bytes que se analisam como
OuterWrapperpegar o desembrulhar-com-Kcaminho; bytes que analisam como um nuSealedQubCborsão aceites diretamente. O valor interno recuperado DEVE concordar (0x00para embrulhado/privado,0x01para nu/público).qub_idnão vincula a visibilidade, mas o canónicoSealedQubbytes transportam-no, por isso as codificações internas públicas e privadas não são idênticas em bytes.
Privado (empacotado) permanece o padrão; público é uma escolha explícita do criador por qub.
14. Vetores de teste
14.1 Derivação de qub_id
Input:
version = 0x01
content_type = 0x01
created_at = 1735689600 (2025-01-01 00:00:00 UTC)
unlock_at = 1736294400 (2025-01-08 00:00:00 UTC)
outcome_at = absent
drand_round = 4695446 (= floor((1736294400 - 1595431050) / 30) + 1, §4.3 mapping, drand mainnet params §14.2)
body = "Hello, future." (UTF-8, 14 bytes)
title = absent
Intermediate:
body_hash = SHA3-256("Hello, future.")
= 76ab8b3f843c6ed4f2d0fd75b9f457b4
ad49dd4450f9c22723ae430e3af3211d
title_hash = [0u8; 32] (title absent — §4.2.1 sentinel)
Domain separator (10 bytes):
[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]
Preimage (108 bytes—current protocol v1):
domain_separator || // 10 bytes
0x01 || // version
0x01 || // content_type
0x0000000067748580 || // created_at as i64 big-endian (1735689600)
0x00000000677DC000 || // unlock_at as i64 big-endian (1736294400)
0x0000000000000000 || // outcome_at_or_zero (outcome_at absent)
0x000000000047A596 || // drand_round as u64 big-endian (4695446)
body_hash || // 32 bytes
title_hash // 32 bytes (all-zeros sentinel; title absent)
Expected output:
qub_id = SHA3-256(preimage)
= 4a84e3dfaec32954949c30073f8e6506
fd3204c1bb97f9162b81c7587afe412e
As implementações DEVEM produzir idêntico body_hash e qub_id valores para esta entrada. Este vetor de teste DEVE ser o primeiro teste unitário escrito. Os valores canónicos acima foram calculados pela implementação de referência e DEVEM corresponder bit a bit. Os protótipos históricos pré-lançamento (nenhum qub em funcionamento dependia dos dois primeiros) usavam 92 bytes antes outcome_at (3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0) e 100 bytes após adicionar outcome_at_or_zero (b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed). O atual layout de 108 bytes foi então adicionado drand_round e o QUB_ID_V2 separador de domínio. Um vetor inicial de 108 bytes usava o legado ceil mapeamento redondo (drand_round = 4695445) e produzido 3a9fcb31b750d985c262fada6d4f777fd6a28be831d941d85c131f5a4bbaf8a4—ainda válido qub_id para essa entrada de ronda, enquanto o exemplo acima segue o mapeamento de ronda atual do §4.3.
14.2 Mapeamento Desbloquear-Ronda
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calculation:
(1735689600 - 1595431050) / 30 = 4675285.0
floor(4675285.0) + 1 = 4675286
drand_round = 4675286
A ronda 4675286 publica-se às 1595431050 + (4675286 - 1) * 30 = 1735689600—exactamente às unlock_at, nunca antes. (A pré-versão legada ceil mapeamento deu 4675285, publicado em 1735689570—30 segundos de antecedência; os verificadores aceitam essa ronda legada conforme §4.3.)
14.3 Round-trip de CBOR canónico
As implementações DEVEM verificar que serialize(parse(serialize(qub))) == serialize(qub) para todas as entradas válidas. Este é um teste de propriedade, não um vetor único.
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)
Os bytes CBOR canónicos e o body_hash SHA3-256 são calculados pela implementação de referência. As implementações DEVEM produzir CBOR idêntico em bytes para esta entrada.
As implementações TAMBÉM DEVEM verificar que serialize(parse(serialize(pact))) == serialize(pact) para todas as entradas PactTerms válidas (teste de propriedade).
14.5 Vetores cross-language do wrapper externo
O wrapper externo (§13) tem uma fixture canónica separada em crates/qub-core/tests/vectors/wrapper_v1.json. Cada caso fixa uma tupla (key, nonce, qub_id, sealed_cbor) como entradas hex opacas e afirma uma saída específica expected_wrapper_hex. Ambas as implementações de referência consomem o mesmo ficheiro 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).
O suporte atualmente fixa três casos de invólucro de baixo nível. Eles testam determinísticos OuterWrapper codificação e interoperabilidade AEAD independentemente do invariante de forma de entrega §13.8; em particular, o nome histórico basic-text-public e seu interior visibility = 0x01 fazer não faça com que os bytes encapsulados resultantes constituam uma entrega pública conforme. Um produtor AINDA DEVE armazenar os bytes internos públicos de forma explícita e encapsular apenas privados (0x00) bytes internos.
| Caso | Cobertura |
|---|---|
basic-text-public |
Nome de dispositivo histórico de baixo nível. Menor realista SealedQub forma, sem campos opcionais; testa apenas os bytes do invólucro e não é uma entrega armazenada conforme §13.8. |
with-recipient-pubkey |
SealedQub com recipient_pubkey conjunto (caminho futuro reservado). Exercita um conjunto de chaves CBOR interno diferente; o seu conteúdo de fixture distinto gera independentemente um diferente qub_id (recipient_pubkey não está na pré-imagem do §4.1). |
longer-body |
~4 KiB de corpo — exerce prefixos de comprimento CBOR multi-byte tanto dentro do envelope interno como no texto cifrado externo. |
As implementações DEVEM produzir expected_wrapper_hex byte-idêntico para as entradas registadas. A regeneração da fixture requer QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors e está reservada para mudanças deliberadas de formato.
15. Governança do perfil de cifragem (futuro)
Esta seção é informativa para a v1 e torna-se normativa na primeira vez que um segundo algoritmo entrar em qualquer uma das primitivas criptográficas do qub.
15.1 Postura atual
O protocolo v1 vincula exatamente um algoritmo por primitiva:
- Assinatura: ML-DSA-65 (
sig_alg = 0x01; chave pública de 1952 bytes, assinatura de 3309 bytes) e não assinada (sig_alg = 0x00). A base de código reserva0x02para Ed25519, mas o protocolo v1 não o ativa; um verificador v1 DEVE rejeitar todossig_algfora{0x00, 0x01}. - Bloqueio Temporal: drand quicknet apenas — o hash da cadeia, a chave pública, o tempo de génese e o período são parâmetros de rede fixos transportados pela referência
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) econfig/drand-endpoints.json. - Envoltório exterior: AES-256-GCM v1 apenas (§13).
Os verificadores atualmente codificam de forma fixa os comprimentos de chave e assinatura por primitiva ativa. O sig_alg e os bytes da versão do wrapper são seletores explícitos, mas o v1 não realiza qualquer negociação in-band e admite apenas os valores ativos acima.
15.2 Forma pretendida
Quando um segundo algoritmo entrar no protocolo, o verificador será configurado para um CryptoProfile nomeado (por exemplo, ExqubV1) que lista o conjunto exato de valores permitidos por primitiva — sig_algs, chains drand, versões de wrapper, tipos de conteúdo. O perfil é fixado no momento da verificação, nunca negociado em banda. Qualquer valor fora do perfil ativo é rejeitado.
Isso garante que adicionar ML-DSA-87 ou ativar Ed25519 não pode enfraquecer retroativamente configurações de verificador existentes: um verificador v1 permanece um verificador v1 mesmo depois que um perfil v2 é publicado.
15.3 Condições de ativação
Promova o §15 a status normativo quando qualquer uma das seguintes situações for proposta:
- Um segundo
sig_algbyte (ativação Ed25519, ML-DSA-87, ou qualquer nova entrada no registo §9). - Uma segunda cadeia drand em uso de produção.
- Uma segunda versão de embalagem exterior.
- Uma rotação da raiz de confiança do registo de transparência — a
LogProfile.anchor_ownerendereço ou a chave pública de recibo fixada (§16.6). ALogProfileune a superfície do perfil §15.2 como uma primitiva governada: uma rotação é assinadaLogProfilebump enviado numa atualização do verificador (as rotações planeadas assinam cruzadas de saída → entrada; as rotações motivadas por compromissos não podem, e dependem deste bump com a verificação prev-anchor fork limitando o dano interino). Os espaços de versões do registo de transparência (LOG_VERSION,ANCHOR_FORMAT) evoluir como irmãos independentes, exatamente como a versão wrapper §12.5 é independente da versão do protocolo.
Até então, o §15 é um marcador que fixa a forma de migração para que PRs futuros aterrissem contra um alvo conhecido em vez de re-litigar a superfície de negociação do zero.
16. Registo de transparência e níveis de durabilidade (Design — revisão concluída)
Estado. Esta secção é implementado (W5/UP-B1, Fases 1–8), com o produtor e o âmbito da raiz de confiança declarados aqui. Os formatos de dados, funções de hash e caminhos de verificador estão ativos: os tipos principais Merkle + CBOR canónico (
qub-core), o espelho TypeScript + empacotador ANS-104 (workers/api/src/crypto/), o escritor únicoLogDO+ armazenamento de nós R2 com chave de coordenada, o/uploadtentativa de anexação de registo, os crons diários anchor + bundler-drain, oGET /api/v1/qub/:tx_id/proof(inclusão) eGET /api/v1/log/consistency(RFC 9162) pontos finais de prova, a prova de inclusão tipada transportada no.qubpacote (§17.5), o verificador de âncoras nativo ANS-104 (tools/qub-verify), e o gancho de cabeças auto-publicadas duplo (§16.6). Um bem-sucedido/uploadé sempre durável R2 mas está coberto de registos apenas quandoLOG_DOestá configurado e a anexação em linha tem sucesso; só então é que a sua resposta é transmitidalog_seq,receipt, eanchor_status. SeRECEIPT_SKestá ausente ou inválido, esse recibosig_b64urlestá vazio e não fornece qualquer não repúdio. O atual/seale o cronograma de caminhos de publicação de pact agenda transações individuais do Arweave, mas não adiciona uma folha de registo. Nenhum código realiza atualmente o/uploadcomentário sobre a reconciliação proposta mais tarde após uma falha de anexação. A revisão externa W5 está concluída: §16.15 regista decisões de design e restrições de lançamento, mas essas restrições não ampliam a cobertura do produtor mencionada anteriormente. Três itens de confiança/desdobramento permanecem bloqueados: (a) a carteira âncora dedicada (ANCHOR_JWK;LogProfile.anchor_ownerainda é o[0xAB; 32]placeholder); (b) a chave de assinatura do recibo e correspondendo ao pin de chave pública (RECEIPT_SKé opcional eLogProfile.receipt_pubkeyestá atualmente vazio); e (c) o repositório GitHub self-published-heads + token (§16.6). Até que os pinos de âncora/perfil sejam provisoriados, um verificador independente reporta o estado da prova de forma honesta em vez de alegar uma verificação totalmente ancorada e fixada. O design é estritamente aditivo e há sem alterações aoSealedQub/QubEnvelopeformato de fios.
16.1 Justificativa e níveis de durabilidade
Os atuais caminhos de publicação desacoplam o reconhecimento da confirmação na Arweave: eles derivam e assinam uma transação individual, persistem o artefacto e o estado exato da submissão no R2, e depois publicam de forma assíncrona. O registo de transparência adiciona uma camada de ordenação ancorada independentemente para o subconjunto geral /upload pedidos cujos LogDO append tem sucesso:
| Nível | Nome | Garantia | Quando |
|---|---|---|---|
| T1 | Reconhecimento síncrono R2-primeiro | Piso de durabilidade — bytes selados e estado de publicação exato são gravados em armazenamento durável antes de o sucesso ser retornado. | Implementado em todos os caminhos de publicação atuais. |
| T2 | Inclusão de registo de transparência em lotes | Compromisso apenas para anexar, à prova de adulteração + ordenação total uma vez incluído e ancorado. | Produtor atual: bem-sucedido LogDO anexa de /upload; a resposta transporta a tupla de recibo. Não é universal. |
| T3 | Per-qub Permanência Arweave | Uma transação individual do Arweave para o qub. | Atualmente preparado para cada publicação aceita e enviado de forma assíncrona; a transação assinada exata permanece na caixa de saída drenável até ser entregue. |
Os níveis descrevem propriedades distintas de evidência e durabilidade, não o plano comercial atual. O código atual ainda agenda uma transação individual na Arweave para cada publicação aceite; não expõe o T3 apenas como um aumento pago. Os limites de quota de chave API/conta continuam a ser controlos separados da aplicação.
Durabilidade honestidade. A escrita T1 é síncrona, por isso uma resposta bem-sucedida estabelece durabilidade ao nível da aplicação sem esperar por um gateway Arweave. Ela não estabelece por si só uma marca temporal independente. Uma transação individual confirmada fornece o limite superior do tempo de bloco. Para uma resposta que contenha o tuplo completo do recibo T2, a próxima âncora confirmada pode fornecer a prova de registo descrita abaixo. Se o tuplo estiver ausente, nenhuma superfície pode implicar que este qub já está no registo de transparência. A latência da âncora e da publicação não possui SLA numérico a nível de protocolo.
16.2 Estrutura de LogLeaf (duas formas comprometidas)
Uma entrada do registo é uma LogLeaf, codificada como CBOR canónico escrito à mão sob o perfil do §3.1 (comprimento definido, sem tags, sem floats, inteiros na forma mais curta, texto NFC, campos opcionais omitidos quando ausentes, chaves ordenadas por comprimento de bytes codificados ascendente e depois bytewise). A guarda canónica parse → re-encode → compare do §3.1 é aplicada no caminho de codificação antes do hashing (não apenas na descodificação), pelo que duas implementações não podem discordar sobre os bytes da leaf por uma diferença de largura de inteiro ou de ordem de chaves. Todos os inteiros são u8 / u64 / i64; todos os digests são byte strings de 32 bytes (bstr[32]). Um id de transação Arweave armazenado é um digest SHA-256 cru de 32 bytes carregado como bstr[32], nunca uma string de texto base64url (corresponde ao §3.3).
A folha tem duas formas selecionadas por um kind byte, porque o caminho geral de carregamento é cego aos bytes: POST /api/v1/upload trata deliberadamente ambas as formas de carga útil aceites como opacas e recebe qub_id e unlock_at apenas como declarações de cliente não confiáveis. No caminho privado por defeito, body_hash, drand_round, created_at, e drand_chain_version são adicionalmente escondidos dentro do invólucro externo §13, cuja chave o Trabalhador nunca possui. O sistema de tipos também define uma forma atestada para um produtor que deriva body_hash / drand_round ele próprio. O atual /seal rota tem esses valores mas não chama LogDO, portanto a produção atualmente emite apenas afirmado (0x02) resulta de anexos de upload geral bem-sucedidos. A divisão mantém cada valor comprometido honesto sem fingir que o produtor atestado está ligado:
| Chave | Compr. len | Tipo | Presença | Significado |
|---|---|---|---|---|
seq |
quatro | u64 |
necessário | Índice de folha global baseado em 0; a posição à qual a prova de inclusão se compromete. |
kind |
cinco | u8 |
necessário | 0x01 capacitado atestado (definido, não atualmente emitido) ou 0x02 afirmado (upload com selo do cliente / cego por bytes). |
ref |
quatro | bstr[32] |
necessário | ID de referência da folha. Atestado → cru qub_id. Afirmado → o cego id SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
seis | bstr[32] |
necessário | Endereço de conteúdo SHA3-256(stored_bytes) — o único empate de conteúdo que o Trabalhador pode sempre calcular honestamente, em ambos os caminhos. |
unlock_at |
dez | i64 |
necessário | Copiado (atestadо) ou afirmado (afirmado); validado > 0 antes de entrar na folha. |
received_at |
doze | i64 |
necessário | Relógio de parede do trabalhador em R2-ack. Não probatório (afirmado pelo operador; §16.6). Presente para auto-descrição, nunca uma prova. Validado > 0. |
body_hash |
dez | bstr[32] |
kind=0x01 apenas |
Omitido em 0x02 — o Trabalhador não o possui ao abrigo do §13. |
drand_round |
doze | u64 |
kind=0x01 apenas |
Omitido em 0x02. |
Uma leaf kind=0x02 deliberadamente não compromete nem body_hash nem drand_round: atesta o compromisso e a ordenação de um ciphertext opaco no endereço de conteúdo chash, reivindicando qub_id e unlock_at — não o seu texto puro ou round. As pernas de texto puro/round para um qub afirmado vêm da verificação existente do bundle .qub do §11, não do registo (§16.11). drand_chain_version não está na leaf (está dentro do wrapper no caminho por predefinição); a granularidade da chain vive na âncora (§16.7). Disciplina do codificador: rejeitar um ref ou chash totalmente a zeros, e rejeitar unlock_at / received_at não positivos, espelhando a guarda do sentinela outcome_at > 0 em cbor.rs.
16.2.1 Ocultação de qubs privados
O registo não pode tornar-se o oráculo de enumeração que o wrapper externo do §13 existe para prevenir (§13.1). Para um qub privado (com wrapper), a leaf asserted compromete o identificador cego SHA3-256(qub_id ‖ log_blind_secret), onde log_blind_secret é um segredo detido pelo servidor, e omite body_hash. Um terceiro não consegue ligar tal leaf a um qub_id específico; o detentor do qub, que tem a URL de entrega e portanto qub_id, pode recomputar a ocultação para confirmar a sua própria inclusão. Um qub público (já enumerável, já carregando a tag Arweave Visibility: public por §13.8) compromete o qub_id cru. Este é o único ponto onde a verificabilidade autónoma cede deliberadamente a um invariante de privacidade carregador; o laço autónomo para qubs privados é chash (§16.9).
Custódia de log_blind_secret (resolvido — §16.15 Q4). A ocultação protege a não-ligabilidade da leaf, não a confidencialidade do texto puro (o wrapper do §13 garante isso de forma independente). Num comprometimento de log_blind_secret, para qualquer qub_id que o adversário já detenha ou possa reconstruir (cada qub cujo bundle/URL ele tenha, mais qualquer qub_id de baixa entropia ou público), ele recomputa o ref da leaf em um único hash e liga-o — isto é ligação direta de uma população conhecida, não força bruta sobre um espaço desconhecido. Classifique log_blind_secret como um segredo de grau correlação/Sybil no mesmo nível de custódia dos outros segredos do servidor, e rode-o apenas para a frente (uma rotação re-oculta leaves futuras; não pode desligar retroativamente as já ancoradas).
16.3 Hashing de leaf e de nó
Hashing com separação de domínio do RFC 6962 §2.1, com SHA-256 substituído 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
Os bytes de prefixo de domínio 0x02 (cadeia de entradas, §16.4) e 0x03 (hash de STH, §16.6) são reservados e disjuntos destes. São bytes únicos e portanto não podem colidir com os separadores de domínio ASCII de 10 bytes existentes (QUB_ID_V2, etc.). A árvore é a árvore desequilibrada cheia-à-esquerda do RFC 6962 (cada divisão interior na maior potência de dois estritamente menor que a contagem de leaves da subárvore), o que permite às provas de inclusão e de consistência partilharem um único algoritmo de caminho de auditoria. A especificação de referência carrega pseudocódigo explícito de derivação esquerda/direita e fixa um vetor de teste não-potência-de-dois (5 leaves) para que o caso de promoção da borda direita — que um vetor de 4 leaves esconde — seja exercitado.
16.4 Encadeamento de hash (interno)
O LogDO mantém uma cadeia de entradas interna apenas para consistência de crash. Nunca é publicada e nunca é visível ao verificador:
entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1] = SHA3-256("QUB_TLOG_GENESIS_V1")
A autoridade append-only publicada é a raiz Merkle cumulativa + a sua âncora (§16.5–16.6), nunca a ordem crua em que o operador calha servir as leaves: a cadeia recomputa para qualquer ordem servida, pelo que só a raiz ancorada fixa a posição canónica.
16.5 Árvore Merkle cumulativa e lotes
Há uma única árvore RFC 6962 em crescimento perpétuo sobre todas as leaves em ordem de seq — não árvores isoladas por lote. (Uma construção encadeada por carry-leaf por lote foi rejeitada: não é uma verdadeira relação de prefixo, pelo que as suas "provas de consistência" são pouco sólidas.) A árvore cumulativa dá verdadeiras provas de consistência do RFC 9162 e permite que uma única âncora recente prove a inclusão de qualquer qub mais antigo.
O LogDO Durable Object é o escritor único (blockConcurrencyWhile, espelhando QuotaDO / EntitlementDO) — acrescentar a um registo partilhado é ler-modificar-escrever no estado partilhado e, portanto, DEVE passar por um DO, nunca KV. Ele guarda em cache a fronteira do lado direito da árvore (O(log n) hashes) portanto, fechar um lote é O(batch). A lote é o conjunto de folhas ancoradas juntas; os seus acionadores implementados são um tree_size avanço de pelo menos LOG_BATCH_MAX_LEAVES (padrão 4096), idade atingindo a cadência de ancoragem, ou um fecho administrativo/cron explícito. root_i é o Hash cumulativo da Árvore de Merkle sobre as folhas 0 .. tree_size_i.
16.6 Signed Tree Head via âncora Arweave
A transação de âncora Arweave é o Signed Tree Head e substitui uma assinatura do operador para a própria cabeça da árvore: a âncora diária não precisa de chave do qub porque o owner da tx Arweave é a assinatura. A tese do fosso mantém-se — o substrato imutável, não um segredo detido pelo qub, é carregador para a raiz ancorada.
O design do registo exige uma anexação bem-sucedida e única chave de recibo (§16.10), fixado em LogProfile e coassinada por anchor_owner. A implementação atual não completou esse fornecimento de raiz de confiança: RECEIPT_SK é opcional, uma chave ausente/inválida resulta em sig_b64url: "", e o compilado LogProfile.receipt_pubkey está vazio. Tal recibo pode descrever a folha anexada, mas está não um recibo assinado não repudiável. A alegação de design mais forte aplica-se apenas depois que um verificador libera o PIN correspondente à chave pública e o proprietário do âncora a contra-assina. Uma resposta de publicação sem o tuplo de recibo completo não faz qualquer alegação de aceitação no registo; uma com assinatura vazia faz uma alegação de posição de anexação, mas nenhuma alegação de verificação de assinatura.
O SignedTreeHead é CBOR canónico (chaves por comprimento codificado): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (o sth_hash anterior; gênese = 32 bytes a zero), log_id:bstr[32], first_seq:u64, anchored_at:i64. O seu hash é sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).
Raiz de confiança fixada. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Um verificador conforme DEVE exigir anchor_tx.owner == LogProfile.anchor_owner, onde anchor_owner (e a chave pública da chave de recibo) está embutido no qub_core como o LogProfile — a par das constantes da quicknet já presentes em DrandTimelockProvider::quicknet() — e distribuído com o binário do verificador. O verificador DEVE também verificar a vinculação dados → tx_id da tx Arweave localmente, em vez de confiar numa resposta /raw/ de um gateway. Isto fecha o buraco de equivocação por carteira fraudulenta: "ancorado no Arweave" não tem significado até que o verificador fixe qual carteira.
A rotação é uma extensão de governança do §15, não uma reutilização (resolvido — §16.15 Q3). A superfície de perfil do §15.2 atualmente enumera apenas sig_algs / chains drand / versões de wrapper / tipos de conteúdo, e os gatilhos do §15.3 não listam nenhum destes — LogProfile / anchor_owner ainda não está na superfície do §15. A governança de rotação tem, portanto, de ser construída: o §15.3 é estendido (abaixo) para acrescentar o gatilho LogProfile, e uma rotação é um bump de LogProfile assinado, enviado numa atualização do verificador. Uma rotação planeada carrega uma co-assinatura de saída → entrada; uma rotação motivada por comprometimento não pode (a chave de saída é não confiável/indisponível precisamente nesse momento) e recai sobre o bump governado pelo §15, com a verificação de fork da âncora anterior (abaixo) a limitar os danos no intervalo.
Janela de ambiguidade (parâmetro de confiança de primeira classe). Uma folha é resistente à ambiguidade apenas quando a sua âncora de cobertura é Arweave-confirmado. A janela está received_at → anchor confirmation (cadência + finalização Arweave, sem garantia de latência de protocolo). Antes do fornecimento da raiz de confiança, a implementação atual fornece a integridade operacional do qub mais quaisquer metadados de anexação não assinados presentes; não fornece a garantia de não-repudiação planeada. Três artefactos de responsabilidade definem o design concluído (o modelo de testemunha é a resolução do §16.15 Q2):
- Recibo de selo (dependente de aprovisionamento) — o análogo SCT retornado quando a adição de registos de um upload tem sucesso (§16.10). Torna-se irrefutável apenas quando
sig_b64urlnão está vazio e A relação correspondente chave pública/proprietário do âncora está fixada no verificador. O pin do perfil de produção atualmente vazio não pode suportar esse veredicto. Este controlo não se aplica a uma tupla de recibo omitida ou a um recibo não assinado. - Metodologia de monitor publicado + caminhada de cadeia anterior — a âncora
prevcadeia é percorrida cabeça→génese; um garfo (duas âncoras em umasizecom diferenteroot, ou um quebradoprev) é prova publicável de má conduta. A deteção de equívocos é um compromisso operacional declarado, não uma suposição implícita. - Cabeças auto-publicadas duplas — cada nova cabeça
{sth_hash, tree_size}é publicado num dedicado pertencente à qub repositório público do GitHub apenas para anexos (a perna de auto-publicação à prova de violação e resistente ao peso), com uma publicação social apenas como corroboração de melhor esforço. Uma publicação falhada DEVE enviar uma página (não falhar silenciosamente). Implementado (Estágio 8) como opublishHeadgancho na âncora cron (workers/api/src/utils/heads-publish.ts): aPUTpara a API de conteúdos sem umshaé apenas de anexação (a422significa que a cabeça já está publicada, nunca uma sobrescrição); opt-in / controlado por implementaçãoPUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO}e inerte até que o repositório seja provisionado. Uma falha grave do GitHub páginas via ohealth_alertcanal e aumenta uma métrica de falha durável (m:tlog_publish_head_fail); o próprio anchor da Arweave nunca retrocede num falhanço de publicação. O "não falhar em silêncio" é garantido por essa métrica durável — sobre a qual a equipa de operações DEVE criar alertas no dashboard — mesmo que a página de email de esforço máximo não possa ser entregue. Duas limitações honestas decorrem do "anchor-on-advance" (o cron apenas publica quando o tamanho avança): um falhanço transitório do GitHub deixa um lacuna na sequência de cabeças publicadas para esse tamanho — limitada, não silenciosa (ela faz paginação), e porque cada cabeça comete uma árvore superset, uma prova de consistência §16.9 preenche a lacuna; crucialmente, essa prova de consistência é calculada a partir da árvore autoritativa ancorada no Arweave, não a partir da superfície do GitHub, assim uma lacuna no GitHub nunca enfraquece a verificabilidade. Um preenchimento posterior que preenche lacunas das heads publicadas é uma melhoria adiada.
Honestidade vinculada (restrição vinculativa). Porque o qub controla ambas as superfícies de postagem planeadas, isto é auto-publicado, não testemunhado de forma independente. Nenhum produto, marketing ou superfície legal pode afirmar que o registo é "testemunhado de forma independente". Após o fornecimento dos recibos/perfis/portões principais, a afirmação permitida é que a ambiguidade é detectável e um apêndice assinado com sucesso deixa um recibo incontestável. Antes disso, essa reivindicação não está disponível. Um verdadeiro testemunho independente de terceiros será adiado para um futuro aumento de governança §15.
received_at é afirmado pelo operador e nenhuma afirmação pode apoiar-se nele — nunca é apresentado como prova ou como corroboração de disputa em qualquer superfície de produto / legal / API / renderização de prova. O tempo do bloco de âncora Arweave T é o único timestamp sem confiança (um limite superior para "registado em"). Qualquer verificação de sanidade de monitor sobre received_at DEVE comparar contra T, não contra o campo anchored_at do STH controlado pelo operador; tal verificação é uma guarda apenas contra um bug de relógio de um operador honesto, não um controlo de responsabilização contra um operador malicioso (§16.15 Q5).
16.7 Formato da transação de âncora e cadência
O AnchorBundle é o corpo CBOR canónico da transação Arweave, escrito através do bundler do §16.8: ver:u8, sth:bstr (bytes canónicos do SignedTreeHead), prev_anchor:bstr (id da tx de âncora anterior em bytes crus; omitido na gênese), chain_hash:tstr (a chain drand em vigor — quicknet) e o fluxo de leaf-CBOR do lote em ordem de seq para que a âncora seja autossuficiente: um monitor re-deriva root a partir do corpo com zero dependência do qub. (Se o fluxo de leaves se tornar grande em volume elevado, uma revisão futura pode comprometer apenas um intervalo de leaves por referência; anotado, não adotado na v1.)
As tags Arweave são intencionalmente enumeráveis — o registo destina-se a ser encontrado, ao contrário dos 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. As tags são dicas não confiáveis; o corpo CBOR é a única autoridade.
Cadência: diariamente por defeito, revisto com volume (o gatilho de tamanho encurta automaticamente a cadência efetiva sob carga). O produtor atual não implementa um gancho de âncora-força de selo pago. O carteira Anchor é dedicado e de baixa velocidade, separado da carteira de upload — DEVE ser seu JWK próprio (uma chave distinta, não um papel lógico na carteira de upload) de modo que uma violação da carteira de upload não possa falsificar âncoras — com um orçamento rígido de transações de âncora por dia. A postura de custódia é declarada claramente: a tecla de atalho de âmbito restrito com um disjuntor apertado e baixo saldo, não “fria” — uma carteira que assina automaticamente diariamente não pode ser fria, e a especificação não finge o contrário.
16.8 Bundler ANS-104
Um codificador de DataItem ANS-104 e assinador deep-hash internos, cerca de 300 linhas, apenas Web Crypto, zero dependências npm (ambos os SDKs Turbo falham a guarda de cadeia de fornecimento npm ci --ignore-scripts). Layout de bytes do 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
A assinatura é o deepHash do Arweave — um digest SHA-384 recursivo (requisito de fio do Arweave, crypto.subtle.digest("SHA-384")) sobre ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — seguido de RSA-PSS sobre o deep hash com a JWK da carteira via crypto.subtle; id = base64url(SHA-256(signature)). O SHA-384 aqui está isolado em quarentena como uma primitiva apenas-de-fio-do-Arweave, nunca uma primitiva de confiança do qub (o §15 regista a cerca; o hashing de confiança do qub é SHA3-256 do início ao fim).
O caminho de código ANS-104 serve a maquinaria de retaguarda/esmague diferida e escreve AnchorBundle DataItems. O percurso de publicação normal cria primeiro uma transacção Arweave assinada exacta e mantém o seu JSON numa caixa de saída durável; a publicação direta é uma optimização de latência, e o percurso de escoamento tenta novamente a mesma transacção antes de aplicar a sua solução alternativa de agregador. Esquema de assinatura (resolvido — §16.15 Q8): v1 assina com RSA-PSS (tipo de assinatura 1) reutilizando o mecanismo existente de JWK da carteira Arweave (zero nova custódia de chaves de longa duração, servindo a tese do "um segredo a menos"); Ed25519 é adiado para o caminho de migração PQ §15.
O deep hash feito à mão é o código de maior risco e de cobertura natural mais baixa na W5, pelo que o seu gating é inegociável (§16.15 Q8):
- A fixture cross-language
tlog_v1.json(Rust + TS, o padrãowrapper_v1.jsondo §14.5) cobre deep-hash, bytes + id do DataItem, hashes de leaf, uma raiz de 5 leaves + caminho de auditoria, um hash de STH, uma prova de inclusão e uma prova de consistência — em ambas as direções, assinar e verificar (a direção de verificar importa porque a verificação local tx → tx_id do §16.6 puxa o deep hash para dentro de todos os verificadores autónomos, não apenas do escritor). - Um round-trip de interoperabilidade pontual através de um bundler ANS-104 de referência, consumido como dados de teste estáticos apenas — nunca uma dependência npm de runtime (mantém-se a postura apenas-Web-Crypto / sem-install-scripts).
- O caminho deep-hash + RSA-PSS deve fazer round-trip pelas mesmas primitivas
crypto.subtleque a produção usa, para que o codificador interno seja byte-compatível. - Um monitor de aceitação pós-bundle contínuo confirma que cada DataItem de âncora / fallback alcança efetivamente a aceitação do Arweave, com um alarme + disjuntor — porque o deep hash serve também a fila de fallback de indisponibilidade do Arweave, pelo que uma regressão silenciosa encheria essa fila com itens rejeitados pela rede durante exatamente a interrupção que ela existe para cobrir.
16.9 Provas de inclusão e de consistência
Ambas são RFC 9162, SHA3-256, servidas como CBOR canónico.
InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (o CBOR exato da leaf — o verificador recomputa o leaf_hash ele próprio e nunca confia num hash fornecido), 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. Uma única lista de chaves não ambígua, fixada por vetor de teste.
Verificação autónoma (sem servidor do qub, estende o §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).
O armazenamento de serviço de provas DEVE ser indexado por coordenada (resolvido — §16.15 Q7, pré-condição bloqueadora). A geração de provas de leaf fria é neutra em correção apenas se o material de auditoria do R2 for um armazenamento persistente de nós Merkle indexado por coordenada absoluta da árvore (level, index) — não deltas de nós por batch. Com um armazenamento indexado por coordenada, qualquer caminho de auditoria (leaf i, size N) é um conjunto de O(log N) GETs diretos ao R2 com nenhuma recomputação através das fronteiras de lote; com um armazenamento indexado por lote não é, e é esta lacuna de layout de armazenamento que esta resolução fecha. Os corpos das leaves são, de igual modo, endereçáveis por conteúdo por seq. Um vetor de teste da W5 DEVE provar uma leaf fria de era de gênese contra uma raiz muito posterior usando apenas R2 + Arweave com o armazenamento do LogDO apagado, para que a afirmação de segurança de reclamação no §16.13 seja sustentada em vez de afirmada. Os O(log N) GETs sequenciais ao R2 pertencem apenas ao endpoint de provas assíncrono — nunca ao caminho quente de selamento (§16.10) ou a um cron por tick.
16.10 Ordenação do ack R2-first
O implementado POST /api/v1/upload a sequência é:
- Portões da metade frontal (auth, validação, chave de fragmento de idempotência) — inalterados.
- Crie, marque e assine a transação individual exata do Arweave. Isto deriva
tx_idLocalmente, embora a criação de transações possa buscar metadados de recompensa/âncora de um gateway. Uma falha na preparação ainda faz com que o pedido falhe antes do reconhecimento. - Sincronamente escrever o artefacto seleccionado em
qub-cache/<tx_id>e persistir os registos estáveis de criação-operação/caixa de saída. Estes são o piso de durabilidade e tentativa; falhas antes da liquidação retornam 503. - Quando
LOG_DOestá configurado, tentar sincronizadamenteLogDO.append(leaf). O escritor único atribuiseq, estende a cadeia de entradas e atualiza a fronteira. O RPC de anexação faz apenas isso; o fecho em lote executa fora do caminho no alarme. Uma falha de transporte/aplicação de anexação atualmente falhar de forma suave: a resposta pode ainda ter sucesso semlog_seq,receipt, ouanchor_status. Apesar de um comentário sobre a implementação, nenhuma reconciliação automática posterior de registos está configurada hoje. - Devolver o reconhecimento. Incluir
{ log_seq, anchor_status: "pending", receipt }apenas quando o append devolveu a tupla completa com sucesso.receipt.sig_b64urlestá vazio quando o signatário do recibo não está disponível; os clientes NÃO DEVEM chamar esse valor de assinado ou irrefutável. A ausência do tuplo significa apenas publicação durável, não aceitação pelo registo de transparência. - Use uma tarefa adiada para publicar a transação assinada exata. O sucesso remove a caixa de saída; a falha deixa-a para o cron de drenagem limitada e não deve alterar o já reconhecido
tx_id. Metadados provisórios e outros acessórios auxiliares de melhor esforço também são adiados.
Limite de latência. O caminho do pedido inclui trabalho de autoridade/cota da frente, preparação/assinatura de transações, gravações R2 duráveis e (quando configurado) o LogDO tentar. < 300 ms aparece na revisão de design como um objetivo operacional, não como uma garantia de protocolo; o passo atual de preparação da transação pode executar um pedido de metadados do gateway. Alarmes de latência e portões de lançamento são controlos operacionais, não evidências disponíveis para um verificador.
16.11 Modelo de confiança — a afirmação precisa, delimitada por kind de leaf
Para kind=0x01 (atestado): "Este conteúdo — corpo correspondente a body_hash, identificado por qub_id — foi comprometido ao registo append-only do qub na posição seq e existia o mais tardar no tempo do bloco Arweave T; era criptograficamente ilegível até ao round drand R = unlock_round(unlock_at)." Este é o triplo completo {vinculação de round tlock + inclusão Merkle + raiz ancorada}.
Para kind=0x02 (afirmado, a predefinição): "Um ciphertext opaco com endereço de conteúdo chash, reivindicando qub_id e unlock_at, foi comprometido ao registo append-only na posição seq e existia o mais tardar no tempo do bloco Arweave T." As pernas de round e de corpo são fornecidas pela verificação existente do bundle .qub do §11 (qub_core::unlock), não pelo registo; o que o registo acrescenta sobre uma transação por qub nua é ordenação detetável de adulteração, um instante de compromisso de limite superior sem confiança e resistência a equivocação.
Ambas as afirmações excluem, por §11: autoria sem sig_alg ≥ 0x01, intenção e temporização de granularidade sub-âncora. Nenhuma deixa qualquer afirmação apoiar-se em received_at.
Tecto de reivindicação (restrição vinculativa de lançamento — resolvido §16.15 Q1). Para um afirmado (kind=0x02) folha, a reivindicação abrangida acima é a teto sobre o que qualquer superfície de produto, marketing, termos ou renderização de prova possa afirmar. Nenhuma superfície pode declarar ou sugerir que o registo prova o conteúdo ou a ronda de desbloqueio de um carregamento byte-cego — o registo prova ordenação + um tempo de compromisso superior sem confiança de um texto cifrado opaco. A prova de conteúdo e de ronda provém exclusivamente do §11 existente .qub-verificação de pacote, que é independente de registos. Uma publicação sem anexação/recebimento bem-sucedido não tem qualquer reivindicação de registo.
16.12 Versionamento e coordenação com a W3
Há não SealedQub salto de fio e portanto sem aumento da versão do protocolo (§12.2): o registo é um sidecar que se compromete com os campos e bytes existentes, pelo que não entra no histórico de versões do protocolo §12.3. Opcional do W3 drand_chain_version permanece intocado e continua a ser o único opcional SealedQub campo. O registo introduz, em vez disso, os seus próprios espaços de versões independentes — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — espelhando a independência da versão do invólucro do §12.5 (o invólucro transporta um byte de versão independente da versão do protocolo, e as versões do registo seguem a mesma separação).
A entrega da prova é obtida por fetch por predefinição, com um ride-along opcional. Uma prova não pode existir no momento do selamento (a âncora ainda não foi escrita), pelo que o bundle .qub do momento do selamento permanece sem prova. O verificador da W7 faz fetch a GET …/proof uma vez, ou em modo totalmente offline reconstrói a prova a partir do AnchorBundle público via uma consulta Arweave sobre Log-Id. O bundle .qub (W7) reserva um membro inclusion_proof opcional — ausente no selamento, preenchido por uma re-exportação pós-âncora para ficheiro frio — seguindo o mesmo padrão "opcional, omitido por predefinição, aditivo" do drand_chain_version da W3.
16.13 Retenção
As janelas de retenção para a cauda aberta do LogDO, o substrato de serviço de provas do R2, os contadores do disjuntor de âncora e a fila de fallback do bundler estão especificadas em docs/DATA-RETENTION.md. Princípio: o armazenamento quente por entrada do registo (LogDO) é reclamável pós-âncora; o seu material de auditoria — o armazenamento de nós Merkle indexado por coordenada (level, index) + os corpos de leaf endereçados por seq (§16.9) + as âncoras Arweave — é permanente. Reclamar uma leaf fria do DO nunca invalida uma prova emitida, porque uma prova resolve-se contra esse armazenamento de nós R2 permanente e a âncora Arweave, não contra o DO (e o vetor de teste de DO apagado do §16.9 prova-o).
16.14 Vetores de teste
A W5 envia a fixture cross-language tlog_v1.json (§16.8) mais vetores trabalhados: uma leaf kind=0x01 e uma kind=0x02 → leaf_hash; a raiz cumulativa de 5 leaves; uma prova de inclusão; uma prova de consistência; um AnchorBundle; e um id de DataItem. Estes vivem a par dos vetores de wrapper externo do §14.5 e são exercitados tanto pela implementação Rust (qub-core) como pela TypeScript (Worker).
16.15 Rever Decisões (W5 — resolvido)
A revisão externa W5 (uma análise de design adversarial + aprovação do proprietário) está completa. Cada decisão abaixo está resolvida e refletida no texto do §16 acima; o restrições vinculativas de lançamento são reiterados no final. A implementação pode prosseguir de acordo com eles.
- Caminho-padrão (
kind=0x02) honestidade da folha — RESOLVIDO. Envie o tipo de duas folhas dividido conforme especificado:kind=0x02não se compromete com nenhumbody_hashnemdrand_round. Não*_body_hashcampo no caminho cego de bytes (seria o sinal falso “verificado” mais legível para integradores e é uma conveniência que o §11 já fornece a partir do pacote). Faça não exigir server-seal para qubs atestados por log (isso forçaria o texto simples a passar pelo Worker e destruiria o fosso de destruição criptográfica). Qualquer auto-descrição de curto-circuito pertence ao.qubpacote / envelope de prova como um campo recalculado pelo verificador, nunca um campo folha. Teto de reivindicação confirmado pelo proprietário: §16.11. - Responsabilidade por ambivalência/omissão — DESENHO RESOLVIDO, FORNECIMENTO INCOMPLETO. O design requer que a chave de receção do selo seja encaixada
LogProfilee coassinada poranchor_owner, além da metodologia de monitorização, percurso da cadeia anterior e cabeças auto-publicadas duplas. O perfil compilado e os ganchos de implementação ainda são espaços reservados/opcionais como detalhado no §16.6, por isso a reivindicação mais forte detetável + rececionada não é atual até que esses parâmetros sejam concluídos. Nunca deve ser comercializado como testemunhado de forma independente. Uma verdadeira testemunha de terceiros será adiada para uma atualização de governação do §15. - Raiz de confiança do proprietário do anchor fixo + rotação — RESOLVIDO. Adoptar o
LogProfilepino (§16.6); o verificador verificaanchor_tx.owner == anchor_ownere verifica localmente a ligação dos dados tx → tx_id. A governação da rotação é um §15 extensão para construir (§15.3 gatilho adicionado), não é uma reutilização; rotações planeadas cruzam-assinatura, rotações orientadas por compromisso regressam ao aumento do §15 com a verificação do fork a limitar os danos. - Cegueira da folha Private-qub — RESOLVIDA. Continuar a cegar para qubs privados (
ref = SHA3-256(qub_id ‖ log_blind_secret)), cruqub_idpara qubs públicos (já §16.2.1),chashcomo a gravata individual.log_blind_secreté um segredo de correlação/nível Sybil, apenas rodar para a frente (§16.2.1). received_at— RESOLVIDO. Mantenha-o na folha, comprometido mas explicitamente não evidencial; nunca exposto como prova ou corroboração de disputa em qualquer superfície. Qualquer verificação de sanidade do monitor compara com o tempo do bloco Arweave.T, não controlado pelo operadoranchored_at(§16.6).- Temporização comprovável em camadas — RESOLUÇÃO DE PROJETO, NÃO ROTEAMENTO ATUAL. O design revisto atribui a temporização do bloco-âncora ao nível em lotes e a prova de hora exata ao T3 pago, sem SLA numérico para o primeiro. As rotas atuais não implementaram essa distinção comercial: elas agendam uma transação individual para cada publicação aceite, e a cobertura de registo continua condicional como indicado nos §§16.1/16.10. O texto do produto deve descrever a implementação, não esta futura divisão de níveis.
- Árvore cumulativa sobre Trabalhadores — RESOLVIDO. Árvore única cumulativa RFC 9162 + LogDO de escritor único com cache de fronteira (margem confortável em relação ao limite do DO de ~1k escritas/seg; adiar o sharding Merkle-of-shard-roots até perto desse limite). A chave coordenada
(level, index)O vetor de teste do R2 node store + wiped-DO cold-leaf está implementado (§16.9).< 300 mscontinua a ser um objetivo de desenho/operação, não uma promessa do protocolo (§16.10). - Esquema de assinatura ANS-104 + deep-hash — RESOLVIDO. RSA-PSS (tipo de assinatura 1, reutilizando o JWK da carteira âncora dedicada); Ed25519 adiado para o caminho PQ §15. O hash profundo SHA-384 feito à mão está condicionado no fixture cross-impl em ambas-direcções, uma verificação de interoperabilidade de bundler de referência apenas estática, o compartilhado-
crypto.subtleviagem de ida e volta, e o monitor de aceitação do Arweave pós-pacote (§16.8).
Vinculação de restrições de lançamento (levar à implementação + revisão de produto/legal):
- Limite de reivindicação (Q1/Q6). Nenhuma superfície pode dizer o registo prova o conteúdo de uma folha afirmada ou desbloquear rodada; a reivindicação permitida para uma âncora bem-sucedida
kind=0x02a folha é ordenada, de forma evidentemente à prova de adulteração, com um tempo de compromisso de limite superior sem confiança. Uma resposta sem uma tupla de recibo não tem reivindicação de registo. Nenhuma cópia de carimbo temporal possui uma garantia de latência numérica. - Testemunhar a honestidade (Q2). Equivocação de mercado como detectável + recibo, nunca testemunhada independentemente.
- Recibo + teclas de âncora (Q2/Q8). Antes que as alegações de não-repúdio/verificação-ancorada sejam enviadas, forneça e compile o pino da chave pública do recibo, assine-o cruzadamente com o proprietário da âncora fornecido e mantenha a carteira da âncora como seu próprio JWK distinto da carteira de carregamento.
- Portão de hash profundo (Q8). Nenhum navio anchor ou T3 tx até que o encaixe bidirecional + verificação de interoperabilidade seja aprovado; o monitor de aceitação envia alertas em caso de falha.
- Pré-condição de armazenamento (Q7). Armazenamento de nós com chave de coordenadas + vetor de folha fria DO apagado são pré-requisitos para a garantia de que "a recuperação nunca invalida uma prova".
17. Pacote de Verificação Portátil (.qub)
Estado. Esta secção é implementado (W7 / UP-C2):
qub_core::exportproduz e analisa o pacote, etools/qub-verifyé um CLI público e autónomo que verifica um offline. §11 e §16.9 já fazem referência a "o".qub"pacote" como a unidade que um verificador independente consome; esta secção especifica os seus bytes e o passo a passo da verificação. É estritamente aditivo — o pacote empacota os inputs existentes da §11 e não altera nenhum formato de transmissão on-chain.
17.1 Finalidade
§11 estabelece que qualquer terceira parte pode verificar o artefacto criptográfico de um qub sem a cooperação do qub. O .qub o pacote faz essa verificação portátil e offline: ele empacota o CBOR selado e a assinatura de ronda do drand que o desbloqueia num único artefacto autónomo, para que um destinatário possa verificar a integridade do conteúdo, a ligação à ronda e quaisquer assinaturas de autoria com nenhuma chamada de rede de todo (sem busca de armazenamento, sem pedido drand ao vivo, sem API qub). Um pacote por si só não prova quando o seu texto cifrado foi criado; uma transação de armazenamento verificada de forma independente ou uma prova de registo ancorada fornece essa afirmação separada sobre o tempo de existência (§11, §17.5).
17.2 Formato do Pacote
A QubBundle é CBOR canónico escrito à mão sob o perfil §3.1 (comprimento definido, sem etiquetas, sem números flutuantes, inteiros na forma mais curta, texto NFC, campos opcionais omitidos quando ausentes, chaves ordenadas por comprimento em bytes da codificação em ordem ascendente e depois por ordem byte a byte). A ordem das três chaves de 15 caracteres d < i < s. Cru .qub o ficheiro é exatamente estes bytes; para transporte por URL ou copiar-colar os mesmos bytes são base64url(sem preenchimento).
| Chave | Enc. len | Tipo | Presença | Significado |
|---|---|---|---|---|
version |
oito | u8 |
necessário | Versão do formato do pacote (0x01). |
sealed_at |
dez | i64 |
opcional | Tempo de selo afirmado pelo criador (segundos Unix); auto-descritivo, não probatório. |
drand_round |
doze | u64 |
necessário | O círculo ao qual o qub está bloqueado. Uma projeção do qub incorporado e selado. |
arweave_tx_id |
catorze | tstr |
obrigatório | O ID da transação sob o qual os bytes selados foram armazenados (ponteiro de proveniência). |
drand_chain_id |
quinze | tstr |
necessário | A cadeia drand (hex). Uma projeção do qub selado incorporado. |
drand_signature |
dezasseis | bstr |
necessário | A assinatura do farol drand para drand_round — o valor que desbloqueia o texto cifrado. |
inclusion_proof |
dezasseis | bstr |
opcional | A prova de inclusão Merkle do registo de transparência §16, depois de uma prova ancorada estar disponível (§17.5). |
sealed_qub_cbor |
dezasseis | bstr |
necessário | O interior SealedQubCbor bytes (pós-§13-desembrulhar), ou seja, a entrada de verificação §11. |
drand_round e drand_chain_id são projeções de conveniência de sealed_qub_cbor, transportados para que ferramentas possam lê-los sem analisar o CBOR interno. Eles são derivados na construção e reverificado na decodificação contra o qub selado analisado; um pacote cujo campo de nível superior discorda do seu conteúdo é rejeitado. A disciplina do codificador espelha o resto do formato de transmissão: rejeitar um drand_signature ou arweave_tx_id, e vinculou todos os campos de comprimento variável.
17.3 O que a assinatura drand incorporada prova
O pacote contém a assinatura drand em vez de exigir que o verificador a obtenha. A descodificação com bloqueio temporal (tlock sobre a cadeia drand, §8) só pode ter sucesso com a genuíno assinatura do farol para a ronda vinculada — um valor que a cadeia publica apenas uma vez que a ronda termina, e que é uma assinatura BLS válida sob a chave pública da cadeia. Uma assinatura falsificada ou errada falha na verificação BLS ou na descodificação IBE/AEAD. Um pacote que descodifica, portanto, prova: o texto cifrado está ligado à ronda R, e a ronda R já passou. O verificador fixa a cadeia (DrandTimelockProvider::quicknet()) e aplica a verificação de vinculação à ronda do §11, de modo que um pacote não pode reivindicar uma ronda à qual o seu texto cifrado não está vinculado.
Isto é uma prova de condição de libertação, não um carimbo de data/hora de criação. Depois da ronda R ter
decorrido, qualquer pessoa pode criar um novo texto cifrado para R e empacotar o que já é público
assinatura. Portanto, o pacote por si só NÃO DEVE ser descrito como prova de que o
texto cifrado ou conteúdo existia antes de R, antes unlock_at, ou antes de qualquer evento.
17.4 Orientação para verificação offline
qub-verify <file.qub> executa o procedimento padrão §11 inteiramente a partir do pacote, conduzindo qub_core::unlock::unlock com um fixado DrandTimelockProvider:
1. Parse the .qub bytes → QubBundle (canonical-CBOR guard; bound every field;
re-check drand_round / drand_chain_id against the embedded sealed qub).
2. BLS-verify bundle.drand_signature for the pinned chain and round, then
tlock_decrypt(sealed.tlock_ciphertext, bundle.drand_signature) → QubEnvelope.
3. Verify SHA3-256(body) == body_hash (§11 step 8).
4. Verify QubEnvelope.qub_id == SealedQub.qub_id (§11 step 9).
5. Verify QubEnvelope.unlock_at == SealedQub.unlock_at (§11 step 10).
6. Verify ciphertext round == unlock_round(unlock_at) and the chain binding.
7. If sig_alg != 0x00: verify author_signature (and any cosigner; §9.4).
8. Report integrity, round-elapsed/round-binding, authorship, and cosigner
verdicts separately, plus the recovered body. Do not report a commitment
timestamp unless step 9 succeeds.
9. Optional existence-time leg: verify an included §16 proof through its pinned
anchor, or independently verify the referenced storage transaction. Report
its block time as an upper bound on ciphertext existence.
A CLI termina 0 (verificado), 1 (verificação falhou — ainda bloqueado, incompatibilidade de hash do corpo, ligação rodada/cadeia quebrada, ou uma assinatura que falha na verificação), ou 2 (uso / pacote malformado). A --json o relatório apresenta os mesmos veredictos para automação. Como o pacote é autónomo, o crate de verificação (qub-core) e a CLI (qub-verify) são os únicos softwares de que um terceiro precisa; ambos são públicos e reutilizam o caminho de verificação existente do protocolo — sem criptografia personalizada.
17.5 Relação com o registo de transparência
inclusion_proof é um slot opcional para a prova de inclusão de Merkle §16. A verificação apenas do pacote (§17.4) está completa para integridade, encadernação redonda / tempo decorrido redondo, e autoria opcional, mas intencionalmente não possui nenhuma reivindicação de existência com carimbo temporal independente. Um preenchido, totalmente verificado por ancoragem inclusion_proof adiciona o compromisso específico do tipo de folha e o tempo máximo de §16.11 sem alterar a versão do formato do pacote. Uma prova ausente significa apenas "nenhuma prova incluída"—não "inválida" e não necessariamente "não ancorada."
Na implementação de referência, o slot está agora digitado: qub_core::export::QubBundle::inclusion_proof_typed() retorna um Option<InclusionProof> carregando a estrutura completa §16.9 (folha, caminho de auditoria, raiz ancorada, e AnchorRef) através do mesmo campo CBOR opaco — sem alteração da versão do formato de pacote. O independente qub-verify A CLI consome-o através do seu --anchor perna, e — até que a carteira de âncora seja provisionada (§16, Estado) — relata uma prova de proprietário preenchida-mas-com-substituto como apenas-inclusão em vez de totalmente verificada-âncora.