Spécification du protocole qub

qub est un protocole d’engagement temporel cryptographique : un système pour sceller des mots à une date future, puis vérifier exactement ce qui a été scellé, quel tour drand en a conditionné la publication et — lorsqu’une transaction de stockage ou une preuve du journal de transparence est disponible — une borne supérieure horodatée indépendamment du moment où le texte chiffré a été engagé.

Trois primitives le rendent possible. drand est une balise de hasard décentralisée — la date de dévoilement est imposée cryptographiquement plutôt que par la bonne volonté de qub. Un stockage durable associé à un journal de transparence en ajout seul conserve les octets scellés et ancre les engagements groupés dans le stockage public permanent ; le chemin payant T3 écrit également une transaction individuelle dans le stockage permanent. ML-DSA-65 est une signature numérique post-quantique — lorsque la paternité est activée, le qub est rattaché à une paire de clés dont le secret ne quitte jamais l’appareil de l’auteur.

Ensemble, ces primitives produisent une déclaration verrouillée dans le temps et infalsifiable, attribuable de manière facultative et horodatable indépendamment — un reçu dont la valeur croît à mesure que la capacité du monde à fabriquer le passé s’améliore.

La suite de ce document est la spécification normative requise pour des implémentations interopérables.


Spécification du protocole qub

Champ Valeur
Release du document 1.0.0 (protocol-v1.0.0)
Protocole de fil 0x01
Enveloppe externe 0x01
Date d’effet 2026-09-23
Statut Actuel
Vérifié jusqu’au 2026-09-23

Ce document est la spécification normative du protocole pour le système d’engagement temporel qub. Il définit les structures de données, les règles de sérialisation, les formules de dérivation et les procédures de vérification requises pour des implémentations interopérables.

Portée : la couche protocole est intentionnellement neutre vis-à-vis de la langue — le corps d’un qub est un texte/markdown/octets de pacte opaque, et le rendu localisé relève du lecteur (application web qub.social, iframe <qub-embed>, clients MCP, etc.).


1. Notation et conventions

