Aller au contenu

PD-294 - Alignement contractuel du format merkle_proof sur RFC 9162 (inclusion proof)

1. Objectif

Contractualiser un format canonique merkle_proof aligné sur la structure d’inclusion proof RFC 9162 (section 2.1.3), avec les exigences suivantes :

  • lecture compatible des preuves legacy v1 (proof_version absent ou 1),
  • émission exclusive du format v2 (proof_version=2),
  • conservation de l’algorithme de hachage ProbatioVault (sha3-256),
  • absence de réécriture rétroactive des preuves déjà persistées.

2. Périmètre / Hors périmètre

Inclus

  • Définition normative de merkle_proof v2 dans ProofEnvelope.
  • Règles de mapping v1 -> v2 (normalisation à la volée en lecture).
  • Discriminant de version (proof_version) et règles d’interprétation.
  • Règles de validation des formats, tailles et bornes numériques.
  • Applicabilité de l’algorithme RFC 9162 §2.1.3.2 adapté à SHA3-256.
  • Correction contractuelle de hashAlgorithm: 'SHA-256' vers hash_algorithm: 'sha3-256'.
  • Contractualisation de l’ordre merklePath v1 (bottom-to-top).

Exclu

  • Implémentation des fonctions applicatives (getMerkleProof(), verifyMerkleProof()) : hors périmètre.
  • Consistency proofs RFC 9162 §2.1.4 : hors périmètre.
  • Migration rétroactive des preuves v1 en base : hors périmètre.
  • Changement d’algorithme cryptographique vers SHA-256 : hors périmètre.
  • Modification structurelle des autres sous-objets ProofEnvelope (tsa_token, hsm_signature, blockchain_anchor, event_metadata) : hors périmètre.
  • Chiffrement d’artefacts cryptographiques temporaires : hors périmètre PD-294.

3. Définitions

Terme Définition
merkle_proof v1 Format legacy historique (merklePath, merkleRoot, leafIndex, treeSize, eventHash, etc.).
merkle_proof v2 Format cible RFC 9162-inspired (proof_version, leaf_index, tree_size, inclusion_path, merkle_root, hash_algorithm, event_hash).
Inclusion path Liste ordonnée des siblings de bas en haut (index 0 = niveau feuille, dernier = niveau proche racine).
proof_version Discriminant de format en entrée : absent ou 1 = v1 legacy, 2 = v2 explicite.
Dual-read / single-write Lecture v1 et v2 autorisée ; écriture/émission uniquement en v2 (proof_version=2).
RFC 9162 §2.1.3.2 Algorithme de vérification d’inclusion proof, ici appliqué avec SHA3-256.
État terminal État sans transition sortante autorisée.

4. Invariants (non négociables)

ID Règle Justification
INV-294-01-format-v2 Toute preuve émise DOIT contenir exactement proof_version, leaf_index, tree_size, inclusion_path, merkle_root, hash_algorithm, event_hash. Contrat v2 univoque.
INV-294-02-hash-algorithm hash_algorithm DOIT valoir strictement sha3-256. Alignement écosystème ProbatioVault.
INV-294-03-dual-read-single-write Le système DOIT lire v1 et v2, et DOIT émettre uniquement v2. Compatibilité descendante sans ambiguïté de sortie.
INV-294-04-no-retrowrite Les preuves v1 déjà persistées NE DOIVENT PAS être réécrites. Préservation de la valeur probatoire historique.
INV-294-05-discriminant En lecture : proof_version absent ou 1 => v1 ; proof_version=2 => v2 ; toute autre valeur => rejet. Détection de format déterministe, compat legacy explicite.
INV-294-06-rfc-verify-applicable Toute preuve v2 émise DOIT être vérifiable par RFC 9162 §2.1.3.2 adapté SHA3-256. Interopérabilité algorithmique.
INV-294-07-no-treeid-exposed treeId et hashAlgorithmVersion NE DOIVENT PAS apparaître dans merkle_proof v2 exposé. Éviter fuite de détails internes non normatifs.
INV-294-08-format-state-transitions La machine d’états de classification format (§5.4) DOIT être respectée ; toute transition non listée est interdite et DOIT lever ERR-294-11. Prévention des ambiguïtés de parsing.
INV-294-09-format-single-source Tous les formats de données DOIVENT être définis uniquement en §5.1 et référencés ailleurs. Anti-divergence documentaire.
INV-294-10-v1-path-order En entrée v1, merklePath DOIT être interprété bottom-to-top et mappé sans inversion vers inclusion_path. Contrat d’ordonnancement explicite v1.

