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
ProofEnvelopeindividuels - L'export
.pvproofcontenant 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
ProofEnvelopeServiceexistant (appel interne) - Format de sortie custom ProbatioVault (pas RFC 6962/9162)
- Gestion du cas "batch en cours" : statut
pendingavec 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 :
- Récupérer l'événement par
eventId - Calculer ou récupérer son hash canonique (leafHash)
- 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¶
- 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. - 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). - Service interne : Pas d'endpoint REST dans cette story.
getMerkleProofest appelé parProofEnvelopeService. - 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)retourneProofResultDto - 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+hashAlgorithmVersionsont obligatoires dans la réponse.