Notation Signification
u8, u64, i64 Entiers non signés/signés de la largeur indiquée
[u8; N] Tableau d’octets de longueur fixe N
Vec<u8> Tableau d’octets de longueur variable
Option<T> Valeur de type T, ou absente
String Chaîne UTF-8, normalisée NFC
`
SHA3-256(x) Hachage NIST SHA3-256 de la chaîne d’octets x (FIPS 202)
ceil(x) Fonction plafond : plus petit entier ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Octet de poids fort en premier

Tous les entiers dans les pré-images sont encodés en tableaux d’octets big-endian de largeur fixe (i64 → 8 octets, u8 → 1 octet) sauf indication contraire.

Tous les horodatages sont en secondes Unix UTC.


2. Structures de données

2.1 ComposeQub (état créateur en mémoire)

Non sérialisé en CBOR. Non écrit sur le stockage permanent. Local à l’application créateur.

ComposeQub {
    draft_id:       [u8; 16],        // Random, generated locally
    created_at:     i64,             // Unix seconds UTC
    unlock_at:      Option<i64>,     // Unix seconds UTC; None while composing
    visibility:     u8,              // 0x00 = private; 0x01 = public
    content_type:   u8,              // 0x01 text; 0x03 pact; 0x04 verdict
    plaintext:      Vec<u8>,         // Raw body bytes (UTF-8 for text)
    sender_label:   Option<String>,  // Display name; V2-signed when authorship is enabled
    title:          Option<String>,  // Plaintext countdown title; bound via title_hash
    reply_to:       Option<[u8; 32]>,// Parent qub_id; V2-signed when authorship is enabled
    outcome_at:     Option<i64>,     // Optional future judgment time; bound to qub_id
    status:         DraftStatus,     // Composing | Sealed | Uploaded | Failed
}

2.2 QubEnvelope (charge utile déchiffrée)

Sérialisé en CBOR canonique (§3). Chiffré dans le SealedQub. C’est la structure qui prouve l’intégrité du contenu après déchiffrement.

QubEnvelope {
    version:             u8,              // Protocol major version (0x01 for v1)
    qub_id:              [u8; 32],        // Derived (see §4.1)
    content_type:        u8,              // Content type registry (see §6)
    created_at:          i64,             // Unix seconds UTC
    unlock_at:           i64,             // Unix seconds UTC
    outcome_at:          Option<i64>,     // When reality renders judgment; bound to qub_id
    sender_label:        Option<String>,  // Not in qub_id; V2-signed when authorship is enabled
    reply_to:            Option<[u8; 32]>,// Parent qub_id; not in qub_id; V2-signed when present
    body:                Vec<u8>,         // UTF-8 text or canonical CBOR pact/verdict body
    body_hash:           [u8; 32],        // SHA3-256(body) (see §4.2)
    sig_alg:             u8,              // Signature algorithm (see §9.2)
    author_signature:    Option<Vec<u8>>, // Set when sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Set when sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
    cosigner_signature:  Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}

Configuration de référence (qub texte non signé) : version = 0x01, content_type = 0x01, sig_alg = 0x00 ; les champs de signature et de contresignataire sont absents. D’autres champs de métadonnées facultatifs peuvent être présents.

Autres configurations v1 : content_type = 0x03 (corps de pacte, voir §6.1) ; sig_alg = 0x01 (ML-DSA-65) avec author_signature et author_pubkey présents (voir §9.3) ; cosigner_pubkey et cosigner_signature présents ensemble pour les pactes contresignés (voir §9.7) ; reply_to défini sur le qub_id du qub parent pour les fils de réponses (voir §9.3 pour les implications quant à la portée de la signature).

2.3 SealedQub (format de fil canonique)

Sérialisé en CBOR canonique (§3). Il s’agit de l’artefact filaire interne : la livraison publique stocke ces octets nus, tandis que la livraison privée les enveloppe dans OuterWrapper avant stockage (§13).

SealedQub {
    version:           u8,              // Protocol major version (0x01 for v1)
    qub_id:            [u8; 32],        // Same as QubEnvelope.qub_id
    visibility:        u8,              // 0x00 = private/wrapped; 0x01 = public/bare
    unlock_at:         i64,             // Unix seconds UTC
    outcome_at:        Option<i64>,     // Surfaced on the verdict-watch CTA
                                        //   before reveal; mirrors QubEnvelope.outcome_at;
                                        //   bound to qub_id via the §4.1 preimage.
    drand_chain_id:    String,          // drand chain hash (hex string)
    drand_round:       u64,             // Target drand round number
    drand_chain_version: Option<u8>,    // W3 — chain-migration version. Absent / 0 = quicknet
                                        //   (the only chain today). Lets a future chain swap
                                        //   be expressed on the wire without a breaking format
                                        //   change. NOT part of the §4.1 qub_id preimage, so its
                                        //   addition never alters an existing qub's identity.
    tlock_ciphertext:  Vec<u8>,         // tlock-encrypted QubEnvelope CBOR bytes
    recipient_pubkey:  Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
                                        //   but not interpreted by the v1 reference viewer
    title:             Option<String>,  // Plaintext title surfaced on the viewer
                                        //   countdown before reveal. Bound to qub_id
                                        //   via title_hash (§4.1). 1..=100 NFC code
                                        //   points, no hostile/control code points.
}

2.4 RevealedQub (état de l’application lecteur)

Non sérialisé en CBOR. Local à l’application lecteur. Construit après déchiffrement et vérification réussis.

RevealedQub {
    qub_id:              [u8; 32],
    arweave_tx_id:       String,
    visibility:          u8,
    content_type:        u8,
    created_at:          i64,
    unlock_at:           i64,
    outcome_at:          Option<i64>,       // Carried from both wire layers; drives the verdict-watch block
    drand_chain_id:      String,
    drand_round:         u64,
    sender_label:        Option<String>,
    title:               Option<String>,    // Carried forward from SealedQub.title
    reply_to:            Option<[u8; 32]>,
    body:                Vec<u8>,
    body_hash:           [u8; 32],
    body_hash_verified:  bool,
    author_signature:    Option<Vec<u8>>,
    author_pubkey:       Option<Vec<u8>>,
    signature_verified:  Option<bool>,
    cosigner_pubkey:     Option<Vec<u8>>,
    cosigner_signature:  Option<Vec<u8>>,
    cosigner_verified:   Option<bool>,
}

3. Profil CBOR canonique

Toute sérialisation de SealedQub et QubEnvelope DOIT respecter ce profil. Deux implémentations à qui l’on fournit la même structure logique DOIVENT produire des octets identiques.

3.1 Règles d’encodage

Règle Spécification
Norme RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Ordre des clés de map Trié par longueur d’octets encodés d’abord (les plus courts avant les plus longs), puis lexicographiquement (octet par octet pour les clés de même longueur)
Encodage des entiers Forme la plus courte : 0–23 dans l’octet initial ; 24–255 sur 2 octets ; 256–65535 sur 3 octets ; etc.
Encodage des longueurs Longueurs définies uniquement. Pas de tableaux, maps, chaînes d’octets ou de texte de longueur indéfinie (additional info = 31 interdit).
Tags Pas de tags CBOR (le major type 6 est interdit).
Virgule flottante Pas de flottants (les valeurs major type 7 0xF9–0xFB sont interdites).
Chaînes de texte Encodées en UTF-8, normalisées NFC (Unicode Normalization Form C).
Chaînes d’octets Octets bruts. Pas d’encodage base64 au niveau CBOR.
Clés en double Rejet avec erreur. Les analyseurs NE DOIVENT PAS accepter silencieusement des clés en double.
Clés inconnues Rejet avec erreur. Les analyseurs NE DOIVENT PAS tolérer de clés de map hors de l’ensemble de clés canonique du type — deux chaînes d’octets canoniques distinctes ne doivent jamais se décoder vers la même valeur (encode(decode(x)) == x), et pour les charges utiles signées, une clé supplémentaire serait du contenu caché sur lequel les deux signatures s’engagent. L’évolution du schéma passe par version, jamais par des clés supplémentaires.
Valeurs simples Seules true (0xF5), false (0xF4) et null (0xF6) sont autorisées.
Champs optionnels Les champs optionnels absents sont omis entièrement de la map CBOR (non encodés en null). Les champs optionnels présents sont inclus dans l’ordre de tri des clés.

3.2 Ordres de clés canoniques vérifiés

Ces ordres de clés sont normatifs. Les implémentations DOIVENT émettre les clés exactement dans cet ordre. Les assertions de débogage DEVRAIENT vérifier l’ordre dans les builds non-release.

QubEnvelope (version 0x01, non signé, tous les champs optionnels absents) :

"body"                (5 encoded bytes)
"qub_id"              (7 encoded bytes)
"sig_alg"             (8 encoded bytes)
"version"             (8 encoded bytes)
"reply_to"            (9 encoded bytes)   ← only if present (reply chains)
"body_hash"           (10 encoded bytes)
"unlock_at"           (10 encoded bytes)
"created_at"          (11 encoded bytes)
"outcome_at"          (11 encoded bytes)  ← only if present (verdict mechanic)
"content_type"        (13 encoded bytes)
"sender_label"        (13 encoded bytes)  ← only if present
"author_pubkey"       (14 encoded bytes)  ← only if present
"cosigner_pubkey"     (16 encoded bytes)  ← only if present (pact cosign)
"author_signature"    (17 encoded bytes)  ← only if present
"cosigner_signature"  (19 encoded bytes)  ← only if present (pact cosign)

Dérivation de l’ordre des clés QubEnvelope : chaque clé est une chaîne de texte CBOR. Longueur encodée = 1 octet d’en-tête + longueur de la chaîne (pour les chaînes inférieures à 24 octets). Trier d’abord par longueur encodée totale, puis lexicographiquement pour les clés de même longueur.

SealedQub (version 0x01, public, sans destinataire) :

"title"             (6 encoded bytes)   ← only if present
"qub_id"            (7 encoded bytes)
"version"           (8 encoded bytes)
"unlock_at"         (10 encoded bytes)
"outcome_at"        (11 encoded bytes)  ← only if present (verdict mechanic)
"visibility"        (11 encoded bytes)
"drand_round"       (12 encoded bytes)
"drand_chain_id"    (15 encoded bytes)
"recipient_pubkey"  (17 encoded bytes)  ← only if present
"tlock_ciphertext"  (17 encoded bytes)
"drand_chain_version" (20 encoded bytes) ← only if present (W3; absent = quicknet)

PactTerms (corps de pacte, 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 (ligne du tableau terms) :

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

PartyIdentifier (map party_a / party_b) :

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

3.3 Référence d’encodage des octets

Type Encodage CBOR Exemple
Hachage SHA3-256 (32 octets) 0x58 0x20 + 32 octets body_hash, qub_id
Horodatages (i64) Major type 0 (positif) ou 1 (négatif), encodage le plus court secondes Unix
Version (u8, valeur 1) 0x01 (un seul octet)
Type de contenu (u8, valeur 1) 0x01 (un seul octet)
sig_alg (u8, valeur 0) 0x00 (un seul octet)
Signature ML-DSA-65 (3 309 octets) 0x59 0x0C 0xED + 3 309 octets author_signature, cosigner_signature
Clé publique ML-DSA-65 (1 952 octets) 0x59 0x07 0xA0 + 1 952 octets author_pubkey, cosigner_pubkey

4. Dérivations normatives

4.1 qub_id

Le qub_id identifie de manière unique un qub et lie le QubEnvelope au SealedQub. Il est dérivé de manière déterministe du contenu de l’enveloppe.

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

Encodage du séparateur de domaine : la chaîne "QUB_ID_V2" fait 9 octets ASCII. Un seul octet de remplissage 0x00 est ajouté pour atteindre 10 octets pour l’alignement. Les implémentations DOIVENT utiliser exactement ces 10 octets : [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Encodage d’outcome_at : une révision de l’implémentation antérieure à la release a étendu la pré-image de 92 à 100 octets afin d’intégrer le champ optionnel outcome_at dans la liaison. Un outcome_at absent est encodé en 8 octets nuls ; les validateurs du protocole rejettent outcome_at <= 0 partout, de sorte que cette sentinelle ne peut pas entrer en collision avec une valeur légitime. Voir §3.2 (format de fil) et le document interne tasks/verdict-uplift-plan.md pour la mécanique de verdict qui motive ce champ.

Encodage de drand_round : une révision ultérieure de l’implémentation antérieure à la release a étendu la pré-image de 100 à 108 octets afin d’intégrer drand_round (le tour drand cible, §4.3) dans la liaison, et a fait passer le séparateur de domaine à QUB_ID_V2. Cela lie le tour de verrou temporel à l’identité du qub : une passerelle ne peut pas relier le texte chiffré à un tour différent (par exemple déjà passé) de celui qu’implique l’unlock_at affiché. La procédure de déverrouillage (§8) vérifie en outre que le tour intégré à la strophe du texte chiffré tlock correspond à unlock_round(unlock_at), de sorte que l’heure de déverrouillage affichée est, de façon prouvable, le tour qui conditionne le déchiffrement.

Propriétés :

4.2 body_hash

body_hash = SHA3-256(body)

Où body est la charge utile brute Vec<u8>. Pour les qubs texte, c’est le corps du qub encodé en UTF-8.

4.2.1 title_hash

title_hash = SHA3-256(NFC(title).utf8_bytes)   if title is present
title_hash = [0u8; 32]                         if title is absent

Où title est le titre optionnel en clair affiché sur le compte à rebours du lecteur avant le dévoilement (voir §3.2). La normalisation NFC se fait au moment du hachage afin que le condensat soit stable entre des séquences de codepoints visuellement équivalentes. La sentinelle entièrement à zéro est réservée au cas absent ; une chaîne vide est rejetée à la frontière du CBOR canonique comme un encodage non canonique de « absent » (l’encodage canonique omet entièrement le champ).

4.3 Correspondance déverrouillage-tour

drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
Paramètre Source Exemple
unlock_at Secondes Unix UTC choisies par l’utilisateur 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time Infos de chaîne drand (genesis_time) 1595431050
chain_period_seconds Infos de chaîne drand (period) 30

Il s’agit de la correspondance tlock de référence, celle de CurrentRound de drand. drand publie le tour N à chain_genesis_time + (N - 1) * chain_period_seconds. La formule sélectionne donc le tour actuel à unlock_at — le tour dont la signature est la première qu’un lecteur arrivant à unlock_at peut utiliser.

Propriété d’alignement — le cas important en pratique : lorsque (unlock_at - chain_genesis_time) est exactement divisible par chain_period_seconds, la signature du tour sélectionné est publiée exactement à unlock_at, jamais avant. Cela est toujours vrai pour le déploiement de référence : l’heure de genèse de quicknet (1692803367) est divisible par sa période de 3 secondes, et les applications de référence calent les heures de déverrouillage sur des minutes entières. Pour un unlock_at non aligné, la signature du tour sélectionné est publiée strictement moins d’une période avant unlock_at — la précision temporelle de l’engagement est d’une période de balise.

Ancienne correspondance antérieure à la release et tolérance au déverrouillage : la correspondance d’origine était ceil((unlock_at - chain_genesis_time) / chain_period_seconds). Dans le cas aligné ci-dessus, elle sélectionnait le tour publié une période entière avant unlock_at, rendant le texte chiffré déchiffrable exactement une période trop tôt. Les deux correspondances diffèrent exactement de +1 lorsque la différence est divisible par la période, et coïncident sinon. Comme drand_round est intégré à la pré-image immuable de qub_id (§4.1), les artefacts scellés avec l’ancienne correspondance ne peuvent pas être dérivés à nouveau ; les vérificateurs qui effectuent le contrôle croisé du tour à l’étape 6a du §8 DOIVENT donc accepter un drand_round stocké égal soit au tour dérivé, soit au tour dérivé moins un, et DOIVENT exiger que le tour de la strophe tlock soit exactement égal au tour stocké. La tolérance avance au plus d’une période la première signature de conditionnement. Le service de préparation des pactes applique la même tolérance lorsqu’il dérive à nouveau le qub_id d’un pacte préparé : si le tour de la correspondance actuelle ne reproduit pas le qub_id engagé et que la différence est divisible par la période, il réessaie avec le tour moins un et scelle le pacte finalisé sur le tour auquel le qub_id est effectivement lié — jamais aveuglément sur le tour recalculé, ce qui rendrait l’artefact définitivement impossible à dériver.

Validation : unlock_at DOIT être dans le futur au moment du scellement. unlock_at NE DOIT PAS dépasser created_at + 10 ans (pour limiter le risque de dépendance drand à long horizon ; l’interface DEVRAIT avertir pour les dévoilements au-delà de 2 ans).


5. Newtypes du format de fil

Les newtypes du format de fil offrent une sécurité à la compilation contre la confusion des octets CBOR avec du JSON, du texte brut ou d’autres encodages d’octets.

Type Contient Produit par Consommé par
SealedQubCbor CBOR canonique du SealedQub serialize_sealed_qub() Artefact filaire interne ; stocké nu pour la livraison publique ou enveloppé pour la livraison privée, puis récupéré par le lecteur
QubEnvelopeCbor CBOR canonique du QubEnvelope serialize_qub_envelope() Entrée chiffrement tlock, sortie déchiffrement tlock

5.1 Règles de construction

// 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 Validation à la construction

from_encoded() DEVRAIT valider que l’entrée commence par un en-tête de map CBOR valide. La validation structurelle complète a lieu au moment de l’analyse, pas à la construction, pour éviter une double analyse.


6. Registre des types de contenu

Valeur Type Taille de corps maximale Notes
0x00 Réservé (invalide) — NE DOIT PAS être utilisé
0x01 Texte brut (UTF-8, Markdown restreint) 50 Ko payant / 10 Ko gratuit Voir §10 pour les règles de rendu. Le partage gratuit/payant est appliqué par le service d’envoi ; le plafond strict de la couche protocole est de 50 Ko.
0x02 Réservé (futur) — Alloué pour un futur type de contenu ; non valide en v1. Les lecteurs DOIVENT le rejeter conformément à la règle ci-dessous.
0x03 Pacte (accord bilatéral, corps CBOR) 100 Ko Le corps est un PactTerms CBOR canonique (§6.1). Signature du contre-signataire selon §9.7.
0x04 Verdict (auto-évaluation du créateur, corps CBOR) 8 Ko Le corps est un VerdictBody CBOR canonique (§6.2). Émis uniquement par l’intent verdict côté système. La relation au parent est portée par l’étiquette Arweave Parent-Tx-Id, et non par le corps. Voir verdict-uplift-plan §3.4.

Les lecteurs DOIVENT rejeter les types de contenu inconnus avec une erreur claire visible par l’utilisateur. Les lecteurs NE DOIVENT PAS tenter d’afficher des types inconnus en tant que texte.

6.1 Corps de pacte (content_type = 0x03)

Un corps de pacte est l’encodage CBOR canonique d’une valeur PactTerms :

PactTerms {
    pact_version:  u8,                    // 0x01 for structured/v1
    title:         String,                // ≤ 200 bytes, NFC
    terms:         Vec<PactTerm>,         // ≤ 20 rows
    party_a:       PartyIdentifier,       // initiator
    party_b:       PartyIdentifier,       // counter-signer
    notes:         Option<String>,        // ≤ 5,000 bytes, NFC; absent key if none
}

PactTerm       { key: String (≤ 100 bytes), value: String (≤ 2,000 bytes) } // NFC
PartyIdentifier{ label: String (≤ 100 bytes), contact: Option<String (≤ 320 bytes)> }

Les ordres de clés CBOR canoniques pour les trois maps sont donnés en §3.2. Le CBOR de pacte sérialisé total NE DOIT PAS dépasser 100 Ko (correspond à §6).

Discriminateur de schéma. La première ligne de terms pour un pacte structured/v1 DOIT être { key: "pact_schema", value: "structured/v1" }. Les lignes sans ce marqueur sont des pactes « personnalisés » et ne reçoivent ni validation structurée ni rendu sensible au schéma.

Emplacements d’accusé de réception figés. Les pactes structured/v1 portent exactement quatre lignes d’accusé de réception sous ces clés :

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

La value de chacune est l’une des huit chaînes anglaises figées choisies par la paire (role, kind), où role ∈ { seller, buyer, provider, client } et kind ∈ { standard, capacity }. Ces chaînes sont elles-mêmes des données de protocole normatives — les signatures ML-DSA-65 des deux parties s’engagent sur les octets exacts via body_hash. Elles NE SONT PAS localisées ; le corps signé est neutre vis-à-vis de la langue. Toute modification de formulation requiert une nouvelle version de schéma (structured/v2).

Les huit chaînes, leur lookup (acknowledgement_for(role, kind)) et le motif de chacune sont fixés par l’implémentation de référence. Les implémentations conformes DOIVENT émettre des valeurs d’accusé de réception identiques au niveau octet ; les tests de fixtures fixées sur le SHA3-256 du body_hash couvrant les quatre combinaisons de rôle attrapent toute dérive.

Ordre d’affichage dans le lecteur. Les chaînes d’accusé de réception contiennent des phrases telles que « described above » (« décrit ci-dessus »), qui supposent que les lignes de description / périmètre s’affichent avant les accusés de réception. Les lecteurs DOIVENT afficher le tableau terms dans l’ordre CBOR ; un réordonnancement casse la sémantique de la prose.

Contact de la contrepartie. Lorsque le contact de Party B est une adresse e-mail valide, le service d’envoi qub envoie automatiquement un e-mail d’invitation à examen / contre-signature au moment de l’émission et lie la contre-signature ultérieure à la vérification de cette même adresse (§9.7). Les pactes dont le contact de Party B est absent peuvent toujours être contresignés, mais uniquement par un canal hors-bande — le service refuse les requêtes de contre-signature qui ne peuvent pas produire un marqueur de vérification d’e-mail correspondant de 15 minutes.

6.2 Corps de verdict (content_type = 0x04)

Un corps de verdict est l’encodage CBOR canonique d’une valeur 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
}

Ordre canonique des clés 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)

Le CBOR de verdict sérialisé total NE DOIT PAS dépasser 8 Ko (correspond à la ligne du registre ci-dessus).

Énumération du résultat. L’octet sur le fil est neutre vis-à-vis de l’intent ; les quatre catégories Right / Partial / Wrong / Unfalsifiable couvrent l’espace des résultats de toute intent porteuse de verdict. Les libellés par intent (« Vu juste » / « Tenu » / « Livré » / « Confirmée » pour Right, etc.) sont une préoccupation de rendu côté lecteur, résolue par rapport à l’intent du qub parent — le fil reste neutre en langue et en intent. Les valeurs hors de 1..=4 DOIVENT être rejetées au décodage.

Liaison au parent. Un qub de verdict ne porte PAS la référence au parent dans son corps. L’identifiant de transaction Arweave du qub parent est émis comme étiquette de stockage Parent-Tx-Id au moment de l’envoi (§7, couche d’étiquettes de stockage). Cela garde le corps comme une déclaration signée autonome d’auto-évaluation ; la chaîne d’audit (« avoir raison à propos de quoi ? ») est établie par la recherche d’étiquettes Arweave.

Sécurité de l’URL de preuve (normatif). Lorsque evidence_url est présente, les validateurs (côté composition, côté fil, à la périphérie du Worker) DOIVENT appliquer :

  1. HTTPS uniquement. La chaîne DOIT commencer par la séquence d’octets https://. Tout autre schéma — http, ftp, javascript, data, file, etc. — est rejeté.
  2. Plafond de longueur. ≤ 2 048 octets (limite pratique des URL dans le navigateur).
  3. NFC + contrôle des codepoints hostiles. Même règle que title et reflection — les codepoints de bidi-override / largeur nulle / bloc tag / BOM / C0 / C1 sont rejetés. La définition correspond à la fonction Rust crate::handle::contains_hostile_text_codepoint et à la fonction TS workers/api/src/utils/unicode.ts::isHostileCodepoint (à garder en synchronisation).
  4. Aucun blanc, aucun caractère de contrôle ASCII. Tout blanc / DEL / octet sous 0x20 n’importe où dans l’URL est rejeté — referme le vecteur d’injection \n / \t que la règle bidi ne couvre pas.
  5. Segment d’hôte non vide. Tout ce qui se trouve entre https:// et le premier /, ? ou # DOIT être non vide.

Aucune récupération côté serveur. Le Worker NE DOIT PAS proxifier, récupérer ou prévisualiser l’URL. Le protocole stocke une chaîne ; le rendu se fait côté lecteur avec rel="nofollow noopener noreferrer" target="_blank" et l’hôte affiché visiblement à côté du texte du lien.

Réflexion. Texte de réflexion facultatif rédigé par le créateur (« ce qui a changé, ce que vous avez appris »). Même validation NFC + codepoints hostiles que title. Une saisie vide ou ne contenant que des blancs est repliée à « absente » au moment de la construction.

Version de schéma. La v1 ne prend en charge que verdict_version = 0x01. Les futures révisions de schéma incrémentent cet octet et arrivent en même temps qu’une nouvelle version de protocole selon §12.


7. Protocole de scellement

Séquence complète de scellement. Chaque étape est normative.

 1. User composes plaintext and metadata in ComposeQub.
 2. Validate:
    a. body is non-empty.
    b. body size ≤ max for content_type and user tier (see §6).
    c. unlock_at is in the future.
    d. unlock_at ≤ created_at + 10 years.
    e. content_type is a known, supported value.
    f. visibility is 0x00 (private) or 0x01 (public).
 3. Compute body_hash = SHA3-256(body).
 4. Set created_at = current Unix seconds UTC.
 5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
    compute drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
    (§4.3). (Computed here, before qub_id, because drand_round is bound into the
    qub_id preimage—§4.1.)
 6. Compute qub_id (see §4.1), folding in drand_round from step 5.
 7. Construct QubEnvelope with all fields.
 8. Serialise QubEnvelope using canonical CBOR → bytes B.
    Assert: serialised output matches canonical profile (§3).
 9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C and matching qub_id, version,
    visibility, unlock_at, drand_chain_id, and drand_round.
11. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12. Select the delivery shape from visibility:
    a. Private (0x00): generate K = 32 random bytes and N = 12 random bytes
       using a CSPRNG. Compute W = wrap_sealed_qub(SealedQubCbor,
       qub_id=qub_id, key=K, nonce=N) per §13. Upload payload = W.
    b. Public (0x01): upload payload = bare SealedQubCbor; do not generate K.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit the selected upload payload to the qub upload service. For a private
    browser seal, the service is byte-blind to the inner SealedQubCbor and never
    receives K. The Builder `/api/v1/seal` route is an explicit exception: it
    receives plaintext and caller-supplied K in memory, then persists neither.
16. Receive arweave_tx_id from the service. For private delivery, construct
    `<origin>/c/<arweave_tx_id>#<base64url(K)>` (or the equivalent short-code
    path). For public delivery, omit the fragment. Browsers do not transmit URL
    fragments to servers, so K from the browser-seal path is not observed by
    qub.social or any storage gateway.