5. Flux nominaux

5.1 Modèle de données (source de vérité unique)

Donnée canonique Format / encodage Taille / longueur Jeu de caractères Sensibilité casse Validation Comportement si invalide
proof_version entier JSON entrée: absent, 1, 2; sortie: 2 chiffres n/a entrée: absent/1 => v1, 2 => v2 ; sortie: =2 autre valeur: rejet 400 ERR-294-01
leaf_index entier signé 32-bit 0..(tree_size-1) chiffres n/a leaf_index >= 0 et < tree_size rejet 422 ERR-294-02
tree_size entier signé 32-bit 1..2147483647 chiffres n/a tree_size >= 1 et <= 2147483647 rejet 422 ERR-294-03
inclusion_path tableau JSON de hash hex 0..31 éléments [a-f0-9] case-sensitive chaque élément ^[a-f0-9]{64}$; length=0 si et seulement si tree_size=1 rejet 400 ERR-294-04
merkle_root hex lowercase 64 chars (32 bytes) [a-f0-9] case-sensitive ^[a-f0-9]{64}$ rejet 400 ERR-294-05
event_hash hex lowercase 64 chars (32 bytes) [a-f0-9] case-sensitive ^[a-f0-9]{64}$ rejet 400 ERR-294-06
hash_algorithm enum string 8 chars ASCII lowercase case-sensitive valeur stricte sha3-256 rejet 422 ERR-294-07
treeId (legacy interne) UUID v4 texte 36 chars [0-9a-fA-F-] case-insensitive (hex UUID) UUID v4 non exposé en v2 ; si exposé: rejet 500 ERR-294-08

Notes normatives :

  • Les champs legacy v1 (merklePath, merkleRoot, leafIndex, treeSize, eventHash, hashAlgorithm, hashAlgorithmVersion) ne sont acceptés qu’en entrée de normalisation.
  • L’ordre attendu de merklePath v1 est bottom-to-top et DOIT être conservé tel quel dans inclusion_path (aucune inversion).
  • La sortie contractuelle est toujours en nomenclature v2 (snake_case + proof_version=2).

5.2 Bornes numériques obligatoires

Paramètre Défaut Min Max Unité Contexte de référence Percentile Comportement hors bornes
proof_version (sortie) 2 2 2 version API backend n/a rejet émission (erreur interne)
proof_version (entrée) n/a absent/1/2 absent/1/2 version parsing contrat n/a rejet 400 ERR-294-01
tree_size n/a (valeur source obligatoire) 1 2147483647 feuilles schéma persistance Merkle (INTEGER) n/a rejet 422 ERR-294-03
leaf_index n/a (valeur source obligatoire) 0 tree_size-1 index preuve d’inclusion n/a rejet 422 ERR-294-02
inclusion_path.length n/a (dérivé de l’arbre) 0 31 éléments arbre binaire, tree_size max int32 n/a rejet 400 ERR-294-04
longueur hash (event_hash, merkle_root, élément inclusion_path) 64 64 64 caractères hex SHA3-256 n/a rejet 400

Justification contractuelle tree_size int32 vs RFC 9162 uint64 :

  1. RFC 9162 autorise tree_size sur 64 bits non signés.
  2. Le périmètre PD-294 conserve la contrainte de persistance existante (INTEGER PostgreSQL signé 32 bits).
  3. La borne contractuelle est donc 1..2^31-1 (2147483647) feuilles.
  4. Toute valeur au-delà est hors contrat PD-294 et rejetée par ERR-294-03.
  5. Un passage à BIGINT nécessite une évolution dédiée (hors périmètre PD-294).

