Aller au contenu

PD-56 — Implémenter la génération de preuve de Merkle (Merkle Proof)

1. Contexte

ProbatioVault agrège périodiquement les événements du journal probatoire dans des arbres de Merkle (PD-54 construction, PD-55 ancrage, PD-237 persistance). Les tables merkle_trees et merkle_leaves (schema vault_merkle) stockent les arbres en append-only.

Un MerkleProofVerifier et un getProofByLeaf(leafHash) existent déjà, mais il manque la fonction métier clé : à partir d'un eventId, retrouver l'arbre correspondant et produire une preuve Merkle complète prête à être incluse dans un ProofEnvelope.

2. Problème

Actuellement, pour prouver qu'un événement appartient à un arbre ancré, il faut connaître le leafHash exact et interroger manuellement le service Merkle. Aucune fonction ne fait le lien eventId → leafHash → arbre → preuve. Cette brique est indispensable pour :

  • La génération automatique de ProofEnvelope individuels
  • L'export .pvproof contenant la preuve Merkle
  • La vérification par un tiers (expert judiciaire, auditeur) sans accès à l'infrastructure

3. Besoin utilisateur

En tant que service ProofEnvelope, je veux appeler getMerkleProof(eventId) et obtenir une preuve Merkle complète, afin de l'inclure dans le ProofEnvelope d'un événement pour permettre sa vérification indépendante off-chain.

4. Périmètre

Inclus

  • Fonction getMerkleProof(eventId) : résolution eventId → leafHash → inclusion proof
  • Intégration dans le ProofEnvelopeService existant (appel interne)
  • Format de sortie custom ProbatioVault (pas RFC 6962/9162)
  • Gestion du cas "batch en cours" : statut pending avec ETA estimée
  • Vérification off-chain possible avec script externe

Exclus

  • Endpoint REST (story séparée)
  • Construction de l'arbre Merkle (PD-54 DONE)
  • Ancrage blockchain (PD-55 DONE)
  • Persistance de l'arbre (PD-237 DONE)

5. Fonctionnalités attendues

5.1 getMerkleProof(eventId): MerkleProofResult

Retourne :

interface MerkleProofResult {
  status: 'available' | 'pending';
  // Si available :
  eventHash: string;         // SHA-256 hex lowercase (leafHash)
  merkleRoot: string;        // SHA-256 hex lowercase (64 chars)
  merklePath: string[];      // Hashes ordonnés leaf-to-root (format existant merkle_leaves.inclusion_proof)
  treeId: string;            // UUID de l'arbre
  treeSize: number;          // Nombre de feuilles (leaf_count)
  leafIndex: number;         // Position dans l'arbre (zero-indexed)
  hashAlgorithm: string;     // 'SHA-256' (conforme hash_algorithm_id existant)
  hashAlgorithmVersion: string; // '1.0'
  // Si pending :
  estimatedAvailableAt?: Date; // ETA basée sur le prochain batch window_end
}

5.2 Résolution eventId → leafHash

Le lien entre un eventId et son leafHash passe par le hash canonique de l'événement (même algorithme que celui utilisé par PD-54 pour construire les feuilles). Le service doit :

  1. Récupérer l'événement par eventId
  2. Calculer ou récupérer son hash canonique (leafHash)
  3. Chercher ce leafHash dans merkle_leaves

5.3 Vérification off-chain

La preuve retournée doit permettre :

computedRoot = fold(eventHash, merklePath)  // via hashPair() déterministe existant
assert(computedRoot === merkleRoot)

Cette vérification doit être réalisable sans accès API, sans accès BDD, avec un simple script JS/Python utilisant SHA-256.

6. Cas d'erreur

Code Description
ERR-56-01 eventId inconnu (événement inexistant)
ERR-56-02 Événement non encore inclus dans un batch Merkle → retourner status: 'pending' avec ETA
ERR-56-03 Arbre Merkle corrompu (inclusion_proof absent ou vide)
ERR-56-04 Incohérence hash : la racine recalculée ne correspond pas au merkleRoot stocké

7. Invariants de sécurité

