Especificação do protocolo qub
qub é um protocolo de compromissos temporais criptográficos: um sistema para selar palavras a uma data futura e provar, quando essa data chegar, exatamente o que foi dito e quando.
Três primitivas o tornam possível. drand é um beacon de aleatoriedade descentralizado — a data de revelação é imposta pela física, não pela boa vontade de qualquer parte. O armazenamento público e permanente é um repositório público à prova de adulteração — nenhuma parte pode editar ou apagar um qub depois de selado. ML-DSA-65 é uma assinatura digital pós-quântica — cada qub é vinculado a um par de chaves cujo segredo nunca deixa o dispositivo do autor.
Juntas, essas primitivas produzem uma declaração com bloqueio temporal, à prova de adulterações e atribuível — um recibo cujo valor cresce à medida que a capacidade do mundo de fabricar o passado melhora.
O restante deste documento é a especificação normativa exigida para implementações interoperáveis.
Especificação do protocolo qub
| Campo | Valor |
|---|---|
| Versão | 1.0 (versão do protocolo 0x01, versão do wrapper externo 0x01) |
| Data | 2026-05-01 |
| Status | Rascunho |
| Revisado até | 2026-05-01 |
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, // 0x01 = public (only value in MVP)
content_type: u8, // 0x01 = text (only value in MVP)
plaintext: Vec<u8>, // UTF-8 qub body
sender_label: Option<String>, // Decorative display name; not authenticated
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>, // V1.1 — when reality renders judgment (verdict-uplift-plan §3.1)
sender_label: Option<String>, // Decorative; not authenticated in MVP
reply_to: Option<[u8; 32]>,// Parent qub_id for reply chains; not in qub_id preimage; not signed (see §9.3)
body: Vec<u8>, // Content payload (UTF-8 for text, CBOR for pact)
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 (qub de texto sem assinatura): version = 0x01, content_type = 0x01, sig_alg = 0x00, todos os campos Option ausentes.
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). Gravado no armazenamento permanente. É o artefacto on-chain.
SealedQub {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Same as QubEnvelope.qub_id
visibility: u8, // 0x01 = public; v1 viewers reject other values
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // V1.1 — 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
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 control characters.
}
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>, // V1.1 — propagado de QubEnvelope.outcome_at / SealedQub.outcome_at; alimenta o bloco verdict-watch na página de revelação (verdict-uplift-plan §5.1)
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 (V1.1 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 (V1.1 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)
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].
Codificação de outcome_at: a V1.1 estendeu a pré-imagem de 92 para 100 bytes, de forma a integrar o campo opcional outcome_at na vinculação. A ausência de outcome_at é codificada como 8 bytes a zero; os validadores do protocolo rejeitam em todo o lado outcome_at <= 0, pelo que esta sentinela não pode colidir com um valor legítimo. Ver §3.2 (formato de wire) e o documento no repositório tasks/verdict-uplift-plan.md para o mecanismo de veredito que motiva este campo.
Codificação de drand_round: a V1.2 estendeu a pré-imagem de 100 para 108 bytes, de forma a integrar drand_round (o round drand alvo, §4.3) na vinculação, e elevou o separador de domínio para QUB_ID_V2. Isto vincula o round de bloqueio temporal à identidade do qub: um gateway não pode revincular o ciphertext a um round diferente (por exemplo, já passado) daquele que o unlock_at apresentado implica. O procedimento de desbloqueio (§8) verifica adicionalmente que o round embutido na stanza do ciphertext tlock corresponde a unlock_round(unlock_at), pelo que o instante de desbloqueio apresentado é, de forma comprovável, o round que controla a decifragem.
Propriedades:
- A alteração de qualquer campo do QubEnvelope (corpo, timestamps, tipo de conteúdo, versão) produz um qub_id diferente.
- O qub_id é calculado antes da cifragem. Tanto o QubEnvelope como o SealedQub carregam o mesmo qub_id. O leitor verifica a igualdade após a decifragem.
- O qub_id não depende de
sender_label,author_signaturenemauthor_pubkey. Isso significa que o mesmo conteúdo selado no mesmo instante produz o mesmo qub_id, independentemente de quem assina. - Alterar o
titledo SealedQub (com tudo o mais fixo) altera oqub_idviatitle_hash. Um gateway, portanto, não pode trocar o título em texto puro exibido na contagem decrescente sem invalidar a identidade do qub. - Alterar o
outcome_atdo SealedQub (com tudo o mais fixo) altera oqub_idvia a pré-imagem. Um gateway não pode trocar a data de veredito apresentada na contagem decrescente antes da revelação sem invalidar a identidade do qub. - Alterar
drand_round(com tudo o mais fixo) altera oqub_idvia a pré-imagem. Um gateway não pode revincular o ciphertext de bloqueio temporal a um round diferente sem invalidar a identidade do qub; combinado com a verificação do round da stanza no momento do desbloqueio (§8), ounlock_atapresentado é o round que realmente controla a decifragem.
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 entre instante de desbloqueio e round
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Parâmetro | Origem | Exemplo |
|---|---|---|
unlock_at |
Segundos Unix UTC escolhidos pelo utilizador | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
drand chain info (genesis_time) |
1595431050 |
chain_period_seconds |
drand chain info (period) |
30 |
A operação ceil() seleciona o primeiro round drand cujo instante de revelação é ≥ unlock_at. Isso garante que o qub não se torne decifrável antes do instante de desbloqueio escolhido.
Caso limite: se (unlock_at - chain_genesis_time) for exatamente divisível por chain_period_seconds, o resultado é precisamente esse round — o qub é desbloqueado exatamente no instante de revelação desse round.
Validação: unlock_at DEVE estar no futuro no momento do selamento. unlock_at NÃO DEVE estar mais de 10 anos além de created_at (para limitar o risco de dependência drand de longo prazo; a interface DEVERIA avisar para datas de desbloqueio além de 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 do SealedQub | serialize_sealed_qub() |
Upload ao armazenamento permanente, fetch do leitor |
QubEnvelopeCbor |
CBOR canónico do QubEnvelope | serialize_qub_envelope() |
Entrada do tlock encrypt, saída do tlock decrypt |
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), value: String (≤ 2,000) } // NFC on both sides
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }
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.
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 = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
(Computed here, before qub_id, because drand_round is bound into the qub_id
preimage — §4.1, V1.2.)
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,
unlock_at, drand_chain_id, drand_round.
12. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12a. Generate K = 32 random bytes (CSPRNG) and N = 12 random bytes (CSPRNG).
Compute W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
per §13. The bytes uploaded to permanent storage are the OuterWrapper CBOR W,
never the bare SealedQubCbor. K leaves the device only as the URL
fragment in step 16.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit W (the OuterWrapper bytes) to the qub upload service; the service
signs and uploads to permanent storage. The service is byte-blind to the inner
SealedQubCbor and never receives K.
16. Receive arweave_tx_id from the service. Construct delivery URL as
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (or `<origin>/s/<short_code>#<base64url(K)>`
when a short code is allocated). Browsers do not transmit URL fragments
to servers, so K is never observed by qub.social or any storage gateway.
Camada de tags de armazenamento (fora de banda). O serviço de upload do qub anexa um conjunto deliberadamente pequeno de tags de transação de armazenamento junto com o payload empacotado. Content-Type=application/octet-stream é normativamente exigido. O serviço de referência adicionalmente anexa três tags opcionais quando o criador escolhe expô-las: Intent (intenção de composição validada por allowlist — por exemplo, quote, reply, commitment), Author (impressão digital da pubkey §9.3 do criador como hex minúsculo de 64 caracteres) e Parent-Tx-Id (ID da transação de armazenamento do qub pai para cadeias de respostas, base64url de 43 caracteres).
A tag Author é opt-in por qub: a aplicação de referência do criador a anexa somente quando o utilizador ativa explicitamente a atribuição pública no momento do selamento. Quando o toggle está desligado — a predefinição — nenhuma tag Author é gravada e o qub é não atribuído na cadeia: nada no armazenamento permanente vincula o upload ao @handle do criador, e-mail ou outros qubs. Quando o toggle está ligado, a impressão digital Author resolve-se ao @handle escolhido pelo criador via a cadeia de atestações §9.5. Os relacionamentos de cadeia de respostas e Intent não são identificadores. O wrapper externo (§13) protege o corpo interno contra correlação de ciphertext — impedindo que um harvester reconheça e decifre em massa uploads em formato de qub depois que seu round drand for publicado.
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 path AND
K = base64url_decode(fragment) from the URL fragment. If the fragment
is absent or malformed → display "this URL is missing its decryption
key" and stop; the viewer MUST NOT contact the storage gateway
without K, since fetching wrapped bytes the viewer cannot decrypt
serves no purpose and only leaks the access attempt.
2. Check denylist. If tx_id is denylisted → display block message. Stop.
3. Fetch OuterWrapper bytes from permanent storage (with multi-gateway fallback).
3a. Unwrap: parse the bytes as OuterWrapper (§13), verify the wrapper
`version` byte is `0x01`, and compute SealedQubCbor =
unwrap_sealed_qub(OuterWrapper, key=K). Any AEAD authentication
failure (wrong K, tampered ciphertext, swapped qub_id-as-AAD,
swapped nonce) → display "this URL's decryption key does not match
the stored qub" and stop. Authentication failures are
indistinguishable to the viewer per §13.5.
4. Parse SealedQubCbor → SealedQub.
5. Validate: SealedQub.version is known (0x01). Reject unknown versions.
6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
6a. Round-binding check (V1.2). Recompute expected_round =
ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
Reject unless SealedQub.drand_round == expected_round AND the round baked
into the tlock ciphertext stanza (read via the age/tlock header, no signature
required) == expected_round. 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.
13. Verify: QubEnvelope.content_type is known and renderable.
Known values: 0x01 (text), 0x03 (pact). 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 appropriate renderer (see §10 for text, §6 for pact).
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 |
|---|---|---|---|
0x00 |
Sem assinatura (não assinado) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1.952 bytes | 3.309 bytes |
Os leitores DEVEM rejeitar valores sig_alg desconhecidos.
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 pela assinatura | Como |
|---|---|---|
version |
✓ | Entrada direta de 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 pré-imagem de qub_id |
created_at |
✓ | Transitivamente, via pré-imagem de qub_id |
outcome_at |
✓ | Transitivamente, via pré-imagem de qub_id |
drand_round |
✓ | Transitivamente, via pré-imagem de qub_id (V1.2) |
body |
✓ | Transitivamente, via body_hash = SHA3-256(body) |
author_pubkey |
— (implícito) | A chave que verificou a assinatura é, por definição, o autor |
cosigner_pubkey / cosigner_signature |
— | Assinados independentemente sobre o mesmo sig_input (ver §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Campos do SealedQub externo, fora do envelope — cobertos pelos seus próprios invariantes estruturais (consistência de round / chain), mas não pela assinatura do autor. (drand_round está agora vinculado transitivamente via a pré-imagem de qub_id — ver 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 pode verificar um qub público sem cooperação do qub. O procedimento de verificação:
1. Obtain arweave_tx_id (from delivery URL or direct knowledge).
2. Fetch SealedQubCbor from any storage gateway.
3. Confirm storage block inclusion (block height, block timestamp).
4. Parse SealedQubCbor → SealedQub.
5. Fetch drand round signature for SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify QubEnvelope.qub_id == SealedQub.qub_id.
10. Verify QubEnvelope.unlock_at == SealedQub.unlock_at.
11. If sig_alg != 0x00: verify author_signature (see §9.4).
12. All checks pass → qub is verified.
O que a verificação prova:
| Prova | O que estabelece |
|---|---|
| Compromisso | O ciphertext existia até ao timestamp do bloco de armazenamento. |
| Integridade | O corpo em texto puro corresponde ao hash comprometido e não foi alterado. |
| Temporização | O conteúdo era ilegível até ao round drand, que corresponde ao instante de desbloqueio escolhido (sujeito às hipóteses de segurança do tlock e drand). |
O que a verificação NÃO prova:
| Não-prova | Por quê |
|---|---|
| Autoria | O sender_label é decorativo. Sem sig_alg ≥ 0x01, qualquer pessoa poderia ter selado este conteúdo. |
| Intenção | O qub prova conteúdo e temporização, não o que o criador queria dizer subjetivamente. |
| Temporização pré-evento | A inclusão no bloco de armazenamento pode atrasar o upload real em minutos. O timestamp do compromisso é o instante do bloco, não o momento em que o utilizador clicou em "selar". |
12. Versionamento
12.1 Versão do protocolo
O campo version (u8) tanto em SealedQub como em QubEnvelope identifica a versão maior do protocolo.
- Os leitores DEVEM rejeitar versões maiores desconhecidas com um erro claro.
- Dentro de uma versão maior conhecida, os descodificadores DEVEM rejeitar chaves de mapa desconhecidas (§3.1) — a evolução do esquema passa pela introdução de uma nova
version, e não pela adição de chaves que os descodificadores existentes ignorariam. (Revisões anteriores desta especificação permitiam tolerar campos opcionais desconhecidos; essa cláusula foi retirada — tornavaencode(decode(x))não injetivo e abria um vetor de conteúdo assinado oculto nas cargas úteis de pacto.) - Tipos de conteúdo (
content_type) e esquemas de assinatura (sig_alg) são gated por versão: novos valores só podem ser introduzidos junto com uma nova versão de protocolo ou atualização explícita do registo.
12.2 Histórico de versões
| Versão | Valor | Descrição |
|---|---|---|
| v1 | 0x01 |
qubs de texto público (content_type 0x01), acordos bilaterais de pacto (0x03, esquema structured/v1, autor + co-assinante ML-DSA-65), tlock, SHA3-256 |
12.3 Compatibilidade futura
Um leitor v1 que encontre um QubEnvelope com chaves de mapa CBOR desconhecidas (chaves não presentes na ordem canónica do §3.2) DEVE rejeitá-lo com um erro de descodificação (§3.1). A compatibilidade futura assenta no campo version, e não na tolerância a chaves: adições futuras — mesmo metadados menores — são disponibilizadas sob um novo valor de version, que um leitor v1 rejeita com um erro claro de «protocolo mais recente», em vez de descartar silenciosamente conteúdo ao qual as assinaturas ficam vinculadas.
Um leitor v1 que encontre sig_alg = 0x01 (ML-DSA-65) mas não tenha suporte de verificação ML-DSA-65 DEVERIA exibir o conteúdo do qub com um aviso "assinatura presente, mas não verificável", em vez de rejeitar o qub por completo. A implementação de referência hoje rejeita todo valor de sig_alg exceto 0x00 e 0x01 porque o registo v1 não contém outro algoritmo válido — rejeição estrita e soft-fail são observacionalmente idênticas até que um terceiro algoritmo seja registado. O comportamento de soft-fail acima passa a ser carregador uma vez que §9.2 admita uma nova entrada, e o leitor de referência será atualizado para soft-fail nesse momento.
12.4 Versão do wrapper externo
O OuterWrapper descrito em §13 carrega seu próprio byte version, independente de SealedQub.version e QubEnvelope.version. Os dois espaços de versão evoluem separadamente: uma futura substituição simétrica resistente ao quântico aumenta o byte do wrapper sem tocar na versão interna do protocolo, e uma futura adição na camada do protocolo (por exemplo, um novo campo do envelope) aumenta a versão interna sem tocar no byte do wrapper.
OUTER_WRAPPER_VERSION_* |
Valor | Algoritmo | Status |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM com nonce de 12 bytes, tag de autenticação de 16 bytes, AAD vinculada a qub_id |
padrão v1 |
| — | 0x02–0xFF |
Reservado | Futuro |
Os leitores DEVEM rejeitar versões de wrapper desconhecidas com um erro claro. O protocolo intencionalmente mantém o espaço de versões de wrapper estreito até que um motivador concreto de migração apareça (por exemplo, orientação NIST a favor de outro AEAD); um slot 0x02 será alocado na mesma revisão que introduzir 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.
O wrapper externo de cifragem fecha esse canal interpondo uma camada AEAD simétrica adicional entre o SealedQubCbor canónico e os bytes carregados ao armazenamento permanente. A chave de 256 bits K vive apenas no fragmento de URL do link de entrega e nos dispositivos do utilizador; navegadores não transmitem fragmentos de URL para servidores, então qub.social, todo gateway de armazenamento e toda CDN à frente de qualquer um deles é observacionalmente cego a K. Cada qub no armazenamento permanente é, portanto, um ciphertext opaco cujo texto puro é irrecuperável sem a URL que o criador escolheu partilhar.
Efeito líquido:
- Imunidade à enumeração por predefinição. Os bytes empacotados no armazenamento permanente são byte-indistinguíveis de ciphertext arbitrário. Uma estratégia de harvester de "consultar GraphQL por uploads em formato qub e decifrar em massa com assinaturas drand públicas" não termina em texto puro.
- Postura de privacidade por crypto-shredding. qub.social literalmente não pode decifrar seu próprio corpus. Intimações alcançam ciphertext, não texto puro.
- Escada de confidencialidade em duas camadas. Padrão = acesso controlado por link (esta seção). Os qubs privados cifrados ao destinatário (uma funcionalidade reservada para a Fase 2, ainda não especificada) sobrepõem-se como segunda camada.
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
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12a, this section)
OuterWrapper CBOR bytes ← uploaded to permanent storage (§7 step 15)
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.4
qub_id: [u8; 32], // copied from inner SealedQub; AEAD AAD
nonce: [u8; 12], // 96-bit AEAD nonce
ciphertext: Vec<u8>, // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}
Invariantes dos campos.
versionDEVE ser igual a0x01para bytes de wrapper v1.0.qub_idDEVE ser igual ao campoqub_iddo SealedQub recuperado após o unwrap. A etapa de unwrap não impõe isso diretamente (a vinculação AAD do AEAD torna a adulteração no nível dos bytes impossível), mas a camada de desbloqueio verifica a relação transitivamente: se um criador empacota umSealedQubCborcujoqub_idinterno não corresponde aoqub_iddo wrapper, a etapa 11 do §8 falha.nonceDEVE ter 96 bits (12 bytes), gerado novo por um CSPRNG para cada operação de wrap. Reutilizar um nonce com a mesma chave permite ataques de reuso de nonce no AEAD que recuperam o texto puro; os produtores DEVEM tratar pares (key,nonce) como de uso único.ciphertexté a saída do AES-256-GCM: bytes de ciphertext concatenados com a tag 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 ciphertext sob um campo qub_id diferente no wrapper |
Incompatibilidade de AAD → autenticação AEAD falha |
| Misturar o fragmento de URL do qub A com os bytes de armazenamento do qub B | Incompatibilidade de AAD → autenticação AEAD falha |
Adulterar o campo qub_id do wrapper após o upload |
Incompatibilidade de AAD → autenticação AEAD falha |
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
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C includes the 16-byte authentication tag at the end
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
require K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
require O.version == 0x01 // §12.4
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
return P // P is the inner SealedQubCbor
Colapso 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 dentro do
QubEnvelopeinterno e são recuperadas após unwrap → tlock decrypt → parse de CBOR. - Os qubs privados cifrados ao destinatário (uma funcionalidade reservada para a Fase 2, ainda não especificada) compõem-se em cima deste wrapper como segunda camada de confidencialidade; ambas as camadas podem estar ativas simultaneamente.
- Pactos (§6, content_type
0x03) são empacotados exatamente como qubs de texto; o wrapper é cego em bytes ao tipo de conteúdo interno.
13.8 qubs públicos (omissão do wrapper)
O wrapper externo é opcional na camada de entrega. Um criador pode selar um qub como público, caso em que o SealedQubCbor canónico é gravado no armazenamento permanente diretamente, sem camada OuterWrapper e sem chave K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
Um qub público tem bloqueio temporal, mas não controlo por link: permanece ilegível até que seu round drand seja publicado (a camada tlock não muda), mas após o desbloqueio qualquer um que tenha o arweave_tx_id pode decifrá-lo — nenhum fragmento de URL é necessário, porque não há K. Esta é a troca deliberada para as superfícies que o servidor precisa conduzir: e-mails de notificação de revelação, embeds de terceiros e um SEO pós-revelação mais rico precisam todos de um link que funcione sem um segredo que o servidor nunca detém (§13.6).
Consequências que um produtor DEVE considerar:
- Sem imunidade à enumeração. Os qubs públicos abrem mão da propriedade de imunidade à enumeração do §13.1 por construção. O serviço de upload de referência carimba uma tag de armazenamento permanente
Visibility: publicneles (e somente neles) para que sejam intencionalmente descobríveis; qubs privados não carregam tal tag e mantêm a sua byte-indistinguibilidade. - Título em texto puro exposto no momento do selamento. O campo
titledo §3.2 está em texto puro dentro doSealedQubCbor. Sob o wrapper ele fica oculto até que um leitor forneçaK; sem o wrapper é legível por todo o mundo no armazenamento permanente desde o momento do upload, antes do desbloqueio. Aplicações de criador conformes DEVEM divulgar isso no momento do selamento. - A deteção é estrutural. Um leitor/embed conforme distingue os dois formatos pelo parse: bytes que se parseiam como
OuterWrapperseguem o caminho de unwrap-com-K; bytes que se parseiam como umSealedQubCborsimples são aceitos diretamente. Nenhuma flag de wire é necessária, e oqub_idnão vincula a visibilidade — o mesmo conteúdo é byte-idêntico na camadaSealedQubquer seja selado público ou privado.
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 = 4695445 (= (1736294400 - 1595431050) / 30, drand mainnet params §14.2)
body = "Hello, future." (UTF-8, 14 bytes)
title = absent
Intermediate:
body_hash = SHA3-256("Hello, future.")
= 76ab8b3f843c6ed4f2d0fd75b9f457b4
ad49dd4450f9c22723ae430e3af3211d
title_hash = [0u8; 32] (title absent — §4.2.1 sentinel)
Domain separator (10 bytes):
[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]
Preimage (108 bytes — V1.2):
domain_separator || // 10 bytes
0x01 || // version
0x01 || // content_type
0x0000000067748580 || // created_at as i64 big-endian (1735689600)
0x00000000677DC000 || // unlock_at as i64 big-endian (1736294400)
0x0000000000000000 || // outcome_at_or_zero (outcome_at absent)
0x000000000047A595 || // drand_round as u64 big-endian (4695445)
body_hash || // 32 bytes
title_hash // 32 bytes (all-zeros sentinel; title absent)
Expected output:
qub_id = SHA3-256(preimage)
= 3a9fcb31b750d985c262fada6d4f777f
d6a28be831d941d85c131f5a4bbaf8a4
As implementações DEVEM produzir valores idênticos de body_hash e qub_id para esta entrada. Este vetor de teste DEVERIA 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. Disposições históricas da pré-imagem (pré-lançamento — nenhum qub em produção dependia destas): o qub_id V1.0 de 92 bytes era 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; o qub_id V1.1 de 100 bytes (após integrar outcome_at_or_zero) era b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. A V1.2 integra drand_round e eleva o separador de domínio para QUB_ID_V2.
14.2 Mapeamento entre instante de desbloqueio e round
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calculation:
(1735689600 - 1595431050) / 30 = 4675285.0
ceil(4675285.0) = 4675285
drand_round = 4675285
14.3 Round-trip 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).
A fixture atualmente fixa três casos:
| Caso | Cobertura |
|---|---|
basic-text-public |
Forma realista mais simples de SealedQub; nenhum campo opcional. Estabelece a forma canónica do wrapper para um qub típico v1.0. |
with-recipient-pubkey |
SealedQub com recipient_pubkey definido (caminho da Fase 2). Conjunto de chaves CBOR interno diferente, qub_id diferente. |
longer-body |
Corpo de ~4 KiB — exercita prefixos de comprimento CBOR de múltiplos bytes tanto no envelope interno como no ciphertext 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 assinado (sig_alg = 0x00). O registo do §9.2 não define nenhum outro valor; um verificador v1 DEVE rejeitar todosig_algfora de{0x00, 0x01}. Uma futura entrada Ed25519 é antecipada (§15.3), mas não é alocada na v1. - Timelock: somente drand quicknet — o hash da chain, a chave pública, o instante de gênese e o período são parâmetros de rede fixos carregados pela implementação de referência
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) e porconfig/drand-endpoints.json. - Wrapper externo: somente AES-256-GCM v1 (§13).
Os verificadores atualmente fixam no código os comprimentos de chave e de assinatura por primitiva. Nenhuma superfície de agilidade é exposta pelo formato de fio.
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 byte
sig_alg(ativação de Ed25519, ML-DSA-87 ou qualquer nova entrada no registo do §9). - Uma segunda chain drand em uso de produção.
- Uma segunda versão de wrapper externo.
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)
Status. Esta seção é uma especificação de design. Os formatos de fio, o hashing e o modelo de confiança abaixo são normativos para a implementação, mas ainda não foi lançado nenhum código de registo de transparência. A revisão externa da W5 está concluída: §16.15 regista as decisões resolvidas e as restrições de lançamento vinculativas que dela resultaram. A implementação pode prosseguir sob essas restrições. O §16 mantém-se prospetivo no mesmo sentido que o §15 — fixa o alvo para que a implementação aterrisse contra um design assente, em vez de re-derivar o modelo de confiança em revisão de código. É estritamente aditivo — cada qub existente mantém a sua transação Arweave individual e não há alteração ao formato de fio do
SealedQub/QubEnvelope.
16.1 Justificativa e níveis de durabilidade
Hoje, a durabilidade de um qub e o seu compromisso temporal assentam ambos numa única transação Arweave por qub (§11). Isso acopla a latência do selamento à finalidade do Arweave, faz do upload por qub um teto de custo do produto (ARWEAVE_DAILY_CEILING) e não oferece ordenação detetável de adulteração entre qubs. O registo de transparência acrescenta duas camadas por baixo e em torno desse nível único:
| Nível | Nome | Garantia | Quando |
|---|---|---|---|
| T1 | Ack síncrono R2-first | Piso de durabilidade — os bytes selados são escritos para armazenamento durável antes de o selamento retornar (< 300 ms p95). |
Cada qub, de forma síncrona (§16.10). |
| T2 | Inclusão em registo de transparência por lotes | Compromisso universal append-only e detetável de adulteração + ordenação total, ancorado ao Arweave. | Cada qub, diferido + por lotes (§16.5–16.7). |
| T3 | Permanência Arweave por qub | Uma transação Arweave individual para o qub. | Upsell pago, e o fallback de indisponibilidade do Arweave (§16.8). |
O T2 torna o Arweave por qub uma escolha (T3) em vez do único caminho de durabilidade. ARWEAVE_DAILY_CEILING é descontinuado como teto do produto e rebaixado a um disjuntor apenas sobre a carteira de âncora dedicada (§16.7); os selamentos do utilizador nunca são rejeitados por o excederem.
Honestidade de durabilidade (resolvido — §16.15 Q6). A durabilidade não regride: a escrita R2 do T1 é síncrona e write-once, pelo que um qub de nível gratuito que não comprou o T3 fica totalmente durável no instante em que o selamento retorna. O que se torna mais grosseiro é o instante de compromisso de limite superior comprovável: para um qub gratuito, passa a ser o tempo do bloco de âncora em vez do tempo do bloco de uma transação por qub. Em volume baixo — o estado realista de arranque inicial e de fora de pico — a cadência diária completa é o piso típico, não um caso-limite raro. O enquadramento do produto é, portanto, um limite superior sem latência numérica comprometida — "selado e durável agora; um timestamp público independente é adicionado na próxima âncora do registo (tipicamente diária)" — e a prova de compromisso à hora exata é uma propriedade paga do T3, divulgada na superfície de comparação de níveis e nos termos (§16.11, §16.15 Q6). Qualquer limite temporal é apenas um SLO interno, nunca um SLA comercializado.
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 leaf tem duas formas selecionadas por um byte kind, porque no caminho de upload por predefinição o Worker é byte-cego: POST /api/v1/upload recebe apenas qub_id e unlock_at como afirmações não confiáveis do cliente — body_hash, drand_round, created_at e drand_chain_version estão todos selados dentro do wrapper externo do §13, cuja chave o Worker nunca detém. Apenas o caminho de selamento no servidor (POST /api/v1/seal) deriva body_hash / drand_round do texto puro. Uma forma de leaf única que carregasse body_hash + drand_round comprometeria, portanto, valores que o operador nunca verificou para a maioria dos qubs reais. A divisão mantém honesto cada valor comprometido:
| Chave | Comp. cod. | Tipo | Presença | Significado |
|---|---|---|---|---|
seq |
4 | u64 |
obrigatório | Índice global de leaf de base 0; a posição a que a prova de inclusão se compromete. |
kind |
5 | u8 |
obrigatório | 0x01 atestado (selamento no servidor) ou 0x02 afirmado (selamento no cliente / upload byte-cego). |
ref |
4 | bstr[32] |
obrigatório | Id de referência da leaf. Atestado → qub_id cru. Afirmado → o id cego SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
6 | bstr[32] |
obrigatório | Endereço de conteúdo SHA3-256(stored_bytes) — o único laço de conteúdo que o Worker consegue sempre computar honestamente, em ambos os caminhos. |
unlock_at |
10 | i64 |
obrigatório | Copiado (atestado) ou afirmado (afirmado); validado > 0 antes de entrar na leaf. |
received_at |
12 | i64 |
obrigatório | Relógio de parede do Worker no ack do R2. Não-evidenciário (afirmado pelo operador; §16.6). Presente para autodescrição, nunca uma prova. Validado > 0. |
body_hash |
10 | bstr[32] |
apenas kind=0x01 |
Omitido em 0x02 — o Worker não o tem sob o §13. |
drand_round |
12 | u64 |
apenas kind=0x01 |
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 Durable Object LogDO é o único escritor (blockConcurrencyWhile, espelhando QuotaDO / EntitlementDO) — acrescentar a um registo partilhado é read-modify-write sobre estado partilhado e portanto DEVE passar por um DO, nunca por KV. Mantém em cache a fronteira da borda direita da árvore (O(log n) hashes) para que fechar um lote seja O(batch). Um lote é o conjunto de leaves ancoradas em conjunto; os seus gatilhos são configuráveis, não fixados pelo protocolo: um avanço de tree_size de pelo menos LOG_BATCH_MAX_LEAVES (predefinição 4096), ou a idade a atingir a cadência de âncora, ou um flush forçado quando um selamento T3 pago aterra. root_i é o Merkle Tree Hash cumulativo sobre as leaves 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.
Existe exatamente uma chave de assinatura quente do qub no design, e está fixada: a chave de recibo por selamento (§16.10). A sua chave pública está comprometida em LogProfile (distribuído com o verificador) e co-assinada por anchor_owner, pelo que um verificador valida um recibo contra a mesma raiz fixada que a âncora. Esta é a resolução do §16.15 Q2 — uma chave de recibo não fixada e rotável pelo operador seria repudiável (o operador poderia negar que a chave era sua), o que anularia o valor de responsabilização do recibo contra o adversário de nível-operador que o recibo existe para dissuadir. Portanto: o qub não detém nenhuma chave de assinatura de registo não fixada; a chave de recibo é fixada e co-assinada por anchor_owner.
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 equivocação (parâmetro de confiança de primeira classe). Uma leaf é resistente a equivocação apenas depois de a sua âncora cobertora estar confirmada no Arweave. A janela é received_at → confirmação da âncora (≤ cadência + finalidade do Arweave). Dentro dela, as únicas garantias são o recibo de selamento fixado (§16.10) e a integridade operacional do qub. Três artefactos de responsabilização tornam isto honesto em vez de uma generalização vaga (o modelo de testemunha é a resolução do §16.15 Q2):
- Recibo de selamento assinado e fixado — o análogo de SCT retornado na resposta de upload (§16.10), assinado pela chave de recibo fixada e co-assinada por
anchor_owner. Uma leaf descartada antes da sua âncora deixa à vítima um recibo não-repudiável para publicar, fechando o buraco da omissão silenciosa. - Metodologia de monitor publicada + percurso da cadeia prev — a cadeia
prevda âncora é percorrida cabeça→gênese; um fork (duas âncoras num mesmosizecomrootdiferente, ou umprevpartido) é prova publicável de má conduta. A deteção de equivocação é um compromisso operacional declarado, não uma suposição silenciosa. - Cabeças autopublicadas duplas — cada nova cabeça
{sth_hash, tree_size}é publicada num repositório GitHub público e append-only, detido pelo qub e dedicado (a perna carregadora de autopublicação detetável de adulteração), com uma publicação social como corroboração best-effort apenas. Uma publicação falhada DEVE acionar paging (não falhar em silêncio).
Limite de honestidade (restrição vinculativa). Como o qub controla ambas as superfícies de publicação, isto é autopublicado, não testemunhado de forma independente. Nenhuma superfície de produto, marketing ou legal pode afirmar que o registo é "testemunhado de forma independente"; a afirmação permitida é que a equivocação é detetável e deixa um recibo não-repudiável. Uma verdadeira testemunha terceira independente fica diferida para um futuro bump de governança do §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: diária por predefinição, revisitada com o volume (o gatilho de tamanho encurta automaticamente a cadência efetiva sob carga); um selamento T3 pago força uma âncora para que os clientes pagantes nunca esperem um dia. A carteira de âncora é dedicada e de baixa velocidade, separada da carteira de upload — DEVE ser a sua própria JWK (uma chave distinta, não um papel lógico sobre a carteira de upload) para que um comprometimento da carteira de upload não possa forjar âncoras — com um orçamento rígido de transações de âncora por dia (o rebaixado ARWEAVE_DAILY_CEILING). A postura de custódia é declarada com clareza: uma chave quente de âmbito estreito com um disjuntor apertado e saldo baixo, não "fria" — uma carteira que assina automaticamente todos os dias 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).
Um único caminho de código serve três consumidores: a permanência T3 paga por qub, o fallback de indisponibilidade do Arweave (enfileira o DataItem, retorna o ack R2-first independentemente — isto fecha o beco sem saída atual do 503 ARWEAVE_UNAVAILABLE) e a escrita do AnchorBundle. Esquema de assinatura (resolvido — §16.15 Q8): a v1 assina com RSA-PSS (tipo de assinatura 1) reutilizando o mecanismo existente da JWK da carteira Arweave (zero nova custódia de chaves de longa duração, ao serviço da tese "um segredo a menos"); Ed25519 fica diferido para o caminho de migração PQ do §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
A sequência de POST /api/v1/upload passa a ser:
- Guardas da primeira metade (auth, validação, chave de shard de idempotência) — inalteradas.
- Sincronamente
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— o piso de durabilidade; fecha também a corrida de precache da W1 (anteriormente umctx.waitUntilapós o submit ao Arweave). - Sincronamente
await LogDO.append(leaf)— uma RPC de DO in-colo; o único escritor atribuiseq, estende a cadeia de entradas e atualiza a fronteira. (RMW sobre estado partilhado → DO, nunca KV.) A RPCappendfaz apenas isso — o trabalhoO(batch)de fecho de lote Merkle corre fora desta RPC no alarme do LogDO, ou o p95 do append dispara a cadaLOG_BATCH_MAX_LEAVES-ésimo selamento. - Retorne o ack agora — com o recibo de selamento (assinado pela chave de recibo fixada, §16.6) e
{ tx_id, log_seq, anchor_status: "pending" }. O fan-out Arweave de vários segundos é removido do caminho crítico. - Um
ctx.waitUntilenfileira o trabalho diferido: o submit Arweave por qub (agora best-effort / pago; em caso de falha, encaminha para a fila de fallback do bundler em vez de devolver 503 ao utilizador) mais as escritas de meta provisória existentes. O fecho de lote e a ancoragem correm de forma independente, a partir do alarme do LogDO e do cron diário de âncora. Nenhumctx.waitUntildentro de um loop; a chave de shard de idempotência existente é preservada.
Orçamento de latência (resolvido — §16.15 Q7). O alvo de < 300 ms p95 é uma porta de lançamento medida, não uma suposição. O caminho crítico honesto é as leituras KV da primeira metade + um PUT ao R2 + dois Durable Objects serializados — o débito de quota de selamento do QuotaDO existente e o novo append do LogDO — pelo que o orçamento deve contabilizar duas viagens de ida e volta de DO in-colo, não uma. Envie um alarme de latência do LogDO espelhando o do QuotaDO e trate uma regressão de p95 como um bloqueador de lançamento.
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.
Teto de afirmação (restrição de lançamento vinculativa — resolvido §16.15 Q1). Para qubs gratuitos / por predefinição (kind=0x02), a afirmação kind=0x02 delimitada acima é o teto do que qualquer superfície de produto, marketing, termos ou renderização de prova pode afirmar. Nenhuma superfície pode declarar ou implicar que o registo prova o conteúdo ou o round de desbloqueio de um qub por predefinição — o registo prova ordenação + um instante de compromisso de limite superior sem confiança de um ciphertext opaco. A prova de conteúdo e de round vem exclusivamente da verificação existente do bundle .qub do §11, que é independente do registo. Isto é um bloqueador rígido de lançamento sobre a copy, não uma preferência estilística; é a resolução que mantém honesto o caminho por predefinição byte-cego.
16.12 Versionamento e coordenação com a W3
Não há bump de fio do SealedQub e portanto não há bump de versão de protocolo (§12.1): o registo é um sidecar que se compromete a campos e bytes existentes, pelo que não entra no histórico de versões de protocolo do §12.2. O drand_chain_version opcional da W3 fica intocado e mantém-se o único campo opcional do SealedQub. O registo introduz, em vez disso, os seus próprios espaços de versão independentes — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — espelhando a independência de versão do wrapper do §12.4 (o wrapper carrega 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 arquivo 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 Decisões da revisão (W5 — resolvidas)
A revisão externa da W5 (uma passagem de design adversarial + sign-off do owner) está concluída. Cada decisão abaixo está assente e refletida no texto do §16 acima; as restrições de lançamento vinculativas são reafirmadas no fim. A implementação pode prosseguir sob elas.
- Honestidade da leaf do caminho por predefinição (
kind=0x02) — RESOLVIDO. Envie a divisão em duas formas de leaf conforme especificado:kind=0x02não compromete nembody_hashnemdrand_round. Nenhum campo*_body_hashno caminho byte-cego (seria o sinal falso de "verificado" mais legível para integradores e é uma conveniência que o §11 já fornece a partir do bundle). Não exija selamento no servidor para qubs atestados pelo registo (isso forçaria texto puro através do Worker e destruiria o fosso de crypto-shredding). Qualquer atalho autodescritivo pertence ao bundle.qub/ envelope de prova como um campo recomputado pelo verificador, nunca um campo da leaf. Teto de afirmação confirmado pelo owner: §16.11. - Responsabilização por equivocação / omissão — RESOLVIDO. A chave de recibo de selamento é fixada em
LogProfile+ co-assinada poranchor_owner(fechando a contradição anterior do "sem chave de assinatura"; §16.6). Modelo de testemunha de lançamento: recibo fixado + metodologia de monitor + percurso da cadeia prev + cabeças autopublicadas duplas (repositório GitHub público detido pelo qub, social best-effort), comercializado como detetável + com recibo, nunca testemunhado de forma independente. Uma verdadeira testemunha terceira fica diferida para um bump de governança do §15. - Raiz de confiança de anchor-owner fixada + rotação — RESOLVIDO. Adote a fixação de
LogProfile(§16.6); o verificador verificaanchor_tx.owner == anchor_ownere verifica a vinculação dados → tx_id da tx localmente. A governança de rotação é uma extensão a construir do §15 (gatilho do §15.3 acrescentado), não uma reutilização; as rotações planeadas co-assinam, as rotações motivadas por comprometimento recaem sobre o bump do §15 com a verificação de fork a limitar os danos. - Ocultação de leaf de qub privado — RESOLVIDO. Mantenha a ocultação para qubs privados (
ref = SHA3-256(qub_id ‖ log_blind_secret)),qub_idcru para qubs públicos (já §16.2.1),chashcomo laço autónomo.log_blind_secreté um segredo de grau correlação/Sybil, apenas rotação para a frente (§16.2.1). received_at— RESOLVIDO. Mantenha-o na leaf, comprometido mas explicitamente não-evidenciário; nunca apresentado como prova ou corroboração de disputa em qualquer superfície. Qualquer verificação de sanidade de monitor compara contra o tempo do bloco ArweaveT, não contra oanchored_atcontrolado pelo operador (§16.6).- Temporização comprovável de nível gratuito — RESOLVIDO (sign-off do owner). A durabilidade não regride; apenas o instante de compromisso de limite superior comprovável se torna mais grosseiro, passando ao tempo do bloco de âncora. A copy de nível gratuito não usa SLA numérico ("…adicionado na próxima âncora do registo, tipicamente diária"); a prova à hora exata é uma propriedade paga do T3, divulgada na superfície de comparação de níveis + nos termos (§16.1).
- Árvore cumulativa em Workers — RESOLVIDO. Árvore RFC 9162 cumulativa única + LogDO de escritor único com fronteira em cache (margem confortável face ao teto de ~1k escritas/s do DO; adiar o sharding de Merkle-de-raízes-de-shard até perto dele). Pré-condição bloqueadora: armazenamento de nós R2 indexado por coordenada
(level, index)+ o vetor de teste de leaf fria com DO apagado (§16.9);< 300 msé uma porta de lançamento medida sobre dois DOs serializados (§16.10). - Esquema de assinatura ANS-104 + deep-hash — RESOLVIDO. RSA-PSS (tipo de assinatura 1, reutilizando a JWK da carteira de âncora dedicada); Ed25519 diferido para o caminho PQ do §15. O deep hash SHA-384 feito à mão está gated na fixture cross-impl de ambas as direções, numa verificação de interoperabilidade com bundler de referência apenas-estática, no round-trip de
crypto.subtlepartilhado e no monitor de aceitação Arweave pós-bundle (§16.8).
Restrições de lançamento vinculativas (levar para a implementação + revisão de produto/legal):
- Teto de afirmação (Q1/Q6). Nenhuma superfície pode dizer que o registo prova o conteúdo ou o round de desbloqueio de um qub por predefinição; a afirmação permitida é ordenado, de forma detetável de adulteração, com um instante de compromisso de limite superior sem confiança. A copy de timestamp de nível gratuito não carrega latência numérica; a prova à hora exata é apenas T3 pago.
- Honestidade de testemunha (Q2). Comercialize a equivocação como detetável + com recibo, nunca testemunhada de forma independente.
- Chaves de recibo + âncora (Q2/Q8). A chave de recibo é fixada + co-assinada; a carteira de âncora é a sua própria JWK distinta da carteira de upload.
- Porta do deep-hash (Q8). Nenhuma âncora ou tx T3 é enviada até que a fixture de ambas as direções + a verificação de interoperabilidade passem; o monitor de aceitação aciona paging em caso de falha.
- Pré-condição de armazenamento (Q7). O armazenamento de nós indexado por coordenada + o vetor de leaf fria com DO apagado são pré-requisitos para a garantia de que "a reclamação nunca invalida uma prova".