5.3 SLA temporels

Aucune transition temporelle identifiée.

5.4 Machine d’états de classification format (avec transitions retour)

Règle de classification initiale :

Condition d’entrée État initial
proof_version absent ou 1 LEGACY_V1_DETECTED
proof_version=2 CANONICAL_V2_READY
autre valeur REJECTED_INVALID (ERR-294-01)

Transitions sortantes :

État source État cible Statut Code si interdit
LEGACY_V1_DETECTED CANONICAL_V2_READY AUTORISÉE n/a
LEGACY_V1_DETECTED REJECTED_INVALID AUTORISÉE n/a
LEGACY_V1_DETECTED LEGACY_V1_DETECTED AUTORISÉE n/a
CANONICAL_V2_READY CANONICAL_V2_READY AUTORISÉE n/a
CANONICAL_V2_READY REJECTED_INVALID AUTORISÉE n/a
CANONICAL_V2_READY LEGACY_V1_DETECTED INTERDITE ERR-294-11
REJECTED_INVALID * INTERDITE (état terminal) ERR-294-11

Comportement au retour/downgrade :

  • Données existantes : conservées, aucune suppression.
  • Quotas/limites : sans objet.
  • Re-verrouillage fonctionnalités : sans objet.
  • Toute tentative de transition interdite DOIT retourner 409 ERR-294-11.

5.5 Mapping v1 -> v2 (normalisation contractuelle)

Source v1 Cible v2 Règle
merklePath inclusion_path renommage sans inversion ; ordre attendu bottom-to-top conservé
merkleRoot merkle_root renommage
leafIndex leaf_index renommage
treeSize tree_size renommage
eventHash event_hash renommage
hashAlgorithm hash_algorithm forçage strict vers sha3-256
proof_version absent ou 1 proof_version sortie forcée à 2
treeId supprimé du payload exposé
hashAlgorithmVersion supprimé du payload exposé

5.6 Flux nominal A — Lecture d’une preuve v2 native

  1. Le système lit la preuve source.
  2. Le système détecte proof_version=2.
  3. Le système valide les formats §5.1.
  4. Le système retourne merkle_proof inchangé (v2 canonique).

5.7 Flux nominal B — Lecture d’une preuve legacy v1

  1. Le système lit la preuve source avec proof_version absent ou proof_version=1.
  2. Le système classe la preuve en LEGACY_V1_DETECTED.
  3. Le système applique le mapping §5.5.
  4. Le système valide les contraintes v2 §5.1.
  5. Le système retourne merkle_proof v2 (proof_version=2) sans écrire en base.

5.8 Flux nominal C — Vérification RFC 9162 §2.1.3.2 adaptée SHA3-256

Primitives :

  • HASH(x) = SHA3-256(x)
  • NODE_HASH(left, right) = HASH(0x01 || left || right)
  • LSB(x) = x & 1
  • hexToBytes() pour décoder les hex 64 en 32 bytes

Algorithme normatif :

Entrées: leaf_index, tree_size, event_hash, inclusion_path[], merkle_root

1. if leaf_index >= tree_size:
     FAIL(ERR-294-02)

2. fn = leaf_index
   sn = tree_size - 1

3. r = hexToBytes(event_hash)
   Convention PD-294: event_hash est le leaf-hash stocké.

4. for each p_hex in inclusion_path:
     p = hexToBytes(p_hex)

     a. if sn == 0:
          FAIL(ERR-294-09)

     b. if (LSB(fn) == 1) OR (fn == sn):
          i. r = NODE_HASH(p, r)
          ii. while (LSB(fn) == 0):
                fn = fn >> 1
                sn = sn >> 1
                if fn == 0:
                    break

     c. else:
          i. r = NODE_HASH(r, p)

     d. fn = fn >> 1
        sn = sn >> 1

5. if (sn != 0) OR (r != hexToBytes(merkle_root)):
     FAIL(ERR-294-09)

6. SUCCESS