ID Invariant
INV-56-01 Le merkleRoot retourné DOIT correspondre exactement à celui stocké dans merkle_trees (pas de recalcul)
INV-56-02 La preuve DOIT être déterministe : deux appels avec le même eventId produisent la même preuve
INV-56-03 La preuve ne DOIT révéler aucun autre événement (seuls les hashes intermédiaires sont exposés, pas les leafHash des siblings)
INV-56-04 L'algorithme de hash DOIT être explicitement indiqué dans la réponse (SHA-256, version 1.0)
INV-56-05 Auto-vérification : avant de retourner la preuve, le service DOIT vérifier que computedRoot === merkleRoot via MerkleProofVerifier existant

8. Critères d'acceptation

ID Critère
CA-56-01 getMerkleProof(eventId) retourne un MerkleProofResult avec status available pour un événement inclus dans un arbre finalisé
CA-56-02 La racine reconstruite à partir de eventHash + merklePath correspond au merkleRoot retourné
CA-56-03 Un script externe (JS ou Python, SHA-256) peut vérifier la preuve sans accès à ProbatioVault
CA-56-04 La taille de la preuve reste < 10 KB pour un arbre de 1M événements (log2(1M) ≈ 20 hashes × 64 chars = ~1.3 KB)
CA-56-05 La génération est < 10 ms (lookup BDD + assemblage, pas de recalcul d'arbre)
CA-56-06 Pour un événement dans un batch non finalisé, retourne status: 'pending' avec estimatedAvailableAt
CA-56-07 L'auto-vérification (INV-56-05) est exécutée à chaque appel avant retour

9. Arbitrages et clarifications PO

  1. Hash SHA-256 (pas SHA3-256) : Contrairement à la description Jira initiale qui mentionnait SHA3-256, l'implémentation existante (PD-54, PD-237) utilise SHA-256 (Node.js crypto.createHash('sha256')). Le besoin s'aligne sur l'existant. SHA3-256 est utilisé ailleurs (HashService pour documents, SHA3-384 pour envelope sealing) mais PAS pour les arbres Merkle.
  2. Format custom : Pas de conformité RFC 6962/9162 requise. Le format suit la structure existante (inclusion_proof = string[] ordonnés leaf-to-root, position encodée par tri lexicographique).
  3. Service interne : Pas d'endpoint REST dans cette story. getMerkleProof est appelé par ProofEnvelopeService.
  4. Statut pending : Pour ERR-56-02, retourner un objet avec status: 'pending' et une ETA (plutôt qu'une erreur sèche), permettant au consommateur de réessayer.

10. Dépendances

PD Relation Statut
PD-54 Construction arbre Merkle (fournit leafHash) DONE
PD-55 Worker ancrage blockchain (ancre le merkleRoot) DONE
PD-237 Persistance arbre (tables merkle_trees, merkle_leaves) DONE
PD-187 Epic parent BLOCKCHAIN En cours

11. Contraintes techniques existantes

  • Schema PostgreSQL : vault_merkle (append-only, triggers de protection)
  • Entités TypeORM : MerkleTree, MerkleLeaf
  • Service existant : MerkleTreeService.getProofByLeaf(leafHash) retourne ProofResultDto
  • Vérificateur existant : MerkleProofVerifier.verifyProof() + computeRootFromProof()
  • Hash pairing : tri lexicographique déterministe (hashPair() dans MerkleProofVerifier)
  • Max 10 000 feuilles par arbre (MAX_LEAF_COUNT)

12. Learnings injectés

  • [PD-55] : Les specs blockchain/crypto passent difficilement les gates sans formalisme strict (canonicalisation, formats) → spécifier explicitement le format de preuve et l'algorithme de hash dès le besoin.
  • [PD-282] : createVerify(algo) ajoute un hash implicite en Node.js → pour cette story, utiliser les primitives existantes (MerkleProofVerifier.hashPair()) sans introduire de nouvelle couche crypto.
  • [PD-54] : Séparation primitives crypto vs persistance validée ; identifiants normatifs versionnés essentiels → le hashAlgorithm + hashAlgorithmVersion sont obligatoires dans la réponse.