Couche de tags de stockage (hors-bande). Le service d’envoi qub attache un ensemble délibérément réduit de tags de transaction de stockage à côté de la charge utile d’envoi sélectionnée. Content-Type=application/octet-stream est requis normativement. Le service de référence attache en plus trois tags optionnels lorsque le créateur choisit de les exposer : Intent (intention de composition validée par allowlist — exactement l’une de announcement, thesis, prediction, letter, secret, commitment, proof ou verdict émis par le système), Author (empreinte de la clé publique §9.3 du créateur sous forme hex minuscule de 64 caractères) et Parent-Tx-Id (ID de transaction de stockage du qub parent pour les fils de réponses, base64url de 43 caractères).

Le tag Author est opt-in par qub : l’application créateur de référence ne l’attache que lorsque l’utilisateur active explicitement l’attribution publique au moment du scellement. Quand l’interrupteur est désactivé — par défaut — aucun tag Author n’est écrit et le qub n’est pas attribué sur la chaîne : rien sur le stockage permanent ne lie l’envoi à l’identifiant, à l’adresse e-mail ou aux autres qubs d’un créateur. Quand l’interrupteur est activé, l’empreinte Author se résout au @handle choisi par le créateur via la chaîne d’attestation §9.5. Les relations de fils de réponses et Intent ne sont pas identifiantes. Pour une remise privée, l’enveloppe externe (§13) chiffre l’artefact interne SealedQub reconnaissable ; récolter les enveloppes stockées et obtenir les signatures drand publiques ne suffit donc toujours pas à récupérer le corps sans K. Les tags de stockage restent délibérément des métadonnées publiques.

Le service de référence n’attache intentionnellement PAS de tags App-Name, App-Version ou Type : tout filtre à valeur unique de ce genre renverrait l’ensemble du corpus qub à une requête GraphQL, ce qui est incompatible avec la portée de confidentialité « corps uniquement » de l’enveloppe.

Un vérificateur conforme NE DOIT PAS dépendre d’un quelconque tag de stockage pour la vérification tierce §11 ; le hachage de corps / qub_id / signature ne s’engagent que sur le CBOR interne, jamais sur l’ensemble des tags.


8. Protocole de déverrouillage

Séquence complète de déverrouillage. Chaque étape est normative.

 1. Viewer opens delivery URL. Extract arweave_tx_id from the path and retain
    the optional URL fragment. Do not assume a missing fragment is an error:
    public/bare delivery intentionally has no K.
 2. Check denylist. If tx_id is denylisted → display block message. Stop.
 3. Fetch the stored bytes (with multi-gateway fallback).
 3a. Resolve the delivery shape structurally:
    a. If the bytes parse as OuterWrapper, require a well-formed 32-byte K in
       the URL fragment, require wrapper version 0x01, and unwrap per §13.
       Any missing/malformed K or AEAD failure is a terminal error.
    b. Otherwise require the bytes to parse as bare SealedQubCbor; no K is
       required. If neither shape parses, report an integrity error.
 4. Parse SealedQubCbor → SealedQub.
 5. Validate: SealedQub.version is known (0x01), visibility is known, and the
    delivery shape matches it (wrapped = 0x00 private; bare = 0x01 public).
    Reject any mismatch or unknown value.
 6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
 6a. Round-binding check. Recompute expected_round from
    SealedQub.unlock_at per §4.3. Reject unless SealedQub.drand_round ==
    expected_round OR SealedQub.drand_round == expected_round - 1 (the
    legacy pre-release mapping—see §4.3), AND the round baked into the tlock
    ciphertext stanza (read via the age/tlock header, no signature required)
    == SealedQub.drand_round exactly. The stanza round is the one that
    actually gates decryption; without this check a malicious creator could
    bind the ciphertext to an already-past round while displaying a future
    countdown, so anyone reading the stored bytes could decrypt before
    unlock_at. Implementations with no chain identity (test mocks) skip this
    check.
 7. Once current time ≥ SealedQub.unlock_at:
    a. Fetch drand round signature for SealedQub.drand_round from drand network.
    b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
 8. Parse B → QubEnvelope.
 9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
    Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
    Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
    Fail → integrity error.
12a. Verify: QubEnvelope.outcome_at == SealedQub.outcome_at (both absent, or
    both present and equal). Fail → integrity error.
12b. Content re-derivation. Recompute qub_id per §4.1 from the decrypted
    fields — (QubEnvelope.version, content_type, created_at, unlock_at,
    outcome_at, SealedQub.drand_round, QubEnvelope.body_hash,
    title_hash(SealedQub.title)) — and verify it equals SealedQub.qub_id.
    Fail → integrity error. The pairwise checks in steps 10-12a only prove
    the two layers agree with EACH OTHER; a forger who rewrites a bound
    field consistently on both surfaces (a pre-reveal title swap, or a
    post-round body swap with a recomputed body_hash re-encrypted to the
    same round under the same qub_id) passes them all. Only re-deriving
    the identity from content closes this.
13. Verify: QubEnvelope.content_type is known and renderable.
    Known values: 0x01 (text), 0x03 (pact), 0x04 (verdict).
    Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using the appropriate renderer (see §10 for text and §6 for pact/verdict).
17. Construct RevealedQub for display.

9. Signature d’auteur

9.1 Motivation

Les qubs sont stockés sur un stockage permanent. Les signatures d’auteur doivent rester infalsifiables indéfiniment, c’est pourquoi v1.0 utilise le schéma post-quantique ML-DSA-65 (FIPS 204) plutôt qu’un schéma classique dont la sécurité pourrait se dégrader pendant la durée de vie permanente du qub.

9.2 Registre d’algorithmes

sig_alg Schéma Taille de clé Taille de signature Statut
0x00 Pas de signature (non signé) — — Actif
0x01 ML-DSA-65 (FIPS 204) 1 952 octets 3 309 octets Actif
0x02 Ed25519 32 octets 64 octets Constante réservée ; non prise en charge dans le protocole v1

Les lecteurs du protocole v1 DOIVENT rejeter toute valeur hors de {0x00, 0x01}, y compris la valeur réservée 0x02. La réservation empêche une réutilisation accidentelle ; elle ne constitue pas une activation. Son activation exige la modification gouvernée décrite au §15.

9.3 Construction de la pré-image signée

Deux versions de pré-image ont existé. Toutes les signatures DOIVENT utiliser V2, et les vérificateurs DOIVENT accepter uniquement V2. L’ancienne pré-image V1 (documentée ci-dessous à titre de référence historique) était acceptée comme repli de vérification uniquement pendant la migration vers V2 ; ce repli a été retiré et une signature V1 uniquement est désormais rejetée.

V2 (actuelle — produite par toute nouvelle signature d’auteur, ainsi que par les deux signatures du flux de préparation / contre-signature de pacte) :

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 suit la même convention de sentinelle d’absence que title_hash (§4.2.1) : 32 octets nuls ne constituent pas une sortie SHA3-256 valide, de sorte que « absent » ne peut jamais entrer en collision avec une étiquette présente. Tous les champs sont de largeur fixe, donc la pré-image est sans ambiguïté sans préfixes de longueur.

V1 (ancienne — RETIRÉE ; n’est plus produite ni acceptée à la vérification) :

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V1"  ||    // domain separator (17 bytes)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 bytes)
    body_hash            ||    // [u8; 32] (32 bytes)
    unlock_at            ||    // i64 big-endian (8 bytes)
    0x00                       // u8 (1 byte): MUST be 0x00 in v1.0
)

// Total preimage: 91 bytes → 32-byte hash

La pré-image V1 omettait sender_label et reply_to. Elle était acceptée comme repli de vérification uniquement pendant la migration vers V2 ; ce repli a depuis été retiré — les vérificateurs DOIVENT accepter uniquement la pré-image V2. La définition est conservée ici à titre de référence historique et pour expliquer le séparateur de domaine ci-dessous. Une signature qui ne se vérifie que contre V1 DOIT être traitée comme un échec de vérification.

Séparateurs de domaine : "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" font 17 octets ASCII chacun ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Pas de remplissage. Le séparateur différent sépare par domaine les deux constructions, de sorte qu’une signature sur une pré-image ne peut jamais se vérifier comme l’autre.

Octet org_id_present : l’octet suivant unlock_at DOIT être 0x00. L’implémentation de référence l’expose sous forme de constante ORG_ID_PRESENT_INDIVIDUAL = 0x00 dans crates/qub-core/src/signing.rs ; les lecteurs reconstruisant sig_input pour vérification DOIVENT émettre le même octet.

Portée de la signature — ce qui est et n’est pas couvert. La sig_input V2 s’engage directement sur version, qub_id, body_hash, unlock_at, sender_label et reply_to (plus le séparateur de domaine fixe et l’octet org_id_present). qub_id est lui-même dérivé de version, content_type, created_at, unlock_at, outcome_at, drand_round et body_hash via la pré-image §4.1, donc tout changement de ces champs produit un qub_id différent et invalide la signature transitivement. La surface authentifiée est donc :

Champ Authentifié par la signature Comment
version ✓ Entrée directe de sig_input
qub_id ✓ Entrée directe
body_hash ✓ Entrée directe
unlock_at ✓ Entrée directe
sender_label ✓ Entrée directe via sender_label_hash (pré-image V2 — la seule forme acceptée)
reply_to ✓ Entrée directe via reply_to_or_zero (pré-image V2 — la seule forme acceptée)
content_type ✓ Transitivement, via la pré-image qub_id
created_at ✓ Transitivement, via la pré-image qub_id
outcome_at ✓ Transitivement, via la pré-image qub_id
drand_round ✓ Transitivement, via la pré-image qub_id
body ✓ Transitivement, via body_hash = SHA3-256(body)
author_pubkey — (implicite) La clé qui a vérifié la signature est l’auteur, par définition
cosigner_pubkey / cosigner_signature — Signés indépendamment sur la même sig_input (voir §9.7)
drand_chain_id, tlock_ciphertext, visibility — Champs du SealedQub externe, pas dans l’enveloppe — couverts par leurs propres invariants structurels (cohérence tour / chaîne) mais pas par la signature de l’auteur. (drand_round est désormais lié transitivement via la pré-image du qub_id — voir ci-dessus.)

Pourquoi V2 est la seule pré-image acceptée.

Les implémentations qui affichent sender_label ou reply_to aux utilisateurs finaux DOIVENT mettre en avant l’identité authentifiée (empreinte de clé publique, attestation) comme signal d’identité principal, pas l’étiquette.

9.4 Procédure de vérification

1. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
   only accepted form — the legacy V1 fallback is retired (§9.3), so a
   signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."

La vérification de signature est l’opération la plus coûteuse (en particulier ML-DSA-65). Elle DEVRAIT être effectuée après que toutes les vérifications moins coûteuses (hachage, qub_id, unlock_at) soient passées.

9.5 Attestations d’identité

Les attestations d’identité — la correspondance entre author_pubkey et des revendications d’identité reconnaissables (identifiant qub, adresse e-mail, identifiant social, identifiant de passkey) — sont une amélioration progressive côté lecteur et ne sont pas requises pour la vérification de signature. Les lecteurs qui résolvent des attestations en identité affichée DOIVENT appliquer la précédence :

handle > email > social > fingerprint

Le repli sur l’empreinte est l’hex minuscule de SHA3-256(author_pubkey) ; il est toujours disponible pour tout qub signé. Les lecteurs PEUVENT l’abréger pour l’affichage — le lecteur de référence rend qub: suivi des quatre premiers et des quatre derniers octets (qub:<8 hex>…<8 hex>).

Un vérificateur conforme peut effectuer toutes les vérifications de §9.4 sans contacter l’API qub, sans aucun réseau au-delà du stockage permanent et de drand, et sans aucune recherche côté serveur. La résolution d’attestation est une étape distincte au mieux, effectuée seulement après le succès de la vérification de signature.

9.6 Impact en taille

Ed25519 ML-DSA-65
Signature 64 octets 3 309 octets
Clé publique 32 octets 1 952 octets
Total par qub 96 octets 5 261 octets
Surcoût de stockage (à ~5 $/Mo) ~0,0005 $ ~0,026 $

Pour un qub texte de 500–2 000 octets, ML-DSA-65 triple à peu près la taille stockée. Le coût absolu est négligeable.

9.7 Vérification du contre-signataire (pactes bilatéraux)

Pour les accords bilatéraux (content_type = 0x03), une seconde couche de signature prouve que les deux parties ont consenti aux mêmes termes.

Champs de l’enveloppe :

Les deux champs DOIVENT être présents ensemble ou tous deux absents. Si exactement un est présent, les lecteurs DOIVENT signaler une erreur d’intégrité.

Procédure de vérification :

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

Propriétés :

Verrou de liaison par e-mail (opérationnel). Lorsqu’un pacte émis porte un contact e-mail Party B (§6.1), le service d’envoi qub DOIT refuser la requête de contre-signature à moins qu’il n’existe un marqueur de vérification d’e-mail à courte durée correspondant à la fois à l’id d’émission et au hachage de l’e-mail normalisé de ce contact. Le marqueur est écrit par /api/v1/auth/verify quand le jeton du lien magique porte un staging_id et que l’adresse vérifiée correspond à SHA-256(normalise_email(party_b.contact)) — où normalise_email(addr) préserve la casse de la partie locale et met en minuscules uniquement la partie domaine (selon RFC 5321 §2.3.11), et SHA-256 ici est le hachage NIST FIPS 180-4 (distinct du SHA3-256 utilisé en §4) — et expire 900 secondes (15 minutes) après émission. C’est un verrou opérationnel anti-usurpation, PAS une partie de la preuve qub on-chain — un vérificateur tiers rejouant §11 n’a besoin que du stockage permanent et de drand, sans aucune recherche côté serveur. Le marqueur n’existe que côté serveur et ne fait jamais partie du corps signé.