Règles additionnelles :

  • inclusion_path.length = 0 est valide uniquement si tree_size = 1.
  • Toute divergence (sn==0 dans boucle, sn!=0 final, racine non égale) retourne ERR-294-09.

5.9 Stratégie de migration DDL

Aucune migration DDL requise dans PD-294 : la borne tree_size reste contractuellement alignée sur le type INTEGER existant.

5.10 Atomicité multi-composant

Scope Synchrone/Async Garantie
Lecture DB de la preuve Synchrone Cohérence de lecture transactionnelle
Normalisation v1 -> v2 Synchrone (mémoire) Déterministe, sans effet de bord persistant
Vérification cryptographique Synchrone (calcul) Résultat déterministe sur même entrée
Crash pré-réponse n/a Aucune mutation persistée
Crash post-réponse n/a Aucun rattrapage requis (flux sans écriture)

5.11 Mécanismes de protection distribuée

Aucun mécanisme de protection distribuée applicable (module synchrone mono-instance).

5.12 Contraintes inter-modules

Aucune contrainte inter-module applicable.

5bis. Diagrammes (si applicable)

Diagramme d’état

stateDiagram-v2
    [*] --> LEGACY_V1_DETECTED: proof_version absent|1
    [*] --> CANONICAL_V2_READY: proof_version=2
    [*] --> REJECTED_INVALID: autre proof_version (ERR-294-01)

    LEGACY_V1_DETECTED --> CANONICAL_V2_READY: normalisation v1->v2 (lecture)
    LEGACY_V1_DETECTED --> REJECTED_INVALID: format legacy invalide
    LEGACY_V1_DETECTED --> LEGACY_V1_DETECTED: relecture idempotente

    CANONICAL_V2_READY --> CANONICAL_V2_READY: relecture idempotente
    CANONICAL_V2_READY --> REJECTED_INVALID: contrainte invalide
    CANONICAL_V2_READY --> LEGACY_V1_DETECTED: INTERDITE (ERR-294-11)

    REJECTED_INVALID --> LEGACY_V1_DETECTED: INTERDITE (ERR-294-11)
    REJECTED_INVALID --> CANONICAL_V2_READY: INTERDITE (ERR-294-11)

Diagramme de séquence

sequenceDiagram
    participant V as Verifier
    participant API as Proof API (NestJS)
    participant DB as vault_merkle (PostgreSQL)
    participant N as Normalizer
    participant C as SHA3-256 Engine

    V->>API: GET proof(event_id)
    API->>DB: SELECT event_hash, merkle_root, leaf_index, tree_size, inclusion_proof, proof_version
    DB-->>API: record (v1 proof_version absent|1 OU v2 proof_version=2)

    API->>N: classify+normalize(record)
    alt record v1
        N-->>API: v2{proof_version=2, inclusion_path=merklePath, hash_algorithm='sha3-256'}
    else record v2
        N-->>API: v2 inchangé
    end

    API-->>V: ProofEnvelope.merkle_proof(v2)
    V->>C: fn=leaf_index, sn=tree_size-1, r=event_hash
    loop pour chaque p in inclusion_path
        V->>C: if sn==0 => invalid
        alt (LSB(fn)=1) ou (fn==sn)
            V->>C: r=SHA3-256(0x01 || p || r)
        else
            V->>C: r=SHA3-256(0x01 || r || p)
        end
        loop while (LSB(fn)=0) et (fn!=0)
            V->>C: fn >>= 1 ; sn >>= 1
        end
        V->>C: fn >>= 1 ; sn >>= 1
    end
    V->>C: check sn==0 and r==merkle_root
    C-->>V: valid / invalid(ERR-294-09)

6. Cas d’erreur

