PD-237 — Plan d'implementation
1. Objectif
Implementer la persistance backend des arbres de Merkle conformement a la specification PD-237, en tant que consommateur de la primitive cryptographique PD-54.
Le backend doit : - Recevoir et valider des arbres de Merkle clos - Persister les racines, preuves d'inclusion et metadonnees de maniere atomique - Garantir l'immutabilite (append-only) et l'integrite probatoire - Exposer des API de recuperation par tree_id, par feuille ou par racine
2. Choix techniques retenus
| Decision | Justification |
| NestJS + TypeORM | Stack backend existante, coherence avec PD-13/PD-14 |
| PostgreSQL | Support natif UUID, JSONB, CHECK constraints, transactions ACID |
Schema dedie vault_merkle | Isolation des donnees probatoires, separation des responsabilites |
| Triggers SQL pour append-only | Enforcement au niveau BDD (defense en profondeur) |
| crypto.createHash (Node.js) | Verification des preuves d'inclusion SHA-256 |
| class-validator + custom pipes | Validation stricte des payloads entrants |
| EventEmitter2 | Emission d'evenements d'audit pour ERR-MK-07 |
3. Architecture ciblee
┌─────────────────────────────────────────────────────────────────┐
│ API Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ POST /merkle │ │ GET /merkle/:id │ │ GET /merkle/ │ │
│ │ /trees │ │ │ │ proof/:leaf │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
└───────────┼─────────────────────┼─────────────────────┼─────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ MerkleTreeService │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ - validatePayload() - persistTree() ││
│ │ - verifyInclusionProofs() - getTreeById() ││
│ │ - validateHashFormat() - getProofByLeaf() ││
│ │ - getProofsByRoot() ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ MerkleProofVerifier │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ - verifyProof(leaf, proof, root, algorithm) ││
│ │ - computeHash(data, algorithm) ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Repository Layer │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ MerkleTreeRepository│ │ MerkleLeafRepository│ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
└─────────────┼─────────────────────────┼─────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL (schema: vault_merkle) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ merkle_trees │◄─┤ merkle_leaves │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ + TRIGGER prevent_update_delete (append-only) │
└─────────────────────────────────────────────────────────────────┘
4. Decoupage technique
4.1 Phase 1 : Schema et migrations
| Tache | Fichier | Observable |
| Creer schema vault_merkle | migrations/xxx-create-schema-vault-merkle.ts | Schema existe |
| Creer table merkle_trees | migrations/xxx-create-merkle-trees.ts | Table avec contraintes CHECK + UNIQUE anti-doublon |
| Creer table merkle_leaves | migrations/xxx-create-merkle-leaves.ts | Table avec FK et index |
| Trigger append-only | migrations/xxx-merkle-append-only-trigger.ts | DELETE/UPDATE rejetes |
Contrainte anti-doublon (multi-instance safe) :
ALTER TABLE merkle_trees
ADD CONSTRAINT uq_merkle_trees_identity
UNIQUE (merkle_root, window_start, window_end);
Cette contrainte garantit qu'un meme arbre (identifie par sa racine et sa fenetre temporelle) ne peut etre persiste qu'une seule fois, meme en deploiement multi-instance. 4.2 Phase 2 : Entites et DTOs
| Tache | Fichier | Observable |
| Entite MerkleTree | src/merkle/entities/merkle-tree.entity.ts | Mapping TypeORM |
| Entite MerkleLeaf | src/merkle/entities/merkle-leaf.entity.ts | Mapping TypeORM |
| DTO PersistTreeRequest | src/merkle/dto/persist-tree.dto.ts | Validation class-validator |
| DTO GetProofRequest | src/merkle/dto/get-proof.dto.ts | Validation leaf/merkle_root mutuellement exclusifs |
| DTO TreeResponse | src/merkle/dto/tree-response.dto.ts | Format de sortie |
| DTO ProofResponse | src/merkle/dto/proof-response.dto.ts | Format de sortie (feuille ou racine) |
4.3 Phase 3 : Verification cryptographique
| Tache | Fichier | Observable |
| MerkleProofVerifier | src/merkle/services/merkle-proof-verifier.ts | Verification unitaire |
| Support SHA-256 | Idem | Hash conforme PD-54 |
| Validation format hex | Idem | Regex ^[a-f0-9]{64}$ |
4.4 Phase 4 : Service metier
| Tache | Fichier | Observable |
| MerkleTreeService | src/merkle/services/merkle-tree.service.ts | Logique metier |
| Validation payload | Idem | Rejet ERR-MK-01..04 |
| Persistance atomique | Idem | Transaction TypeORM |
| Recuperation tree | Idem | getTreeById() |
| Recuperation proof par feuille | Idem | getProofByLeaf() |
| Recuperation proofs par racine | Idem | getProofsByRoot() |
4.5 Phase 5 : Controller et routes
| Tache | Fichier | Observable |
| MerkleController | src/merkle/merkle.controller.ts | Endpoints REST |
| POST /merkle/trees | Idem | Persistance |
| GET /merkle/trees/:id | Idem | Arbre complet |
| GET /merkle/proofs | Idem | Preuve par feuille ou par racine |
4.6 Phase 6 : Gestion des erreurs et audit
| Tache | Fichier | Observable |
| MerkleException | src/merkle/exceptions/merkle.exception.ts | Codes ERR-MK-xx |
| ExceptionFilter | src/merkle/filters/merkle-exception.filter.ts | Mapping HTTP |
| AuditService integration | src/merkle/services/merkle-tree.service.ts | Event ERR-MK-07 |
4.7 Phase 7 : Tests
| Tache | Fichier | Observable |
| Tests unitaires verifier | tests/unit/merkle/merkle-proof-verifier.spec.ts | TC-INV-02 |
| Tests unitaires service | tests/unit/merkle/merkle-tree.service.spec.ts | TC-NOM-, TC-ERR- |
| Tests integration | tests/integration/merkle/merkle.e2e.spec.ts | TC-INV-, TC-NR- |
| Tests adversariaux | tests/integration/merkle/merkle-adversarial.e2e.spec.ts | TC-NEG-* |
5. Mapping Invariants → Mecanismes
| Invariant | Mecanisme technique | Observable | Test(s) |
| INV-BE-01 : Pas d'evenement en clair | DTO n'accepte que des hashes (VARCHAR 64), pas de champ event_data | Inspection schema + payloads API | TC-INV-01, TC-NOM-05 |
| INV-BE-02 : Preuve identique | Stockage JSONB sans transformation, retour direct du champ inclusion_proof | Comparaison byte-to-byte via Buffer.compare() | TC-INV-02, TC-NOM-02 |
| INV-BE-03 : Metadonnees immuables | Trigger SQL BEFORE UPDATE sur merkle_trees → RAISE EXCEPTION | Tentative UPDATE rejetee en BDD | TC-INV-03 |
| INV-BE-04 : Rejet mismatch | Validation DTO + verification avant persistance, pas de PATCH/PUT | Code erreur ERR-MK-07 | TC-INV-04 |
| INV-BE-05 : Append-only | Trigger SQL BEFORE DELETE + BEFORE UPDATE → RAISE EXCEPTION | DELETE/UPDATE rejetes en BDD | TC-INV-05, TC-NR-01 |
6. Mapping Criteres d'acceptation → Mecanismes + Tests
| Critere | Mecanisme | Observable | Test(s) |
| CA-01 : Payload complet requis | Validation DTO (class-validator @IsNotEmpty, @IsArray, etc.) + transaction atomique avec rollback | Rejet + aucune donnee recuperable | TC-NOM-01, TC-ERR-01..04 |
| CA-02 : Preuve exacte | Pas de normalisation, stockage/retour brut | JSON.stringify(stored) === JSON.stringify(returned) | TC-NOM-02, TC-INV-02 |
| CA-03 : Pas de clair | Schema sans colonnes texte libre, uniquement hashes | Inspection colonnes via \d merkle_* | TC-NOM-05, TC-INV-01 |
| CA-04 : Rejet incoherence | Validation pre-persistance, codes ERR-MK-xx explicites | HTTP 400 + body erreur | TC-INV-04, TC-ERR-* |
| CA-05 : Immutabilite | Triggers SQL append-only, pas d'endpoint DELETE/PATCH | HTTP 405 ou trigger exception | TC-INV-05, TC-NEG-01/02 |
| CA-06 : Recherche multi-arbres | Requete SQL WHERE leaf_hash = ? sans restriction tree_id + ORDER BY tree_id ASC | Liste complete ordonnee | TC-NOM-04, TC-NR-02 |
| Perimetre §2.1 : Recuperation par racine | Requete SQL WHERE merkle_root = ? via getProofsByRoot() + ORDER BY leaf_index ASC | Toutes preuves d'un arbre | - |
7. Gestion des erreurs
7.1 Codes d'erreur contractuels
| Code | Situation | HTTP Status | Response body |
| ERR-MK-01 | merkle_root absent | 400 | { "code": "ERR-MK-01", "message": "Missing merkle_root" } |
| ERR-MK-02 | Format hash invalide | 400 | { "code": "ERR-MK-02", "message": "Invalid hash format for {field}" } |
| ERR-MK-03 | Metadonnees incompletes | 400 | { "code": "ERR-MK-03", "message": "Missing or incomplete batch_metadata" } |
| ERR-MK-04 | Cardinalite ou preuve invalide | 400 | { "code": "ERR-MK-04", "message": "Proof validation failed for leaf at index {i}" } |
| ERR-MK-05 | tree_id inexistant | 404 | { "code": "ERR-MK-05", "message": "Tree not found" } |
| ERR-MK-06 | Feuille non trouvee | 404 | { "code": "ERR-MK-06", "message": "Leaf not found in any tree" } |
| ERR-MK-07 | Tentative modification | 403 | { "code": "ERR-MK-07", "message": "Modification of persisted tree forbidden" } |
7.2 Audit pour ERR-MK-07
// Emission evenement audit
this.eventEmitter.emit('audit.security', {
type: 'MERKLE_MODIFICATION_ATTEMPT',
severity: 'HIGH',
payload: {
attempted_tree_id: treeId,
client_ip: request.ip,
timestamp: new Date().toISOString(),
},
});
8. Securite
| Mesure | Implementation | Observable |
| Append-only (BDD) | Triggers prevent_update_merkle_trees, prevent_delete_merkle_trees | Exception PostgreSQL si tentative |
| Append-only (API) | Pas d'endpoint DELETE/PATCH/PUT sur /merkle/trees | HTTP 405 Method Not Allowed |
| Anti-doublon (multi-instance) | Contrainte UNIQUE (merkle_root, window_start, window_end) | Rejet SQL si doublon |
| Validation hash | Regex ^[a-f0-9]{64}$ en DTO et CHECK constraint SQL | Rejet payload non conforme |
| Verification preuves | MerkleProofVerifier.verify() avant persistance | Rejet si proof invalide |
| tree_id en entree interdit | Validation DTO @IsEmpty() sur tree_id | Rejet ERR-MK-07 si present |
| Audit modifications | EventEmitter sur ERR-MK-07 | Log audit + alerte |
| Transactions atomiques | @Transaction() ou queryRunner.startTransaction() | Rollback complet si echec |
9. API Endpoints
9.1 POST /api/v1/merkle/trees
Request:
{
"merkle_root": "a1b2c3...",
"leaves": ["hash1", "hash2", ...],
"inclusion_proofs": [
["sibling1", "sibling2", ...],
...
],
"batch_metadata": {
"window_start": "2025-01-01T00:00:00Z",
"window_end": "2025-01-01T23:59:59Z",
"window_timezone": "Europe/Paris",
"hash_algorithm_id": "SHA-256",
"hash_algorithm_version": "1.0",
"canonicalization_id": "JSON-CANONICAL-V1",
"leaf_ordering_id": "TIMESTAMP-ASC"
}
}
Response (201):
{
"tree_id": "uuid",
"created_at": "2025-01-25T10:30:00Z"
}
9.2 GET /api/v1/merkle/trees/:tree_id
Response (200):
{
"tree_id": "uuid",
"merkle_root": "a1b2c3...",
"leaf_count": 42,
"leaves": [...],
"inclusion_proofs": [...],
"batch_metadata": {...},
"created_at": "..."
}
9.3 GET /api/v1/merkle/proofs
Recuperation de preuves par feuille ou par racine (spec §2.1).
Query parameters : - leaf (optionnel) : hash de la feuille recherchee - merkle_root (optionnel) : hash racine pour recuperer toutes les preuves d'un arbre - tree_id (optionnel) : restreint la recherche a un arbre specifique
Contraintes : - Au moins un parametre leaf ou merkle_root doit etre fourni - leaf et merkle_root sont mutuellement exclusifs (erreur 400 si les deux sont fournis)
Ordre des resultats (spec §5.2) : - Recherche par leaf : resultats ordonnes par tree_id ASC - Recherche par merkle_root : resultats ordonnes par leaf_index ASC
Exemples d'usage :
GET /api/v1/merkle/proofs?leaf=abc123...
GET /api/v1/merkle/proofs?leaf=abc123...&tree_id=uuid
GET /api/v1/merkle/proofs?merkle_root=def456...
Response (200) - Recherche par feuille :
{
"results": [
{
"tree_id": "uuid1",
"merkle_root": "...",
"inclusion_proof": [...],
"leaf_index": 5,
"batch_metadata": {...}
},
...
]
}
Response (200) - Recherche par racine :
{
"results": [
{
"tree_id": "uuid1",
"merkle_root": "def456...",
"leaf_hash": "aaa...",
"inclusion_proof": [...],
"leaf_index": 0,
"batch_metadata": {...}
},
{
"tree_id": "uuid1",
"merkle_root": "def456...",
"leaf_hash": "bbb...",
"inclusion_proof": [...],
"leaf_index": 1,
"batch_metadata": {...}
},
...
]
}
Erreurs : - ERR-MK-06 (404) : Aucune preuve trouvee pour la feuille ou la racine - 400 : Parametres leaf et merkle_root fournis simultanement - 400 : Aucun parametre leaf ou merkle_root fourni
10. Hypotheses d'implementation
| ID | Hypothese | Verification | Impact si faux |
| H-IMPL-01 | hash_algorithm_id = "SHA-256" est le seul supporte initialement | Config + validation | Ajouter factory pour autres algos |
| H-IMPL-02 | Taille max arbre = 10 000 feuilles | Stress test | Pagination ou streaming |
| H-IMPL-03 | inclusion_proof stocke en JSONB < 1MB | Mesure taille moyenne | Compression ou stockage externe |
| H-IMPL-04 | Le service appelant fournit des preuves deja calculees | Contract PD-54 | Rejet ERR-MK-04 si invalide |
Compatibilite multi-instance : Le design est compatible avec un deploiement multi-instance grace a : - Contrainte UNIQUE (merkle_root, window_start, window_end) → prevention des doublons - tree_id UUID DEFAULT gen_random_uuid() → pas de collision - Transactions ACID PostgreSQL → atomicite garantie - Contrainte UNIQUE (tree_id, leaf_index) → integrite des feuilles
11. Points de vigilance
11.1 Risques
| Risque | Probabilite | Impact | Mitigation |
| Performance sur gros arbres (10k+ feuilles) | Moyenne | Moyen | Pagination GET, batch INSERT |
| Taille JSONB inclusion_proofs | Faible | Moyen | Monitoring taille, alerte si > 500KB |
| Doublon en multi-instance | Faible | Faible | Contrainte UNIQUE (merkle_root, window_start, window_end) |
11.2 Dettes potentielles
| Dette | Raison | Remediation future |
| Un seul hash_algorithm_id supporte | Scope initial | Factory pattern extensible |
| Pas de pagination recuperation arbre | Arbres < 10k feuilles | Cursor-based pagination |
| Pas de compression preuves | Complexite vs gain | Evaluer si > 100k arbres |
11.3 Hypotheses fragiles
| Hypothese | Fragilite | Signal d'alerte |
| H-01 (arbres clos) | Non verifiable cote backend | N/A - confiance en PD-54 |
| H-02 (hash conforme) | Depend de l'appelant | Monitoring rejets ERR-MK-02 |
| H-03 (atomicite) | PostgreSQL garantit | Monitoring erreurs transaction |
12. Hors perimetre
| Element | Raison | Reference |
| Construction des arbres | Reserve a PD-54 | Spec §2.2 |
| Horodatage eIDAS | Autre User Story | Spec §2.2 |
| Ancrage blockchain | Autre User Story | Spec §2.2 |
| Replication/backup | Responsabilite infra | Spec §2.2 |
| Compression preuves | Optimisation future | Spec §2.2 |
| Pagination gros arbres | A clarifier (§11) | Spec §11 |
| Mapping GRPC | A clarifier (§11) | Spec §11 |
13. Estimation effort
| Phase | Complexite | Dependances |
| Phase 1 : Schema | Faible | PD-14 (TypeORM) |
| Phase 2 : Entites/DTOs | Faible | Phase 1 |
| Phase 3 : Verifier crypto | Moyenne | PD-54 (spec hashes) |
| Phase 4 : Service | Moyenne | Phase 2, 3 |
| Phase 5 : Controller | Faible | Phase 4 |
| Phase 6 : Erreurs/Audit | Faible | Phase 4, 5 |
| Phase 7 : Tests | Moyenne | Toutes phases |
14. Checklist pre-implementation
References
- Specification : PD-237-specification.md
- Tests : PD-237-tests.md
- Epic : PD-186 BACKEND CORE
- Dependance normative : PD-54
- Pattern de reference : PD-236