Impact en taille (auteur ML-DSA-65 + contre-signataire) :

Composant Taille
Signature de l’auteur 3 309 octets
Clé publique de l’auteur 1 952 octets
Signature du contre-signataire 3 309 octets
Clé publique du contre-signataire 1 952 octets
Surcoût cryptographique total 10 522 octets
Surcoût de stockage ~0,05 $

10. Rendu et assainissement Markdown

Cette section est sensible à la sécurité. Le lecteur affiche les qubs texte (content_type = 0x01) à l’aide d’un sous-ensemble Markdown restreint.

10.1 Éléments autorisés

10.2 Éléments interdits

Élément Traitement
HTML brut (<div>, <script>, etc.) Entièrement retiré. Aucun HTML ne passe.
Images (![alt](url)) Retirées. La syntaxe d’image est supprimée du résultat.
Liens ([text](url)) URL affichée en texte brut visible. Pas d’auto-lien. Pas cliquable sans action explicite de l’utilisateur.
Schémas d’URL dangereux javascript:, data:, vbscript:, file: — retirés.
Iframes, embeds, objects Retirés.
Entités HTML Décodées en caractères d’affichage uniquement si elles sont sûres.

10.3 Implémentation

Les implémentations DOIVENT utiliser un analyseur strict basé sur une allowlist, pas une blocklist. L’approche recommandée :

  1. Analyser le Markdown avec pulldown-cmark (ou équivalent).
  2. Parcourir l’AST et retirer tout nœud absent de l’allowlist (§10.1).
  3. Pour les nœuds liens : émettre l’URL en texte visible, pas en élément <a> cliquable.
  4. Convertir l’AST filtré en représentation intermédiaire typée (par exemple un enum MarkdownNode avec uniquement des variantes sûres). Le HTML brut est structurellement non représentable dans cette IR.
  5. Rendre depuis l’IR typée vers la couche d’affichage cible (par exemple composants de vue réactifs, nœuds DOM). Aucune concaténation de chaînes HTML ni innerHTML à aucun moment.

Les approches blocklist sont fragiles parce que de nouvelles extensions Markdown ou des particularités d’analyseur peuvent introduire des éléments non filtrés. L’approche AST typée rend les XSS structurellement impossibles — il n’y a aucune variante capable de transporter du HTML arbitraire.

10.4 Limites de taille et de structure


11. Vérification tierce

Tout tiers qui détient les octets stockés — et K dans le cas d’un qub privé/enveloppé — peut vérifier l’artefact cryptographique sans la coopération de qub. Une affirmation d’existence horodatée indépendamment exige en outre soit l’inclusion vérifiée du qub dans le stockage permanent, soit une preuve vérifiée du journal de transparence du §16.

1. Obtain the stored bytes. For a private delivery, also obtain K from the
   delivery-link fragment.
2. Resolve the delivery shape (§8 step 3a): unwrap OuterWrapper with K, or
   accept bare SealedQubCbor. Require shape/visibility agreement.
3. Parse SealedQubCbor → SealedQub; validate protocol version, visibility,
   content type, chain identity, and structural bounds.
4. Recompute expected_round from unlock_at (§4.3); require the stored round
   (allowing the documented legacy minus-one case) and ciphertext-stanza round
   to agree exactly as §8 step 6a specifies.
5. Obtain the drand signature for SealedQub.drand_round and verify its BLS
   signature against the pinned chain public key.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify envelope/sealed equality for qub_id, unlock_at, and outcome_at.
10. Recompute qub_id from the decrypted fields, sealed drand_round, and title;
    require it to equal the carried qub_id (§4.1).
11. If sig_alg != 0x00, verify the V2 author signature (§9.4). If cosigner
    fields are present, verify their pairing, key separation, and signature
    (§9.7).
12. For an existence-time claim, independently verify either:
    a. the permanent-storage transaction's data-to-id binding, owner, block
       inclusion, and block timestamp; or
    b. a §16 inclusion proof through its pinned anchor and anchor block time.
13. Report each verified claim separately; do not collapse an absent storage
    or anchor proof into a successful timing verdict.

Ce que la vérification prouve :

Entrée de preuve Ce qu’elle établit
Bundle ou artefact scellé valide + signature drand Le corps récupéré correspond à body_hash ; les métadonnées liées dans qub_id sont intactes ; le texte chiffré est lié au tour drand déclaré ; et ce tour est écoulé. Cela n’établit pas quand le texte chiffré a été créé.
Signature V2 valide de l’auteur ou du contresignataire Le ou les détenteurs des clés secrètes correspondantes ont authentifié la surface signée du §9.3.
Transaction de stockage vérifiée indépendamment pour ce qub Le texte chiffré stocké exact existait au plus tard à l’horodatage de son bloc.
Preuve valide du journal de transparence ancré L’affirmation propre au type de feuille du §16.11, y compris une borne supérieure pour l’heure d’engagement tirée du bloc d’ancrage.

Ce que la vérification NE prouve PAS :

Non-preuve Pourquoi
Authorship Le sender_label est décoratif. Sans sig_alg ≥ 0x01, n’importe qui aurait pu sceller ce contenu.
Intention L’artefact prouve des octets et des relations cryptographiques, pas ce que le créateur entendait subjectivement.
Engagement préexistant à partir du seul .qub Un créateur peut assembler un bundle valide après l’écoulement du tour lié. La signature drand intégrée prouve que le tour est écoulé, pas que le texte chiffré existait auparavant.
Heure exacte du bouton de scellement L’horodatage d’un bloc de stockage ou d’ancrage est une borne supérieure vérifiable indépendamment et peut être postérieur à l’action locale de l’utilisateur. Les affirmations sealed_at / received_at ne constituent pas une preuve.

Le journal de transparence implémenté (§16) étend la vérification à plusieurs qubs en ajoutant un ordre infalsifiable et une borne supérieure de l’heure d’engagement sans confiance — l’heure du bloc d’ancrage — cadrés par le type de feuille (§16.11). Il n’ajoute ni paternité ni intention ; pour le chemin de téléversement aveugle aux octets par défaut, il ne prouve pas lui-même body_hash ou drand_round, qui continuent de provenir des contrôles de l’artefact.


12. Versionnement et contrôle des releases

Les releases du document, le protocole de fil interne et l’enveloppe externe forment des espaces de version distincts. Ainsi, une clarification documentaire seule ne modifie pas silencieusement les octets, et une future migration du fil ne peut pas se faire passer pour une révision éditoriale.

12.1 Version de release du document

Cette spécification utilise des releases sémantiques du document (MAJOR.MINOR.PATCH) et un tag Git immuable nommé protocol-v<release>.

Le statut d’une release est Brouillon — pas encore normative —, Actuel — la seule cible d’implémentation recommandée — ou Remplacé — conservé pour la vérification historique. La route non versionnée /protocol affiche la release Actuelle ; le tag de release préserve sa source exacte et chaque locale publiée avec elle. Tout changement de statut ou de numéro de release exige la mise à jour de ce tableau et de l’historique de release dans la même modification revue.

Release du document Date d’effet Statut Protocole de fil Enveloppe Source
1.0.0 2026-09-23 Actuel 0x01 0x01 protocol-v1.0.0

12.2 Version du protocole

Le champ version (u8) dans SealedQub et QubEnvelope identifie la version majeure du protocole.

12.3 Historique des versions du protocole

Version Valeur Description
v1 0x01 Remise privée/enveloppée et publique/non enveloppée ; corps texte (0x01), pacte (0x03) et verdict (0x04) ; signature V2 de l’auteur/du contresignataire avec ML-DSA-65 ; tlock sur drand quicknet ; SHA3-256.

12.4 Compatibilité ascendante

Un lecteur v1 rencontrant un QubEnvelope avec des clés de map CBOR inconnues (clés absentes de l’ordre canonique §3.2) DOIT le rejeter avec une erreur de décodage (§3.1). La compatibilité ascendante repose sur le champ version, pas sur la tolérance de clés : les additions futures — même des métadonnées mineures — sont livrées sous une nouvelle valeur de version, qu’un lecteur v1 rejette avec une erreur claire « protocole plus récent » plutôt que d’écarter silencieusement du contenu sur lequel les signatures s’engagent.

Un lecteur v1 rencontrant sig_alg = 0x01 (ML-DSA-65) mais sans support de vérification ML-DSA-65 DEVRAIT afficher le contenu du qub avec un avis « signature présente mais non vérifiable », pas rejeter le qub entièrement. L’implémentation de référence rejette aujourd’hui toute valeur sig_alg autre que 0x00 et 0x01 parce que le registre v1 ne contient aucun autre algorithme valide — rejet strict et soft-fail sont observationnellement identiques jusqu’à ce qu’un troisième algorithme soit enregistré. Le comportement soft-fail ci-dessus devient porteur dès que §9.2 admet une nouvelle entrée, et le lecteur de référence sera mis à jour pour faire un soft-fail à ce moment-là.

12.5 Version de l’enveloppe externe

L’OuterWrapper décrit en §13 porte son propre octet version, indépendant de SealedQub.version et QubEnvelope.version. Les deux espaces de version évoluent séparément : un futur remplacement symétrique post-quantique sûr fait évoluer l’octet du wrapper sans toucher à la version interne du protocole, et une future addition à la couche protocole (par exemple, un nouveau champ d’enveloppe) fait évoluer la version interne sans toucher à l’octet du wrapper.

OUTER_WRAPPER_VERSION_* Valeur Algorithme Statut
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM avec nonce de 12 octets, tag d’authentification de 16 octets, AAD lié à qub_id Actif pour la remise privée
— 0x02–0xFF Réservé Futur

Les lecteurs DOIVENT rejeter les versions de wrapper inconnues avec une erreur claire. Le protocole garde intentionnellement un espace de version de wrapper étroit jusqu’à ce qu’un moteur de migration concret apparaisse (par exemple, des recommandations NIST en faveur d’un AEAD différent) ; un emplacement 0x02 sera attribué dans la même révision qui introduit l’algorithme.


13. Enveloppe de chiffrement externe

13.1 Motivation

Les couches du protocole (QubEnvelope → tlock → SealedQub) rendent un qub scellé verrouillé dans le temps : le corps est illisible jusqu’à ce que unlock_at et la signature de tour drand aient été publiés. Après le déverrouillage, cependant, la signature de tour est publique et la forme CBOR canonique de SealedQub est reconnaissable, donc un agrégateur ayant indexé les transactions de stockage permanent pourrait déchiffrer en masse l’ensemble du corpus qub.

Pour la remise privée, l’enveloppe de chiffrement externe ferme ce canal en interposant une couche AEAD symétrique additionnelle entre le SealedQubCbor canonique et les octets stockés. Dans le chemin de scellement du navigateur, la clé 256 bits K ne vit que dans le fragment de l’URL de remise et sur les appareils des utilisateurs ; les navigateurs ne transmettent pas les fragments d’URL aux serveurs, donc qub.social, toute passerelle de stockage et tout CDN devant l’un ou l’autre sont observationnellement aveugles à K. La représentation stockée d’un qub privé est donc un texte chiffré opaque dont le texte clair est irrécupérable sans l’URL que le créateur a choisi de partager. La remise publique omet délibérément cette couche (§13.8).

Effet net :

13.2 Empilement

plaintext body                       ← QubEnvelope.body (§2.2)
  ↓ canonical CBOR (§3)
envelope CBOR
  ↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
  ↓ canonical CBOR (§3)
SealedQubCbor bytes                  ← inner wire artifact
  ├─ public (visibility=0x01) ───────────────▶ stored directly (§13.8)
  └─ private (visibility=0x00)
       ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12, this section)
     OuterWrapper CBOR bytes         ← stored private payload

Le scellement et le déverrouillage à la couche protocole (§7, §8) sont inchangés sous la frontière du wrapper ; le wrapper s’attache au site d’appel de seal() et se détache au site d’appel de unlock().

13.3 Structure de données OuterWrapper