Code Cas Réponse contractuelle
ERR-294-01 proof_version présent et non supporté (not in {1,2}) HTTP 400
ERR-294-02 leaf_index hors borne ou leaf_index >= tree_size HTTP 422
ERR-294-03 tree_size < 1 ou tree_size > 2147483647 HTTP 422
ERR-294-04 inclusion_path invalide (type, longueur, élément non hex64, incohérence length=0/tree_size) HTTP 400
ERR-294-05 merkle_root invalide HTTP 400
ERR-294-06 event_hash invalide HTTP 400
ERR-294-07 hash_algorithm != 'sha3-256' HTTP 422
ERR-294-08 fuite de champ interne interdit (treeId, hashAlgorithmVersion) en payload v2 HTTP 500
ERR-294-09 échec vérification RFC 9162 adaptée (sn==0 en boucle, sn!=0 final, ou r != merkle_root) HTTP 422
ERR-294-10 preuve absente pour l’identifiant demandé HTTP 404
ERR-294-11 transition de format interdite dans la machine d’états (§5.4) HTTP 409

7. Critères d’acceptation (testables)

ID Critère Observable
CA-294-01 Toute preuve émise contient exactement les 7 champs v2. Contrôle JSON schema v2 PASS.
CA-294-02 hash_algorithm vaut toujours sha3-256. Aucun payload v2 avec autre valeur.
CA-294-03 Une preuve v1 (proof_version absent ou 1) lue est restituée en v2 sans réécriture DB. Réponse v2 + absence de mutation persistance.
CA-294-04 proof_version absent/1 => v1, 2 => v2, autre valeur rejetée. Tests de parsing 4 cas.
CA-294-05 Les champs treeId et hashAlgorithmVersion n’apparaissent jamais en v2 exposé. Contrôle payload systématique PASS.
CA-294-06 Toutes les contraintes de format §5.1 sont appliquées. Tests négatifs par champ -> codes ERR-294-* attendus.
CA-294-07 leaf_index et tree_size respectent les bornes §5.2 (incluant max int32). Cas limites min/max PASS.
CA-294-08 inclusion_path.length respecte [0,31] et length=0 <=> tree_size=1. Cas 0, 31, 32 + cohérence tree_size PASS.
CA-294-09 L’algorithme RFC 9162 §2.1.3.2 adapté SHA3-256 (avec inner while-loop) valide un vecteur connu. valid=true sur vecteur positif.
CA-294-10 L’algorithme RFC 9162 adapté invalide une preuve altérée. valid=false / ERR-294-09.
CA-294-11 Aucune preuve v1 existante n’est réécrite par PD-294. Diff base avant/après sans update legacy.
CA-294-12 Vérification empirique H-294-02 : sur échantillon DB réel, SHA3-256 valide ; SHA-256 ne reproduit pas la racine attendue. Rapport de test comparatif PASS.
CA-294-13 Toute transition interdite de la machine d’états retourne ERR-294-11. Tests de transitions interdites PASS.
CA-294-14 merklePath v1 est interprété bottom-to-top sans inversion. Cas ordre normal PASS, ordre inversé FAIL (ERR-294-09).

8. Scénarios de test (Given / When / Then)

ID Given When Then
ST-294-01 preuve avec proof_version=2 et champs v2 valides lecture preuve réponse identique v2, validation §5.1 OK
ST-294-02 preuve legacy avec proof_version absent lecture preuve réponse normalisée v2 (proof_version=2) sans écriture DB
ST-294-03 preuve legacy avec proof_version=1 lecture preuve classée v1 puis normalisée v2 sans écriture DB
ST-294-04 preuve avec proof_version=3 lecture preuve rejet 400 ERR-294-01
ST-294-05 event_hash non conforme ^[a-f0-9]{64}$ lecture preuve rejet 400 ERR-294-06
ST-294-06 leaf_index=tree_size vérification preuve rejet 422 ERR-294-02
ST-294-07 tree_size=2147483648 lecture/vérification rejet 422 ERR-294-03
ST-294-08 inclusion_path de 32 éléments lecture/vérification rejet 400 ERR-294-04
ST-294-09 merkle_root invalide (non hex64) lecture/vérification rejet 400 ERR-294-05
ST-294-10 hash_algorithm='sha-256' lecture/vérification rejet 422 ERR-294-07
ST-294-11 preuve v2 cohérente (event_hash, inclusion_path, merkle_root) algo RFC 9162 §2.1.3.2 adapté exécuté valid=true
ST-294-12 preuve v2 dont un élément inclusion_path est altéré algo exécuté rejet 422 ERR-294-09
ST-294-13 réponse contenant treeId en merkle_proof validateur de contrat non-conformité 500 ERR-294-08
ST-294-14 aucune preuve pour event_id demandé lecture preuve rejet 404 ERR-294-10
ST-294-15 tentative de transition CANONICAL_V2_READY -> LEGACY_V1_DETECTED exécution machine d’états rejet 409 ERR-294-11
ST-294-16 échantillon DB réel + recalcul double mode SHA3/SHA-256 campagne de validation H-294-02 SHA3-256 conforme, SHA-256 non conforme
ST-294-17 preuve v1 correcte mais merklePath inversé (top-to-bottom) normalisation + vérification échec crypto 422 ERR-294-09

8.1 Matrice de couverture des erreurs

Code erreur Scénario(s) de couverture
ERR-294-01 ST-294-04
ERR-294-02 ST-294-06
ERR-294-03 ST-294-07
ERR-294-04 ST-294-08
ERR-294-05 ST-294-09
ERR-294-06 ST-294-05
ERR-294-07 ST-294-10
ERR-294-08 ST-294-13
ERR-294-09 ST-294-12, ST-294-17
ERR-294-10 ST-294-14
ERR-294-11 ST-294-15

9. Hypothèses explicites

ID Hypothèse Validation testable Impact si faux
H-294-01 Le stockage source expose tree_size et leaf_index pour chaque preuve exploitable. Vérifié par tests d’intégration lecture preuve Vérification RFC impossible, rejet massif des preuves.
H-294-02 Le calcul Merkle amont est réellement en SHA3-256 malgré labels legacy ambigus. ST-294-16 + CA-294-12 Incompatibilité racine/preuve si l’amont est SHA-256.
H-294-03 Le domaine métier actuel n’excède pas 2^31-1 feuilles par arbre. Vérification volumétrie prod + monitoring Bornes §5.2 insuffisantes, évolution BIGINT à planifier.
H-294-04 Le transport externe des erreurs supporte les codes ERR-294-*. Tests contractuels API Mapping erreur non aligné, observabilité affaiblie.
H-294-05 Le besoin d’exposer explicitement l’origine legacy au client n’est pas requis. Validation PO/API review Si requis, contrat de sortie à amender.

10. Points à clarifier

10.1 Contraintes techniques (stack réelle)

Élément Valeur contractuelle
Projet cible ProbatioVault-backend
Langage TypeScript
Framework backend NestJS
ORM TypeORM
Base de données PostgreSQL
Schéma métier concerné vault_merkle (références PD-237/PD-56)

10.2 Questions ouvertes

ID Point à clarifier Impact
Q-294-01 Confirmation convention HTTP 409 pour ERR-294-11 (transition interdite). Risque d’écart avec conventions backend si 422 attendu.
Q-294-02 Plan d’évolution formel vers BIGINT si besoin futur tree_size > 2^31-1. Risque de rejet fonctionnel sur très grands arbres.
Q-294-03 Le placeholder d’injection {{LEARNINGS}} est non renseigné. Risque de manquer des contraintes historiques spécifiques.
Q-294-04 Référence épique textuelle métier non fournie dans le besoin (seul chemin technique disponible). Métadonnées de traçabilité incomplètes.

Références

  • Epic : ProbatioVault-backend/docs/epics/probatoire/PD-294-merkle-rfc9162
  • JIRA : PD-294
  • Repos concernés : ProbatioVault-backend
  • Documents associés :
  • RFC 9162 (sections 2.1.3, 2.1.3.2, 4.12)
  • PD-54 — Construction arbre Merkle
  • PD-55 — Worker ancrage blockchain
  • PD-56 — Generation Merkle proof
  • PD-237 — Persistance Merkle
  • PD-245 — Format preuve multi-chain
  • PD-282 — ProofEnvelope HSM/eIDAS

Note d’exécution : sandbox en lecture seule, donc fichier non écrit sur disque dans ce run.

---SPEC-END---