struct OuterWrapper {
    version:    u8,           // 0x01, see §12.5
    qub_id:     [u8; 32],     // copied from inner SealedQub; AEAD AAD
    nonce:      [u8; 12],     // 96-bit AEAD nonce
    ciphertext: Vec<u8>,      // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}

Invariants des champs.

Encodage CBOR. CBOR canonique selon §3, avec la même règle d’ordre des clés (triées par longueur d’octets encodés croissante, puis lexicographiquement). Les quatre clés sont :

Clé Octets encodés Ordre
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

Le premier octet du CBOR de l’OuterWrapper est donc l’en-tête de map à longueur définie pour une map à 4 entrées (0xA4).

13.4 Liaison AAD à qub_id

Le wrapper lie qub_id comme données authentifiées additionnelles AEAD. C’est la défense structurelle porteuse contre trois classes d’attaques :

Attaque Défense
Déplacer le chiffré sous un autre champ qub_id dans le wrapper Mismatch AAD → l’authentification AEAD échoue
Mélanger le fragment d’URL du qub A avec les octets stockés du qub B Mauvaise clé (et AAD lié indépendamment) → l’authentification AEAD échoue
Falsifier le champ qub_id du wrapper après envoi Mismatch AAD → l’authentification AEAD échoue

Porter qub_id dans le texte clair du wrapper n’affaiblit pas l’immunité à l’énumération de manière significative — qub_id est lui-même un hachage SHA3-256 de la pré-image §4.1 sans pré-image récupérable depuis le condensat, et un énumérateur qui a déjà récolté les octets du wrapper n’apprend rien du qub_id visible qu’il ne pourrait inférer de l’existence de l’envoi lui-même.

13.5 Algorithmes de wrap et d’unwrap

wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
    require K.len() == 32 and N.len() == 12 and Q.len() == 32
    I := canonical_cbor_decode(S) as SealedQub
    require I.qub_id == Q               // reject mismatched caller AAD
    C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
    // C includes the 16-byte authentication tag at the end
    return canonical_cbor_encode(OuterWrapper{
        version:    0x01,
        qub_id:     Q,
        nonce:      N,
        ciphertext: C,
    })

unwrap_sealed_qub(OuterWrapper bytes W, key K):
    require K.len() == 32
    O := canonical_cbor_decode(W) as OuterWrapper
    require O.version == 0x01           // §12.5
    P := AES_256_GCM_decrypt(
            key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
         )
    // any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
    S := canonical_cbor_decode(P) as SealedQub
    require S.qub_id == O.qub_id       // explicit inner/outer cross-check
    return P                            // P is the validated SealedQubCbor

Effondrement des modes d’échec. Une mauvaise K, un mauvais nonce, un mismatch AAD et un chiffré falsifié produisent tous la même erreur DECRYPT_FAILED. C’est une propriété AEAD délibérée : distinguer le mode d’échec créerait un canal latéral qu’un attaquant distant pourrait sonder en envoyant des wrappers malformés et en chronométrant la réponse. Les implémentations de référence DOIVENT effondrer tous les échecs AEAD sur une forme d’erreur unique.

13.6 Matériel de clé et distribution

La clé d’enveloppement K est une valeur aléatoire uniforme de 256 bits générée par qub par un CSPRNG. Les implémentations de référence la sourcent depuis :

Distribution : K DOIT être encodée en base64 URL-safe (RFC 4648 §5, sans padding) et ajoutée à l’URL de remise comme composant fragment :

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

Le fragment n’est jamais transmis à un serveur par un navigateur conforme. Les canaux de récupération (index d’historique côté serveur, envoi automatique d’e-mail opt-in) qui persistent l’URL de remise complète — fragment compris — au-delà de l’appareil de l’utilisateur sont un compromis explicite contre la posture par défaut de crypto-déchirure et DOIVENT être verrouillés par un consentement utilisateur explicite.

Perte du fragment. Si un utilisateur perd le fragment d’URL et n’a pas de canal de récupération, le qub est illisible. C’est le compromis porteur de la conception et DOIT être divulgué à l’utilisateur au moment du scellement. Le MVP renforce la mise en garde au scellement avec une copie explicite « enregistrez cette URL » et un canal de récupération par e-mail vérifié pour les utilisateurs qui s’y inscrivent.

13.7 Hors de la portée de cette section

13.8 Les qubs publics (omission du wrapper)

Le wrapper externe est facultatif à la couche de remise. Un créateur peut sceller un qub comme public, auquel cas le SealedQubCbor canonique entre directement dans le pipeline de stockage, sans couche OuterWrapper ni clé K :

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

Un qub public est verrouillé dans le temps mais non protégé par lien : il reste illisible jusqu’à la publication de son tour drand (la couche tlock est inchangée), mais après le déverrouillage, quiconque dispose de l’identifiant de transaction de stockage peut le déchiffrer — aucun fragment d’URL n’est requis, car il n’y a pas de K. C’est le compromis délibéré pour les surfaces que le serveur doit piloter : les e-mails de notification de dévoilement, les liens oEmbed/intégration automatique sans fragment et un référencement post-dévoilement plus riche ont tous besoin d’un lien qui fonctionne sans un secret que le serveur ne détient jamais (§13.6). Un qub privé peut toujours utiliser la forme explicite <qub-embed src="full_delivery_url"> lorsque l’éditeur fournit sa capacité complète contenant le fragment.

Conséquences qu’un producteur DOIT prendre en compte :

Le privé (enveloppé) reste la valeur par défaut ; le public est un choix explicite du créateur, par qub.


14. Vecteurs de test

14.1 Dérivation de qub_id

Input:
  version      = 0x01
  content_type = 0x01
  created_at   = 1735689600 (2025-01-01 00:00:00 UTC)
  unlock_at    = 1736294400 (2025-01-08 00:00:00 UTC)
  outcome_at   = absent
  drand_round  = 4695446  (= floor((1736294400 - 1595431050) / 30) + 1, §4.3 mapping, drand mainnet params §14.2)
  body         = "Hello, future."  (UTF-8, 14 bytes)
  title        = absent

Intermediate:
  body_hash  = SHA3-256("Hello, future.")
             = 76ab8b3f843c6ed4f2d0fd75b9f457b4
               ad49dd4450f9c22723ae430e3af3211d
  title_hash = [0u8; 32]   (title absent — §4.2.1 sentinel)

Domain separator (10 bytes):
  [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]

Preimage (108 bytes—current protocol v1):
  domain_separator    ||  // 10 bytes
  0x01                ||  // version
  0x01                ||  // content_type
  0x0000000067748580  ||  // created_at as i64 big-endian (1735689600)
  0x00000000677DC000  ||  // unlock_at as i64 big-endian (1736294400)
  0x0000000000000000  ||  // outcome_at_or_zero (outcome_at absent)
  0x000000000047A596  ||  // drand_round as u64 big-endian (4695446)
  body_hash           ||  // 32 bytes
  title_hash              // 32 bytes (all-zeros sentinel; title absent)

Expected output:
  qub_id = SHA3-256(preimage)
         = 4a84e3dfaec32954949c30073f8e6506
           fd3204c1bb97f9162b81c7587afe412e

Les implémentations DOIVENT produire des valeurs body_hash et qub_id identiques pour cette entrée. Ce vecteur de test DEVRAIT être le premier test unitaire écrit. Les valeurs canoniques ci-dessus ont été calculées par l’implémentation de référence et DOIVENT correspondre bit pour bit. Les anciennes dispositions de prototype antérieures au lancement — aucun qub en production ne dépendait des deux premières — utilisaient 92 octets avant outcome_at (3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0) et 100 octets après l’ajout d’outcome_at_or_zero (b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed). La disposition actuelle de 108 octets a ensuite ajouté drand_round et le séparateur de domaine QUB_ID_V2. Un ancien vecteur de 108 octets utilisait l’ancienne correspondance de tour par ceil (drand_round = 4695445) et produisait 3a9fcb31b750d985c262fada6d4f777fd6a28be831d941d85c131f5a4bbaf8a4 — toujours un qub_id valide pour cette entrée de tour, tandis que l’exemple ci-dessus suit la correspondance de tour actuelle du §4.3.

14.2 Correspondance déverrouillage-tour

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

Calculation:
  (1735689600 - 1595431050) / 30 = 4675285.0
  floor(4675285.0) + 1 = 4675286

drand_round = 4675286

Le tour 4675286 est publié à 1595431050 + (4675286 - 1) * 30 = 1735689600 — exactement à unlock_at, jamais avant. (L’ancienne correspondance ceil antérieure à la release donnait 4675285, publié à 1735689570 — 30 secondes trop tôt ; les vérificateurs acceptent cet ancien tour selon §4.3.)

14.3 Aller-retour CBOR canonique

Les implémentations DOIVENT vérifier que serialize(parse(serialize(qub))) == serialize(qub) pour toutes les entrées valides. C’est un test de propriété, pas un vecteur unique.

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)

Les octets CBOR canoniques et le body_hash SHA3-256 sont calculés par l’implémentation de référence. Les implémentations DOIVENT produire un CBOR identique au niveau octet pour cette entrée.

Les implémentations DOIVENT également vérifier que serialize(parse(serialize(pact))) == serialize(pact) pour toutes les entrées PactTerms valides (test de propriété).

14.5 Vecteurs cross-langage de l’enveloppe externe

L’enveloppe externe (§13) a une fixture canonique distincte à crates/qub-core/tests/vectors/wrapper_v1.json. Chaque cas fixe un tuple (key, nonce, qub_id, sealed_cbor) en entrées hex opaques et asserte une sortie expected_wrapper_hex spécifique. Les deux implémentations de référence consomment le même fichier JSON :

La fixture fixe actuellement trois cas d’enveloppe de bas niveau. Ils testent l’encodage déterministe d’OuterWrapper et l’interopérabilité AEAD indépendamment de l’invariant de forme de livraison de la §13.8 ; en particulier, le nom historique basic-text-public et sa valeur interne visibility = 0x01 ne font pas des octets enveloppés résultants une livraison publique conforme. Un producteur DOIT toujours stocker les octets internes publics nus et n’envelopper que les octets internes privés (0x00).

Cas Couverture
basic-text-public Nom historique d’une fixture de bas niveau. La plus petite forme réaliste de SealedQub, sans champ optionnel ; teste uniquement les octets du wrapper et ne constitue pas une livraison stockée conforme à la §13.8.
with-recipient-pubkey SealedQub avec recipient_pubkey défini (chemin futur réservé). Exerce un autre ensemble de clés CBOR internes ; le contenu distinct de sa fixture produit indépendamment un qub_id différent (recipient_pubkey lui-même ne fait pas partie de la préimage de la §4.1).
longer-body Corps d’environ 4 KiB — exerce les préfixes de longueur CBOR multi-octets à l’intérieur de l’enveloppe interne et du chiffré externe.

Les implémentations DOIVENT produire un expected_wrapper_hex identique au niveau octet pour les entrées enregistrées. La régénération de la fixture nécessite QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors et est réservée aux changements de format délibérés.


15. Gouvernance du profil cryptographique (futur)

Cette section est informative pour la v1 et devient normative dès la première fois qu’un second algorithme entre dans l’une des primitives cryptographiques de qub.

15.1 Posture actuelle

Le protocole v1 lie exactement un algorithme par primitive :

Les vérificateurs codent actuellement en dur les longueurs de clé et de signature de chaque primitive active. Les octets sig_alg et de version de l’enveloppe sont des sélecteurs explicites, mais la v1 n’effectue aucune négociation en bande et n’admet que les valeurs actives ci-dessus.

15.2 Forme prévue

Lorsqu’un second algorithme entrera dans le protocole, le vérificateur sera configuré pour un CryptoProfile nommé (par exemple, ExqubV1) énumérant l’ensemble exact des valeurs autorisées par primitive — sig_algs, chaînes drand, versions de wrapper, types de contenu. Le profil est fixé au moment de la vérification, jamais négocié en bande. Toute valeur en dehors du profil actif est rejetée.

Cela garantit que l’ajout de ML-DSA-87 ou l’activation d’Ed25519 ne peut pas affaiblir rétroactivement les configurations de vérificateur existantes : un vérificateur v1 reste un vérificateur v1 même après la publication d’un profil v2.

15.3 Conditions de déclenchement

Promouvoir §15 au statut normatif dès que l’une des propositions suivantes est faite :

Jusque-là, §15 est un emplacement réservé qui fixe la forme de migration afin que les futures PR atterrissent sur une cible connue plutôt que de re-débattre de la surface de négociation à partir de zéro.


16. Journal de transparence et niveaux de durabilité (Implémenté — révision terminée)

Statut. Cette section est implémenté (W5/UP-B1, Étapes 1–8), avec le producteur et la portée de la racine de confiance indiqués ici. Les formats de données, le hachage et les chemins de vérification sont actifs : les types Merkle de base + CBOR canonique (qub-core), le miroir TypeScript + le bundler ANS-104 (workers/api/src/crypto/), l'auteur unique LogDO + magasin de nœuds R2 clé-coordonnée, le /upload tentative de journal-ajout, les cron quotidiens anchor + bundler-drain, le GET /api/v1/qub/:tx_id/proof (inclusion) et GET /api/v1/log/consistency (RFC 9162) points de terminaison de preuve, la preuve d'inclusion typée transportée dans le .qub bundle (§17.5), le vérificateur d'ancrage ANS-104 natif (tools/qub-verify), et le crochet à double autopublication (§16.6). Un succès /upload est toujours R2-durable mais n'est couvert de logs que lorsque LOG_DO est configuré et l'ajout en ligne réussit ; ce n'est qu'alors que sa réponse est portée log_seq, receipt, et anchor_status. Si RECEIPT_SK est absent ou invalide, ce reçu sig_b64url est vide et ne fournit aucune non-répudiation. Le courant /seal et les chemins de publication de pact programment des transactions individuelles Arweave mais n'ajoutent pas de feuille de journal. Aucun code n'exécute actuellement le /upload commentaire proposé pour une réconciliation ultérieure après un échec d'ajout. L'examen externe W5 est terminé : §16.15 enregistre les décisions de conception et les contraintes de lancement, mais ces contraintes n'élargissent pas la couverture du producteur mentionnée précédemment. Trois éléments de confiance/déploiement restent verrouillés: (a) le portefeuille ancre dédié (ANCHOR_JWK; LogProfile.anchor_owner est toujours le [0xAB; 32] espacement réservé); (b) la clé de signature de reçu et la correspondance de l'empreinte de clé publique (RECEIPT_SK est facultatif et LogProfile.receipt_pubkey est actuellement vide) ; et (c) le dépôt GitHub self-published-heads + jeton (§16.6). Jusqu'à ce que les ancres/profils soient provisionnés, un vérificateur autonome rapporte l'état de la preuve honnêtement plutôt que de prétendre à une vérification entièrement ancrée et épinglée. La conception est strictement additive et il y a aucun changement au SealedQub / QubEnvelope format de câble.

16.1 Justification et niveaux de durabilité

Les voies de publication actuelles découplent la reconnaissance de la confirmation Arweave : elles dérivent et signent une transaction individuelle, conservent l'artefact et l'état exact de soumission dans R2, puis publient de manière asynchrone. Le journal de transparence ajoute une couche de classement indépendante pour le sous-ensemble général. /upload demandes dont LogDO append réussit :

Niveau Nom Garantie Quand
T1 Accusé de réception synchrone R2-premier Plancher de durabilité — les octets scellés et l'état exact de publication sont écrits dans un stockage durable avant que le succès ne soit renvoyé. Mis en œuvre à travers les voies de publication actuelles.
T2 Inclusion dans le journal de transparence par lots Engagement en lecture seule, insensible aux falsifications + ordre total une fois inclus et ancré. Producteur actuel : réussi LogDO ajoute de /upload; la réponse contient le tuple de reçu. Pas universel.
T3 Permanence Per-qub Arweave Une transaction individuelle Arweave pour le qub. Actuellement préparé pour chaque publication acceptée et envoyé de manière asynchrone ; la transaction signée exacte reste dans la boîte d'envoi drainable jusqu'à sa livraison.

Les niveaux décrivent des propriétés distinctes de preuve et de durabilité, pas le plan commercial actuel. Le code actuel planifie toujours une transaction Arweave individuelle pour chaque publication acceptée ; il n'expose pas T3 uniquement comme une option payante. Les plafonds de quota de clé API/compte restent des contrôles applicatifs distincts.

Durabilité honnêteté. L'écriture T1 est synchrone, donc une réponse réussie établit la durabilité au niveau de l'application sans attendre une passerelle Arweave. Elle n'établit pas elle-même un horodatage indépendant. Une transaction individuelle confirmée fournit sa limite supérieure de temps de bloc. Pour une réponse portant le tuple complet de reçu T2, le prochain ancrage confirmé peut fournir la preuve de journal décrite ci-dessous. Si le tuple est absent, aucune surface ne peut impliquer que ce qub est déjà dans le journal de transparence. La latence des ancrages et de la publication n'a aucun SLA numérique au niveau du protocole.

16.2 Structure des feuilles de journal (deux formes engagées)

Une entrée de journal est un LogLeaf, encodé en CBOR canonique manuscrit selon le profil §3.1 (longueur définie, sans tags, sans nombres à virgule flottante, entiers en forme la plus courte, texte en NFC, champs optionnels omis lorsqu'absents, clés ordonnées par longueur en octets encodée croissante puis par ordre des octets). Le §3.1 parse → re-encode → compare la garde canonique est appliquée sur le chemin de codage avant le hachage (pas seulement lors du décodage), donc deux implémentations ne peuvent pas être en désaccord sur les octets feuille à cause d'une différence de largeur d'entier ou d'ordre de clé. Tous les entiers sont u8 / u64 / i64; tous les digests sont des chaînes d'octets de 32 octets (bstr[32]). Un identifiant de transaction Arweave stocké est un condensé SHA-256 brut de 32 octets transporté comme bstr[32], jamais une chaîne de texte base64url (correspond à §3.3).

La feuille a deux formes sélectionnées par un kind octet, parce que le chemin de téléchargement général est aveugle aux octets : POST /api/v1/upload traite délibérément les deux formes de charge utile acceptées comme opaques et reçoit qub_id et unlock_at uniquement en tant que déclarations de client non fiables. Sur le chemin privé par défaut, body_hash, drand_round, created_at, et drand_chain_version sont également cachés à l'intérieur de l'enveloppe extérieure §13, dont la clé n'est jamais détenue par le Travailleur. Le système de type définit également une forme attestée pour un producteur qui dérive body_hash / drand_round lui-même. Le courant /seal la route a ces valeurs mais n'appelle pas LogDO, donc la production émet actuellement seulement des affirmations (0x02) provient des ajouts réussis de téléchargements généraux. La répartition maintient chaque valeur engagée honnête sans prétendre que le producteur attesté est connecté :

Clé Longueur Enc. Type Présence Sens
seq quatre u64 Obligatoire Indice de feuille global basé sur 0 ; la position à laquelle la preuve d'inclusion s'engage.
kind cinq u8 requis 0x01 capable de l’être attesté (défini, non actuellement émis) ou 0x02 affirmé (téléversement aveugle par client-sceau / octet).
ref quatre bstr[32] requis ID de référence de feuille. Attesté → brut qub_id. Affirmé → le aveuglé identifiant SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1).
chash six bstr[32] requis Adresse de contenu SHA3-256(stored_bytes) — le seul lien de contenu que le Travailleur peut toujours calculer honnêtement, sur les deux chemins.
unlock_at dix i64 requis Copié (attesté) ou affirmé (affirmé) ; validé > 0 avant qu'il n'entre dans la feuille.
received_at douze i64 requis Horloge murale de travail à R2-ack. Non probatoire (affirmé par l'opérateur ; §16.6). Présent pour l'auto-description, jamais une preuve. Validé > 0.
body_hash dix bstr[32] kind=0x01 seulement Omis sur 0x02 — le Travailleur ne le possède pas en vertu de l’article §13.
drand_round douze u64 kind=0x01 seulement Omis sur 0x02.

A kind=0x02 la feuille ne commet délibérément aucun body_hash ni drand_round: il atteste l'engagement et l'organisation d'un texte chiffré opaque à l'adresse du contenu chash, affirmant qub_id et unlock_at — pas son texte en clair ou son tour. Les jambes en clair/tour pour un qub affirmé proviennent du §11 existant .qub-vérification du bundle, pas à partir du journal (§16.11). drand_chain_version n'est pas dans la feuille (il est à l'intérieur de l'enveloppe sur le chemin par défaut) ; la granularité de la chaîne se trouve sur l'ancre (§16.7). Discipline de l'encodeur : rejeter un tout-zéro ref ou chash, et rejeter les non-positifs unlock_at / received_at, reflétant le outcome_at > 0 garde sentinelle à l'intérieur cbor.rs.

16.2.1 Aveuglement Private-qub

Le journal ne doit pas devenir l'oracle d'énumération que le wrapper externe §13 est destiné à empêcher (§13.1). Pour un qub privé (enveloppé) le asserted la feuille engage le aveuglé identifier SHA3-256(qub_id ‖ log_blind_secret), où log_blind_secret est un secret détenu par le serveur, et omet body_hash. Un tiers ne peut pas associer une telle feuille à un élément spécifique qub_id; le détenteur du qub, qui possède l'URL de livraison et donc qub_id, peut recalculer l’aveugle pour confirmer leur propre inclusion. Un qub public (déjà énumérable, déjà portant le Visibility: public La balise Arweave selon §13.8) engage le brut qub_id. C'est le seul endroit où la vérifiabilité autonome cède délibérément à un invariant de confidentialité porteur de charge ; le lien autonome pour les qubs privés est chash (§16.9).

log_blind_secret garde (résolu — §16.15 Q4). L'aveugle protège l'inunlinkabilité des feuilles, pas la confidentialité du texte en clair (le wrapper §13 le garantit indépendamment). Sur un log_blind_secret compromis, pour tout qub_id l'adversaire détient déjà ou peut reconstruire (chaque qub dont il possède le paquet/URL, ainsi que tout à faible entropie ou public qub_id) il recompute la feuille ref dans un hachage et le relie — il s'agit d'une liaison directe d'une population connue, pas d'une force brute sur un espace inconnu. Classifier log_blind_secret comme un secret de corrélation/de niveau Sybil dans le même niveau de garde que les autres secrets du serveur, et ne tourner qu'en avant (une rotation ré-aveugle les feuilles futures ; elle ne peut pas dissocier rétroactivement celles déjà ancrées).

16.3 Hachage des feuilles et des nœuds

RFC 6962 §2.1 hachage séparé par domaine avec SHA-256 remplacé par 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

Octets de préfixe de domaine 0x02 (chaîne d'entrée, §16.4) et 0x03 (Le hachage STH, §16.6) sont réservés et distincts de ceux-ci. Ce sont des octets uniques et ne peuvent donc pas entrer en collision avec les séparateurs de domaine ASCII existants de 10 octets (QUB_ID_V2, etc.). L'arbre est le RFC 6962 gauche-plein déséquilibré arbre (chaque division intérieure à la plus grande puissance de deux strictement inférieure au nombre de feuilles du sous-arbre), ce qui permet aux preuves d'inclusion et de cohérence de partager un algorithme de chemin d'audit. La spécification de référence contient un pseudocode explicite de dérivation gauche/droite et fixe un vecteur de test non puissance de deux (à 5 feuilles) Ainsi, le cas de promotion sur le bord droit — que masque un vecteur à 4 feuilles — est mis en œuvre.

16.4 Chaînage de hachage (interne)

Le LogDO maintient une chaîne d'entrées interne uniquement pour la cohérence en cas de panne. Il est jamais publié et jamais destiné aux vérificateurs:

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

L'autorité publiée en mode ajout uniquement est la racine Merkle cumulative + son ancrage (§16.5–16.6), jamais l’ordre brut dans lequel l’opérateur sert les feuilles : la chaîne recalculera pour n’importe quel ordre servi, donc seul le pivot racine ancré fixe la position canonique.

16.5 Arbre Merkle cumulatif et regroupement

Il y a un arbre RFC 6962 en constante expansion sur toutes les feuilles dans seq ordre — pas des arbres par lot isolés. (Une construction par lot en chaîne à feuilles portées a été rejetée : ce n’est pas une véritable relation préfixe, donc ses « preuves de cohérence » ne sont pas fiables.) L’arbre cumulatif fournit de véritables preuves de cohérence RFC 9162 et permet à une seule ancre récente de prouver l’inclusion pour tout qub plus ancien.

Le LogDO L'objet durable est le écrivain unique (blockConcurrencyWhile, reflétant QuotaDO / EntitlementDO) — ajouter à un journal partagé consiste en une lecture-modification-écriture sur un état partagé et doit donc PASSER par un DO, jamais par KV. Il met en cache le front droit de l'arbre (O(log n) hachages) donc fermer un lot est O(batch). A lot est l'ensemble des feuilles ancrées ensemble ; ses déclencheurs implémentés sont un tree_size avance d'au moins LOG_BATCH_MAX_LEAVES (par défaut 4096), âge atteignant la cadence d'ancrage, ou une fermeture forcée explicite par l'administrateur/cron. root_i est le hachage de l'arbre de Merkle cumulatif sur les feuilles 0 .. tree_size_i.

16.6 Tête d'arbre signée via Ancre Arweave

La transaction d'ancrage Arweave est la tête d'arbre signée et remplace une signature d'opérateur pour la tête de l'arbre elle-même : l'ancre quotidienne n'a pas besoin de clé qub parce que la transaction Arweave owner est la signature. La thèse du fossé tient — le substrat immuable, et non un secret détenu par un qub, porte la charge de la racine ancrée.

La conception du journal prévoit un append réussi et chaud clé de reçu (§16.10), épinglé dans LogProfile et contresigné par anchor_owner. L'implémentation actuelle n'a pas encore terminé ce provisionnement de racine de confiance : RECEIPT_SK est optionnel, une clé absente/invalide produit sig_b64url: "", et le compilé LogProfile.receipt_pubkey est vide. Un tel reçu peut décrire la feuille jointe mais est pas un reçu signé non-répudiable. La revendication de conception plus forte ne s'applique qu'après qu'un vérificateur ait publié les broches et que le propriétaire de l'ancre les ait contresignées. Une réponse de publication sans le tuple complet du reçu ne fait aucune revendication d'acceptation dans le journal ; une réponse avec une signature vide fait une revendication de position d'ajout mais aucune revendication de vérification de signature.

Le SignedTreeHead est le CBOR canonique (clés par longueur encodée) : size:u64, root:bstr[32], batch:u64, prev:bstr[32] (antérieur sth_hash; genèse = 32 octets nuls), log_id:bstr[32], first_seq:u64, anchored_at:i64. Son hachage est sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).

Racine de confiance épinglée. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Un vérificateur conforme DOIT exiger anchor_tx.owner == LogProfile.anchor_owner, où anchor_owner (et la clé publique de réception) est intégrée dans qub_core comme le LogProfile — aux côtés des constantes quicknet déjà présentes DrandTimelockProvider::quicknet() — et distribué avec le binaire du vérificateur. Le vérificateur DOIT également vérifier la transaction Arweave lié localement à data → tx_id plutôt que de faire confiance à une passerelle /raw/ réponse. Cela ferme la faille d'équivoque des portefeuilles voyous : « ancré sur Arweave » n'a aucun sens tant que le vérificateur n'épingle pas quel portefeuille.

La rotation est une extension de gouvernance §15, pas une réutilisation (résolu — §16.15 Q3). La surface de profil de §15.2 énumère actuellement seulement sig_algs / chaînes drand / versions de wrapper / types de contenu, et la liste de déclencheurs de §15.3 ne contient aucun de ceux-ci — LogProfile / anchor_owner est pas pourtant à la surface de §15. La gouvernance de la rotation doit donc être construite : §15.3 est étendu (ci-dessous) pour ajouter le LogProfile déclencheur, et une rotation est signée LogProfile bump expédié dans une mise à jour du vérificateur. A prévu la rotation transporte une signature croisée sortante → entrante ; a axé sur le compromis la rotation ne peut pas (la clé sortante est non fiable/indisponible exactement à ce moment-là) et revient à la relance régie par le §15, avec la vérification du fork de l'ancre précédente (ci-dessous) limitant les dommages entre-temps.

Fenêtre d'équivoque (paramètre de confiance de première classe). Une feuille n'est résistante à l'équivoque que lorsque son ancrage couvrant est Arweave-confirmé. La fenêtre est received_at → anchor confirmation (cadence + finalité Arweave, sans garantie de latence du protocole). Avant la fourniture de la racine de confiance, l'implémentation actuelle fournit l'intégrité opérationnelle de qub ainsi que les métadonnées d'ajout non signées présentes ; elle ne fournit pas la garantie de non-répudiation prévue. Trois artefacts de responsabilité définissent la conception achevée (le modèle du témoin est la résolution de la question §16.15 Q2) :

  1. Reçu scellé (dépendant de l'approvisionnement) — l'analogue SCT retourné lorsqu'un ajout au journal d'un téléchargement réussit (§16.10). Il ne devient non répudiable que lorsque sig_b64url n'est pas vide et La relation correspondante clé publique/propriétaire de l'ancre est épinglée dans le vérificateur. La broche du profil de production actuellement vide ne peut pas supporter ce verdict. Ce contrôle ne s'applique pas à un tuple de reçu omis ou à un reçu non signé.
  2. Méthodologie de moniteur publiée + parcours de la chaîne précédente — l'ancre prev la chaîne est parcourue tête→genèse ; une bifurcation (deux ancres en une) size avec différent root, ou un cassé prev) est une preuve publiable de mauvaise conduite. La détection de l'équivocation est un engagement opérationnel déclaré, et non une hypothèse tacite.
  3. Têtes auto-publiées doubles — chaque nouvelle tête {sth_hash, tree_size} est publié sur un site dédié appartenant à qub dépôt GitHub public et en ajout uniquement (le support auto-édité porte-charge à preuve de falsification), avec un post social seulement comme corroboration au meilleur effort. Une publication échouée DOIT afficher une page (pas échouer silencieusement). Mis en œuvre (Étape 8) comme le publishHead accrocher au cron de l'ancre (workers/api/src/utils/heads-publish.ts): a PUT au contenu API sans un sha est uniquement ajout (un 422 signifie que la tête est déjà publiée, jamais une réécriture) ; optionnel / déployé avec contrôle PUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO} et inerte jusqu'à ce que le dépôt soit provisionné. Une panne grave de GitHub pages via le health_alert canal et fait sauter un indicateur d'échec durable (m:tlog_publish_head_fail); l'ancre Arweave elle-même ne revient jamais en arrière en cas d'échec de publication. Le fait de « ne pas échouer silencieusement » est garanti par cette métrique durable — sur laquelle les opérations DOIVENT avoir un tableau de bord d'alerte — même si la page e-mail en meilleur effort ne peut pas être livrée. Deux limitations honnêtes découlent de « ancrer à l'avance » (la tâche cron ne publie que lorsque la taille avance) : un échec transitoire de GitHub laisse un écart dans la séquence des têtes publiées pour cette taille — bornée, non silencieuse (elle fait défiler), et parce que chaque tête engage un arbre surensemble, une preuve de cohérence §16.9 comble l'écart ; de manière cruciale, cette preuve de cohérence est calculée à partir de arbre faisant autorité ancré sur Arweave, pas à partir de la surface GitHub, donc un écart GitHub n'affaiblit jamais la vérifiabilité. Un rattrapage rétroactif qui comble les lacunes des têtes publiées est une amélioration différée.

Honnêteté liée (contrainte contraignante). Parce que qub contrôle les deux surfaces de publication prévues, c'est auto-édité, non témoigné de manière indépendante. Aucune surface de produit, de marketing ou juridique ne peut prétendre que le journal est "témoigné de manière indépendante". Après que les réceptions/profils/portes principales soient provisionnés, l'affirmation autorisée est que L'équivoque est détectable et un ajout signé avec succès laisse un reçu non répudiable. Avant cela, cette revendication n'est pas disponible. Un véritable témoin tiers indépendant est reporté à une future augmentation de gouvernance §15.

received_at est affirmé par l'opérateur et aucune réclamation ne peut s'y appuyer — il n'apparaît jamais comme preuve ni comme corroboration de litige sur aucun produit / juridique / API / surface de rendu de preuve. Le temps de bloc d'ancrage Arweave T est le seul horodatage sans confiance (une limite supérieure sur « enregistré par »). Toute vérification de cohérence du moniteur sur received_at DOIT comparer avec T, pas contre l'opérateur contrôlé anchored_at Champ STH ; une telle vérification n'est qu'une protection contre un bug d'horloge d'un opérateur honnête, pas un contrôle de responsabilité contre un opérateur malveillant (§16.15 Q5).

16.7 Format et Cadence des Transactions Anchor

Le AnchorBundle est le corps de la transaction Arweave canonique-CBOR, écrit via le bundler §16.8 : ver:u8, sth:bstr (canonique SignedTreeHead octets), prev_anchor:bstr (identifiant de transaction d'ancrage précédent en octets bruts ; omis lors de la genèse), chain_hash:tstr (la chaîne de drand en vigueur — quicknet), et le flux CBOR de feuilles de lot dans seq commande donc l'ancre est autonome : un moniteur redérive root à partir du corps sans dépendance qub. (Si le flux de feuilles devient important à volume élevé, une révision future pourrait ne commettre qu’une plage de feuilles par référence ; noté, non adopté dans la v1.)

Les tags Arweave sont intentionnellement énumérables — le journal est destiné à être trouvé, contrairement aux qubs privés : App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. Les balises sont indices non fiables; le corps CBOR est la seule autorité.

Cadence : quotidiennement par défaut, revisité avec volume (le déclencheur de taille raccourcit automatiquement la cadence effective sous charge). Le producteur actuel n'implémente pas de crochet de force-ancrage payé. portefeuille Anchor est dédié et à faible vitesse, séparé du portefeuille de téléchargement — il DOIT être le sien posséder JWK (une clé distincte, pas un rôle logique sur le portefeuille de téléchargement) de sorte qu'une compromission du portefeuille de téléchargement ne puisse pas falsifier les ancres — avec un budget strict de transactions d'ancrage par jour. La posture de conservation est énoncée clairement : un touche de raccourci à portée étroite avec disjoncteur serré et faible solde, pas "froid" — un portefeuille qui signe automatiquement tous les jours ne peut pas être froid, et la spécification ne prétend pas le contraire.

16,8 ANS-104 Regroupeur

Un encodeur ANS-104 DataItem interne et un signataire deep-hash, environ 300 lignes, Web Crypto uniquement, zéro dépendance npm (les deux SDK Turbo échouent à npm ci --ignore-scripts porte de la chaîne d'approvisionnement). Disposition des octets de DataItem :

signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data

La signature est Arweave deepHash — un récursif SHA-384 digest (exigence de fil d'Arweave, crypto.subtle.digest("SHA-384")) par-dessus ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — puis RSA-PSS sur le hash profond avec la JWK du portefeuille via crypto.subtle; id = base64url(SHA-256(signature)). Le SHA-384 ici est mis en quarantaine en tant que primitive uniquement filaire Arweave, jamais une primitive de confiance qub (§15 enregistre la clôture ; le hachage qub trust est SHA3-256 partout).

Le chemin de code ANS-104 sert la machinerie de repli/déversement différé et écrit AnchorBundle DataItems. Le chemin de publication ordinaire crée d'abord une transaction Arweave exactement signée et en conserve le JSON dans une boîte d'envoi durable ; la publication directe est une optimisation de latence, et le chemin de vidage réessaie la même transaction avant d'appliquer son secours de regroupement. Schéma de signature (résolu — §16.15 Q8) : v1 signe avec RSA-PSS (type de signature 1) réutilisation du mécanisme JWK du portefeuille Arweave existant (aucune nouvelle garde de clé à long terme, servant la thèse du « un secret de moins ») ; Ed25519 est différé vers le chemin de migration PQ §15.

Le hachis profond roulé à la main est le code à risque le plus élevé et à couverture naturelle la plus faible dans W5, donc son passage conditionnel est non négociable (§16.15 Q8) :

  1. Le dispositif inter-langues tlog_v1.json (Rust + TS, le §14.5 wrapper_v1.json le modèle) couvre le deep-hash, les octets + identifiant du DataItem, les hachages de feuilles, une racine de 5 feuilles + chemin d’audit, un hachage STH, une preuve d’inclusion et une preuve de cohérence — en à la fois les directions signer et vérifier (la direction de vérification est importante parce que la vérification locale tx → tx_id du §16.6 tire le hachage profond dans chaque vérificateur autonome, pas seulement dans l'écrivain).
  2. Un aller-retour d’interopérabilité unique via un empaqueteur ANS-104 de référence, consommé comme données de test statiques uniquement — jamais une dépendance d'exécution npm (la posture Web-Crypto-only / sans scripts d'installation reste).
  3. Le chemin deep-hash + RSA-PSS doit faire un aller-retour via le même crypto.subtle primitifs usages de production, donc l'encodeur interne est compatible par octet.
  4. En cours moniteur d'acceptation post-lot confirme que chaque DataItem d’ancrage / de secours atteint réellement l’acceptation par Arweave, avec une alarme et un disjoncteur — car le hachage profond sert également à la file d’attente de secours en cas d’indisponibilité d’Arweave, de sorte qu’une régression silencieuse remplirait cette file d’attente avec des éléments rejetés par le réseau pendant la panne exacte qu’elle est censée couvrir.

16.9 Preuves d'inclusion et de cohérence

Les deux sont RFC 9162, SHA3-256, servis comme CBOR canonique.

Preuve d'inclusion — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (le CBOR de feuille exact — le vérificateur recalculé leaf_hash lui-même et ne fait jamais confiance à un hachage fourni), 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 }.

Preuve de cohérence — 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. Une seule liste de clés non ambiguë, fixée par vecteur de test.

Vérification autonome (pas de serveur qub, prolonges §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).

Le stockage servant de preuve DOIT être clé-coordinate (résolu — §16.15 Q7, condition préalable bloquante). La génération de preuves à feuilles froides est neutre en termes de correction seulement si le matériel d'audit R2 est un magasin de nœuds Merkle persistant indexé par coordonnée absolue de l'arbre (level, index) — pas par-batch deltas de nœuds. Avec un magasin indexé par coordonnées, tout (leaf i, size N) le chemin d'audit est un ensemble de O(log N) R2 direct GETavec pas de recomputation à travers les frontières de lots; avec un magasin à clé par lot, ce n'est pas le cas, ce qui est le vide dans la disposition du stockage que cette solution comble. Les corps des feuilles sont également accessibles par contenu via seq. Un vecteur de test W5 DOIT prouver un feuille froide de l'ère de la genèse contre une racine beaucoup plus tardive en utilisant seulement R2 + Arweave avec le stockage LogDO effacé, donc la revendication de sécurité de récupération dans le §16.13 est étayée plutôt que simplement affirmée. La O(log N) R2 séquentiel GETappartiennent seulement sur le point de terminaison de preuve asynchrone — jamais sur le chemin critique du scellage (§16.10) ni sur un cron par tick.

16.10 R2-Premier Ordre d'Ack

Le mis en œuvre POST /api/v1/upload la séquence est :

  1. Portes avant (auth, validation, clé de fragment d'idempotence) — inchangées.
  2. Créez, étiquetez et signez la transaction Arweave individuelle exacte. Cela dérive tx_id localement, bien que la création de transaction puisse récupérer des métadonnées de récompense/ancre depuis une passerelle. Un échec de préparation entraîne toujours l'échec de la requête avant l'accusé de réception.
  3. Synchroniquement écrire l'artéfact sélectionné à qub-cache/<tx_id> et persister les enregistrements de création-opération/boîte de sortie stables. Ce sont le niveau de durabilité et de nouvelle tentative ; les échecs avant règlement renvoient 503.
  4. Quand LOG_DO est configuré, essayer simultanément LogDO.append(leaf). L'écrivain unique attribue seq, étend la chaîne d'entrée et met à jour la frontière. L'appel RPC append fait seulement cela ; la fermeture par lot s'exécute hors chemin sur l'alarme. Une défaillance de transport/application append est actuellement défaillance douce: la réponse peut encore réussir sans log_seq, receipt, ou anchor_status. Malgré un commentaire d'implémentation, aucune réconciliation automatique des journaux ultérieurs n'est mise en place aujourd'hui.
  5. Retournez l'accusé de réception. Inclure { log_seq, anchor_status: "pending", receipt } uniquement lorsque l'append a renvoyé le tuple complet réussi. receipt.sig_b64url est vide lorsque le signataire du reçu est indisponible ; les clients NE DOIVENT PAS considérer cette valeur comme signée ou non réfutable. L'absence du tuple signifie uniquement une publication durable, et non une acceptation dans le journal de transparence.
  6. Utilisez une tâche différée pour publier la transaction signée exacte. Le succès supprime la boîte d'envoi ; l'échec la laisse pour le cron de vidage limité et ne doit pas modifier ce qui a déjà été reconnu tx_id. Les métadonnées provisoires et autres composants auxiliaires réalisés selon les meilleures tentatives sont également reportés.

Limite de latence. Le chemin de la requête comprend le travail d'autorité/quota de la première moitié, la préparation/la signature des transactions, les écritures R2 durables, et (lorsqu'il est configuré) le LogDO tentative. < 300 ms apparaît dans la révision de conception comme un objectif opérationnel, et non comme une garantie de protocole ; l'étape actuelle de préparation de la transaction peut effectuer une demande de métadonnées de passerelle. Les alarmes de latence et les portes de lancement sont des contrôles opérationnels, et non des preuves disponibles pour un vérificateur.

16.11 Modèle de confiance — la revendication précise, limitée par le type de feuille

Pour kind=0x01 (attesté) : Ce contenu — correspondance du corps body_hash, identifié par qub_id — a été enregistré dans le journal append-only de qub à la position seq et existait au plus tard à l'heure du bloc Arweave T; c'était cryptographiquement illisible jusqu'au tour de drand R = unlock_round(unlock_at). Ceci est le complet {tlock round binding + Merkle inclusion + anchored root} triple.

Pour kind=0x02 (affirmé, par défaut) : Un texte chiffré opaque avec adresse de contenu chash, affirmant qub_id et unlock_at, a été enregistré dans le journal en mode ajout seul à la position seq et existait au plus tard à l'époque du bloc Arweave TLes jambes rondes et du corps sont fournies par le §11 existant .qub-vérification du bundle (qub_core::unlock), pas par le journal ; ce que le journal ajoute à une transaction per-qub nue, c'est un ordre infalsifiable, un engagement de temps maximal sans confiance et une résistance aux équivoques.

Les deux revendications excluent, selon le §11 : la paternité sans sig_alg ≥ 0x01, d'intention et de granularité de sous-ancrage temporel. Aucun ne permet à une réclamation de s'appuyer sur received_at.

Plafond de réclamation (contrainte de lancement contraignante — résolue §16.15 Q1). Pour un prétendu (kind=0x02) feuille, la revendication délimitée ci-dessus est la plafond sur quoi que ce soit produit, marketing, termes ou surface de rendu de preuve peut affirmer. Aucune surface ne peut déclarer ou insinuer que le journal prouve le contenu ou le tour de déverrouillage d’un téléchargement aveugle à des octets — le journal prouve l’ordonnancement + un engagement sans confiance à la limite supérieure d’un texte chiffré opaque. Le contenu et la preuve du tour proviennent exclusivement de l’existant §11 .qub- vérification de bundle, qui est indépendante du journal. Une publication sans ajout/réception réussi n'a aucune revendication de journal du tout.

16.12 Gestion des versions et coordination avec le W3

Il y a non SealedQub bossage de fil et donc pas de mise à jour de la version du protocole (§12.2) : le journal est un sidecar qui s'engage à respecter les champs et octets existants, donc il n'entre pas dans l'historique des versions du protocole §12.3. Optionnel selon W3 drand_chain_version est intact et reste le seul optionnel SealedQub domaine. Le journal introduit plutôt ses propres espaces de version indépendants — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — reflétant l'indépendance de la version du wrapper §12.5 (le wrapper porte un octet de version indépendant de la version du protocole, et les versions de journal suivent la même séparation).

La livraison de preuve est récupérée par défaut, avec une option d'accompagnement. Une preuve ne peut pas exister au moment du scellement (l'ancre n'a pas encore été écrite), donc le moment du scellement .qub le paquet reste sans preuve. Le vérificateur de W7 récupère GET …/proof une fois, ou en mode entièrement hors ligne reconstruit la preuve à partir du public AnchorBundle via une requête Arweave sur Log-Id. Le .qub le package (W7) réserve un optionnel inclusion_proof membre — absent lors du sceau, peuplé par une réexportation après ancrage pour archivage à froid — suivant le même schéma « facultatif, omis par défaut, additif » que celui de W3 drand_chain_version.

16.13 Rétention

Les fenêtres de rétention pour le LogDO open tail, le substrat de service de preuves R2, les compteurs du disjoncteur d'ancrage et la file de secours du bundler sont spécifiées dans docs/DATA-RETENTION.mdPrincipe : le stockage à chaud par entrée du journal (LogDO) est récupérable après ancrage ; son matériau d'audit — la clé coordonnée (level, index) Magasin de nœuds Merkle + le seq-les corps de feuilles adressées (§16.9) + les ancres Arweave — sont permanents. La récupération d'une feuille froide depuis le DO n'invalide jamais une preuve délivrée, parce qu'une preuve se résout contre ce magasin de nœuds R2 permanent et l'ancre Arweave, pas contre le DO (et le vecteur de test §16.9 du DO effacé le prouve).

16.14 Vecteurs de test

W5 expédie le dispositif multilingue tlog_v1.json (§16.8) vecteurs supplémentaires travaillés : a kind=0x01 et un kind=0x02 feuille → leaf_hash; la racine cumulative à 5 feuilles ; une preuve d'inclusion ; une preuve de cohérence ; une AnchorBundle; et un identifiant DataItem. Ceux-ci coexistent avec les vecteurs de l'enveloppe externe §14.5 et sont utilisés à la fois par le Rust (qub-core) et les implémentations TypeScript (Worker).

16.15 Décisions d'examen (S5 — résolu)

La révision externe W5 (un passage de conception contradictoire + approbation du propriétaire) est terminée. Chaque décision ci-dessous est réglée et reflétée dans le texte §16 ci-dessus ; le contraintes de lancement obligatoires sont réaffirmés à la fin. La mise en œuvre peut se poursuivre selon eux.

  1. Chemin par défaut (kind=0x02) honnêteté de la feuille — RÉSOLU. Expédiez le type à deux feuilles divisé comme spécifié : kind=0x02 ne s'engage ni body_hash ni drand_round. Non *_body_hash champ sur le chemin aveugle aux octets (ce serait le signal faux « vérifié » le plus lisible pour les intégrateurs et c’est une commodité que le §11 fournit déjà depuis le paquet). Faire pas exiger un sceau de serveur pour les qubs attestés par journal (ce qui obligerait le texte en clair à passer par le Worker et détruirait le fossé de destruction cryptographique). Tout court-circuit auto-descriptif appartient dans le .qub paquet / enveloppe de preuve en tant que champ recalculé par le vérificateur, jamais un champ feuille. Plafond de réclamation confirmé par le propriétaire : §16.11.
  2. Responsabilité pour équivocation / omission — CONCEPTION RÉSOLUE, APPROVISIONNEMENT INCOMPLET. La conception exige que la clé de réception du sceau soit verrouillée en place LogProfile et contresigné par anchor_owner, plus la méthodologie de surveillance, le parcours de la chaîne précédente et les têtes auto-publiées doubles. Le profil compilé et les hooks de déploiement sont encore des espaces réservés/options comme détaillé au §16.6, donc la revendication plus forte détectable + reçue n'est pas actuelle tant que ces étapes ne sont pas clôturées. Il ne doit jamais être commercialisé comme témoigné de manière indépendante. Un véritable témoin tiers est renvoyé à une mise à jour de gouvernance §15.
  3. Racine de confiance du propriétaire de l'ancre épinglée + rotation — RÉSOLU. Adopter le LogProfile goupille (§16.6) ; le vérificateur vérifie anchor_tx.owner == anchor_owner et vérifie localement la liaison des données tx → tx_id. La gouvernance de rotation est un §15 extension à construire (§15.3 déclencheur ajouté), pas une réutilisation ; les rotations planifiées se signent mutuellement, les rotations dictées par des compromis reviennent au §15 avec le contrôle de la fourche pour limiter les dégâts.
  4. Cécité de la feuille Private-qub — RÉSOLU. Continue à aveugler pour les qubs privés (ref = SHA3-256(qub_id ‖ log_blind_secret)), cru qub_id pour les qubs publics (déjà §16.2.1), chash comme la cravate autonome. log_blind_secret est un secret de corrélation/de niveau Sybil, rotation vers l'avant uniquement (§16.2.1).
  5. received_at — RÉSOLU. Gardez-le dans la feuille, engagé mais explicitement non probatoire ; jamais présenté comme preuve ou corroboration de litige sur aucune surface. Toute vérification de sanity par moniteur se compare au temps du bloc Arweave T, pas contrôlé par l'opérateur anchored_at (§16.6).
  6. Chronométrage à niveaux vérifiables — RÉSOLUTION DE CONCEPTION, PAS ROUTAGE ACTUEL. Le design examiné attribue le timing du bloc d'ancrage au niveau groupé et la preuve à heure exacte au T3 payant, sans SLA numérique pour le premier. Les routes actuelles n'ont pas intégré cette distinction commerciale : elles planifient une transaction individuelle pour chaque publication acceptée, et la couverture des journaux reste conditionnelle comme indiqué dans §16.1/§16.10. Le texte produit doit décrire la mise en œuvre, et non cette future répartition des niveaux.
  7. Arbre cumulatif sur les Travailleurs — RÉSOLU. Arbre RFC 9162 cumulatif unique + LogDO à écrivain unique avec cache de frontière (marge confortable par rapport au plafond de ~1k écritures/sec du DO ; reporter le partitionnement Merkle des racines de fragments jusqu'à ce que l'on s'en approche). La clé de coordination (level, index) Le vecteur de test R2 node store + wiped-DO cold-leaf est implémenté (§16.9). < 300 ms reste un objectif de conception/opérationnel, et non une promesse de protocole (§16.10).
  8. Schéma de signature ANS-104 + deep-hash — RÉSOLU. RSA-PSS (type de sig 1, réutilisant le JWK du portefeuille-ancre dédié) ; Ed25519 différé vers le chemin PQ §15. Le hachage profond SHA-384 fait maison est soumis au dispositif croisé bidirectionnel, une vérification d'interopérabilité du bundle de référence uniquement statique, le partagé-crypto.subtle aller-retour, et le moniteur d'acceptation Arweave après le regroupement (§16.8).

Contraintes de lancement obligatoires (mettre en œuvre + révision produit/juridique) :


17. Bundle de vérification portable (.qub)

Statut. Cette section est implémentée (W7 / UP-C2) : qub_core::export produit et analyse le bundle, et tools/qub-verify est une CLI publique et autonome qui le vérifie hors ligne. §11 et §16.9 désignent déjà « le bundle .qub » comme l’unité consommée par un vérificateur autonome ; cette section en spécifie les octets et la procédure de vérification. Elle est strictement additive — le bundle regroupe les entrées existantes du §11 et ne modifie aucun format de fil on-chain.

17.1 Objet

§11 établit que tout tiers peut vérifier l’artefact cryptographique d’un qub sans la coopération de qub. Le bundle .qub rend cette vérification portable et hors ligne : il regroupe le CBOR scellé et la signature du tour drand qui le déverrouille dans un artefact autonome, afin qu’un destinataire puisse vérifier l’intégrité du contenu, la liaison au tour et les éventuelles signatures d’auteur sans aucun appel réseau — ni récupération depuis le stockage, ni requête drand en direct, ni API qub. Un bundle seul ne prouve pas quand son texte chiffré a été créé ; une transaction de stockage vérifiée indépendamment ou une preuve de journal ancré fournit cette affirmation distincte relative à l’heure d’existence (§11, §17.5).

17.2 Format du bundle

Un QubBundle est un CBOR canonique écrit à la main sous le profil du §3.1 — longueurs définies, aucun tag, aucun flottant, forme entière la plus courte, texte NFC, champs facultatifs omis lorsqu’ils sont absents, clés triées d’abord par longueur d’octets encodés puis octet par octet. Les trois clés de 15 caractères suivent l’ordre d < i < s. Un fichier .qub brut est constitué exactement de ces octets ; pour le transport par URL ou copier-coller, les mêmes octets sont encodés en base64url sans remplissage.

Clé Long. enc. Type Présence Signification
version 8 u8 requise Version du format du bundle (0x01).
sealed_at 10 i64 facultative Heure de scellement affirmée par le créateur (secondes Unix) ; auto-descriptive, non probante.
drand_round 12 u64 requise Tour auquel le qub est verrouillé. Projection du qub scellé intégré.
arweave_tx_id 14 tstr requise Identifiant de la transaction sous laquelle les octets scellés ont été stockés (pointeur de provenance).
drand_chain_id 15 tstr requise Chaîne drand (hex). Projection du qub scellé intégré.
drand_signature 16 bstr requise Signature de la balise drand pour drand_round — la valeur qui déverrouille le texte chiffré.
inclusion_proof 16 bstr facultative Preuve d’inclusion de Merkle du journal de transparence du §16, une fois le journal publié (§17.5).
sealed_qub_cbor 16 bstr requise Octets du SealedQubCbor interne — après déballage du §13 —, c’est-à-dire l’entrée de vérification du §11.

drand_round et drand_chain_id sont des projections pratiques de sealed_qub_cbor, transportées afin que les outils puissent les lire sans analyser le CBOR interne. Elles sont dérivées lors de la construction et revérifiées au décodage par rapport au qub scellé analysé ; un bundle dont un champ de premier niveau diverge de sa charge utile est rejeté. La discipline de l’encodeur reflète le reste du format de fil : rejeter un drand_signature ou arweave_tx_id vide et borner chaque champ de longueur variable.

17.3 Ce que prouve la signature drand intégrée

Le bundle transporte la signature drand au lieu d’exiger que le vérificateur la récupère. Le déchiffrement timelock — tlock sur la chaîne drand, §8 — ne peut réussir qu’avec la signature authentique de la balise pour le tour lié : une valeur que la chaîne ne publie qu’une fois ce tour écoulé et qui constitue une signature BLS valide sous la clé publique de la chaîne. Une signature falsifiée ou erronée échoue à la vérification BLS ou au déchiffrement IBE/AEAD. Un bundle qui se déchiffre prouve donc : le texte chiffré est lié au tour R, et le tour R est écoulé. Le vérificateur épingle la chaîne (DrandTimelockProvider::quicknet()) et applique le contrôle de liaison au tour du §11, de sorte qu’un bundle ne peut revendiquer un tour auquel son texte chiffré n’est pas lié.

Il s’agit d’une preuve de condition de publication, et non d’un horodatage de création. Une fois le tour R écoulé, n’importe qui peut créer un nouveau texte chiffré pour R et y joindre sa signature déjà publique. Le bundle seul NE DOIT donc PAS être présenté comme la preuve que le texte chiffré ou le contenu existait avant R, avant unlock_at ou avant un événement quelconque.

17.4 Procédure de vérification hors ligne

qub-verify <file.qub> exécute entièrement depuis le bundle la procédure standard du §11, en pilotant qub_core::unlock::unlock avec un DrandTimelockProvider épinglé :

1. Parse the .qub bytes → QubBundle (canonical-CBOR guard; bound every field;
   re-check drand_round / drand_chain_id against the embedded sealed qub).
2. BLS-verify bundle.drand_signature for the pinned chain and round, then
   tlock_decrypt(sealed.tlock_ciphertext, bundle.drand_signature) → QubEnvelope.
3. Verify SHA3-256(body) == body_hash               (§11 step 8).
4. Verify QubEnvelope.qub_id   == SealedQub.qub_id   (§11 step 9).
5. Verify QubEnvelope.unlock_at == SealedQub.unlock_at (§11 step 10).
6. Verify ciphertext round == unlock_round(unlock_at) and the chain binding.
7. If sig_alg != 0x00: verify author_signature (and any cosigner; §9.4).
8. Report integrity, round-elapsed/round-binding, authorship, and cosigner
   verdicts separately, plus the recovered body. Do not report a commitment
   timestamp unless step 9 succeeds.
9. Optional existence-time leg: verify an included §16 proof through its pinned
   anchor, or independently verify the referenced storage transaction. Report
   its block time as an upper bound on ciphertext existence.

La CLI quitte avec le code 0 — vérification réussie —, 1 — échec de vérification : encore verrouillé, différence de hachage du corps, liaison au tour ou à la chaîne rompue, ou signature dont la vérification échoue —, ou 2 — usage ou bundle mal formé. Un rapport --json fournit les mêmes verdicts pour l’automatisation. Le bundle étant autonome, le crate de vérification (qub-core) et la CLI (qub-verify) sont les seuls logiciels nécessaires à un tiers ; tous deux sont publics et réutilisent le chemin de vérification existant du protocole, sans cryptographie particulière.

17.5 Relation avec le journal de transparence

inclusion_proof est un emplacement facultatif pour la preuve d’inclusion de Merkle du §16. La vérification du bundle seul (§17.4) est complète pour l’intégrité, la liaison au tour/l’écoulement du tour et la paternité facultative, mais ne fournit intentionnellement aucune affirmation d’existence horodatée indépendamment. Une inclusion_proof renseignée et entièrement vérifiée jusqu’à l’ancre ajoute l’engagement propre au type de feuille et la borne supérieure temporelle du §16.11 sans changer la version du format du bundle. Une preuve absente signifie uniquement « aucune preuve incluse » — ni « invalide », ni nécessairement « non ancré ».

Dans l’implémentation de référence, cet emplacement est désormais typé : qub_core::export::QubBundle::inclusion_proof_typed() renvoie une Option<InclusionProof> qui transporte la structure complète du §16.9 — feuille, chemin d’audit, racine ancrée et AnchorRef — à travers le même champ CBOR opaque, sans changement de version du format du bundle. La CLI autonome qub-verify la consomme par sa branche --anchor et, jusqu’au provisionnement du portefeuille d’ancrage (§16, Statut), décrit une preuve renseignée mais dotée du propriétaire substitut comme limitée à l’inclusion plutôt qu’entièrement vérifiée par l’ancre.