Aller au contenu

PD-56 — Plan d'implémentation

1. Découpage en composants

# Composant Responsabilité Fichiers Dépend de
C1 ProofAvailabilityState (enum + helpers) Enum PENDING/AVAILABLE/CORRUPTED, matrice de transitions, guard isValidTransition() src/modules/merkle/enums/proof-availability-state.enum.ts
C2 MerkleLeaf entity (extension) Ajout colonne proof_availability_state sur vault_merkle.merkle_leaves src/modules/merkle/entities/merkle-leaf.entity.ts + migration C1
C3 MerkleProofService Orchestrateur principal : getMerkleProof(eventId) — résolution, validation, auto-vérification, machine d'état, construction MerkleProofResult src/modules/merkle/services/merkle-proof.service.ts C1, C2, C4, C5, C6, C7, C8, C9
C4 EventResolverService Résolution eventId → eventHash via anchor_batch_events (table PD-55) src/modules/merkle/services/event-resolver.service.ts
C5 ProofAvailabilityStateMachine Gestion transitions d'état + persistance atomique + détection première corruption src/modules/merkle/services/proof-availability-state-machine.service.ts C1, C2
C6 MerkleProofAuditService Émission SECURITY_ALERT severity=CRITICAL sur première corruption, anti-flood src/modules/merkle/services/merkle-proof-audit.service.ts
C7 MerkleProofResult DTO Type discriminé available | pending, validation format §5.1 src/modules/merkle/dto/merkle-proof-result.dto.ts
C8 MerkleProofException Codes contractuels ERR-56-01..05 src/modules/merkle/exceptions/merkle-proof.exception.ts
C9 ETA Calculator Calcul estimatedAvailableAt UTC depuis window_end batch candidat src/modules/merkle/services/eta-calculator.service.ts
C10 Migration DDL ALTER TABLE vault_merkle.merkle_leaves ADD COLUMN proof_availability_state + index + default PENDING src/database/migrations/XXXXXXXXX-PD56-AddProofAvailabilityState.ts
C11 Tests unitaires + intégration Couverture TC-NOM-, TC-ERR-, TC-INV-, TC-NEG-, TC-NR-* src/modules/merkle/__tests__/merkle-proof*.spec.ts C1–C9
C12 Script vérification off-chain Script JS standalone pour CA-56-03 scripts/verify-merkle-proof-offchain.ts

2bis. Diagramme de dépendances agents

graph LR
    subgraph "Wave 1 — Fondations"
        A1[C1: ProofAvailabilityState enum]
        A7[C7: MerkleProofResult DTO]
        A8[C8: MerkleProofException]
        A10[C10: Migration DDL]
        A9[C9: ETA Calculator]
    end

    subgraph "Wave 2 — Services intermédiaires"
        A2[C2: MerkleLeaf extension]
        A4[C4: EventResolver]
        A5[C5: StateMachine]
        A6[C6: ProofAuditService]
    end

    subgraph "Wave 3 — Orchestrateur"
        A3[C3: MerkleProofService]
    end

    subgraph "Wave 4 — Validation"
        A11[C11: Tests]
        A12[C12: Script off-chain]
    end

    A1 --> A2
    A1 --> A5
    A10 --> A2
    A7 --> A3
    A8 --> A3
    A4 --> A3
    A5 --> A3
    A6 --> A3
    A9 --> A3
    A2 --> A3
    A3 --> A11
    A3 --> A12

4 waves d'exécution. Wave 1 : 5 agents en parallèle. Wave 2 : 4 agents (C4/C6 indépendants, C2/C5 dépendent de C1). Wave 3 : 1 agent (intégration). Wave 4 : 2 agents (tests + script off-chain).

2. Flux techniques

F-01 — Preuve disponible (available)

getMerkleProof(eventId)
  ├─ 1. Valider format eventId (UUID v4 regex) → sinon ERR-56-01
  ├─ 2. EventResolverService.resolve(eventId) → eventHash
  │     └─ Lookup anchor_batch_events.event_id → event_hash (PD-55)
  │     └─ Si introuvable → ERR-56-01
  ├─ 3. MerkleLeaf.findOne({ leafHash: eventHash }) + join MerkleTree
  │     └─ Si leaf introuvable → flux pending (F-02/F-05)
  ├─ 4. Lire proofAvailabilityState
  │     └─ Si CORRUPTED → ERR-56-03 (pas de transition, pas d'alerte)
  ├─ 5. Validation structurelle (merklePath format, bornes §5.1/§5.2)
  │     └─ Si invalide → F-06 (corruption structurelle)
  ├─ 6. Auto-vérification : MerkleProofVerifier.verifyProof(eventHash, merklePath, merkleRoot)
  │     └─ Si mismatch → F-04 (corruption crypto)
  ├─ 7. Vérifier finalized_at IS NOT NULL (via AnchorBatch lié)
  │     └─ Si NULL → flux pending (F-02/F-05)
  ├─ 8. Retourner MerkleProofResult available (AUCUNE écriture — l'état proof_availability est déduit du contexte, pas persisté en nominal)
  └─ 9. Construire MerkleProofResult { status:'available', ... }

F-02 — Preuve en attente (pending)

  ... (après étape 3 ou 7, leaf absent ou finalized_at IS NULL)
  ├─ Lookup AnchorBatch candidat (le plus récent contenant eventId — NOTE: le filtre sur batch FAILED est un point ouvert §10.2 Q2, le plan sélectionne tout batch contenant l'eventId sans filtrage de statut)
  ├─ Si window_end IS NOT NULL → estimatedAvailableAt = window_end (UTC Z)
  │   └─ Retour { status:'pending', estimatedAvailableAt }
  └─ Si window_end IS NULL → ERR-56-05

F-04 — Mismatch cryptographique

  ... (étape 6 : computedRoot != merkleRoot)
  ├─ ProofAvailabilityStateMachine.transitionTo(CORRUPTED, eventHash)
  │   └─ UPDATE merkle_leaves SET proof_availability_state = 'CORRUPTED'
  │      WHERE leaf_hash = :eventHash AND proof_availability_state != 'CORRUPTED'
  │   └─ Si rows affected > 0 → première détection
  ├─ Si première détection → MerkleProofAuditService.emitSecurityAlert(eventId, 'ERR-56-04')
  └─ Retourner ERR-56-04

F-06 — Corruption structurelle

  ... (étape 5 : structure invalide)
  ├─ Même logique que F-04 mais code ERR-56-03
  └─ ProofAvailabilityStateMachine.transitionTo(CORRUPTED) + alerte si première détection

Diagramme de séquence enrichi

sequenceDiagram
    participant PES as ProofEnvelopeService
    participant MPS as MerkleProofService
    participant ERS as EventResolverService
    participant DB_ABE as vault_blockchain.anchor_batch_events
    participant DB_ML as vault_merkle.merkle_leaves
    participant DB_MT as vault_merkle.merkle_trees
    participant DB_AB as vault_blockchain.anchor_batches
    participant MPV as MerkleProofVerifier
    participant SM as ProofAvailabilityStateMachine
    participant AUD as MerkleProofAuditService

    PES->>MPS: getMerkleProof(eventId: UUID)
    MPS->>MPS: validateEventIdFormat(eventId)
    alt eventId invalide
        MPS-->>PES: ERR-56-01
    end

    MPS->>ERS: resolve(eventId)
    ERS->>DB_ABE: SELECT event_hash FROM anchor_batch_events WHERE event_id = $1
    alt inexistant
        DB_ABE-->>ERS: null
        ERS-->>MPS: null
        MPS-->>PES: ERR-56-01
    else trouvé
        DB_ABE-->>ERS: eventHash
        ERS-->>MPS: eventHash
    end

    MPS->>DB_ML: SELECT * FROM merkle_leaves WHERE leaf_hash = $1
    alt leaf introuvable
        MPS->>DB_AB: SELECT window_end FROM anchor_batches JOIN anchor_batch_events ON ... WHERE event_id = $1
        alt window_end IS NULL
            MPS-->>PES: ERR-56-05
        else window_end calculable
            MPS-->>PES: {status:'pending', estimatedAvailableAt}
        end
    else leaf trouvé
        DB_ML-->>MPS: {leafHash, leafIndex, inclusionProof, proofAvailabilityState, treeId}
        alt state = CORRUPTED
            MPS-->>PES: ERR-56-03 (sans alerte)
        end

        MPS->>DB_MT: SELECT merkle_root, leaf_count FROM merkle_trees WHERE tree_id = $1
        DB_MT-->>MPS: {merkleRoot, leafCount}

        MPS->>MPS: validateStructure(inclusionProof, leafCount)
        alt structure invalide
            MPS->>SM: transitionTo(CORRUPTED)
            SM->>DB_ML: UPDATE proof_availability_state = 'CORRUPTED'
            SM-->>MPS: {firstDetection: true}
            MPS->>AUD: emitSecurityAlert(CRITICAL, ERR-56-03)
            MPS-->>PES: ERR-56-03
        end

        MPS->>MPV: verifyProof(eventHash, inclusionProof, merkleRoot, 'SHA-256')
        MPV-->>MPS: {valid, computedRoot}

        alt computedRoot != merkleRoot
            MPS->>SM: transitionTo(CORRUPTED)
            SM-->>MPS: {firstDetection: true/false}
            opt firstDetection
                MPS->>AUD: emitSecurityAlert(CRITICAL, ERR-56-04)
            end
            MPS-->>PES: ERR-56-04
        end

        MPS->>DB_AB: SELECT finalized_at, window_end FROM anchor_batches WHERE tree_id = $1
        alt finalized_at IS NULL
            alt window_end IS NULL
                MPS-->>PES: ERR-56-05
            else
                MPS-->>PES: {status:'pending', estimatedAvailableAt: window_end}
            end
        else finalized_at IS NOT NULL
            Note over MPS: Pas d'écriture — état déduit, non persisté en nominal
            MPS-->>PES: {status:'available', eventHash, merkleRoot, merklePath, treeId, treeSize, leafIndex, hashAlgorithm:'SHA-256', hashAlgorithmVersion:'1.0'}
        end
    end

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-56-01-root-source-of-truth merkleRoot retourné = valeur persistée Lecture directe merkle_trees.merkle_root, aucune transformation/recalcul C3 (MerkleProofService) Assertion result.merkleRoot === tree.merkleRoot dans tests Faible — lecture seule
INV-56-02-determinisme Même eventId + même snapshot → même résultat Lectures dans même transaction (READ COMMITTED suffit car pas d'écriture concurrente sur leaf) ; aucun aléa dans le flux C3 TC-NOM-03 : deux appels identiques → égalité structurelle Faible
INV-56-03-minimal-disclosure Seuls champs contractuels exposés DTO MerkleProofResult avec whitelist explicite de propriétés, pas de spread operator C7 (DTO) TC-NOM-08 : vérification stricte des clés du payload Faible — contrôlé par typage
INV-56-04-algo-explicit hashAlgorithm='SHA-256' + hashAlgorithmVersion='1.0' présents quand available Constantes littérales dans le constructeur du DTO available C7 TC-NOM-01 : assertion présence et valeur exacte Faible
INV-56-05-auto-verification Vérification computeRoot(eventHash, merklePath) == merkleRoot avant retour available Appel MerkleProofVerifier.verifyProof() (existant PD-237), résultat vérifié avant construction réponse C3 + MerkleProofVerifier (existant) TC-NOM-05 : trace de vérification ; TC-ERR-08 : mismatch → ERR-56-04 Moyen — dépend du verifier existant ; roundtrip test obligatoire
INV-56-06-transitions Machine d'état PENDING/AVAILABLE/CORRUPTED respectée ProofAvailabilityStateMachine avec matrice de transitions, isValidTransition() garde, UPDATE ... WHERE state != 'CORRUPTED' atomique C5 (StateMachine) TC-INV-06 : toutes transitions ; TC-ERR-09 : AVAILABLE→PENDING refusé ; TC-ERR-10 : CORRUPTED terminal Moyen — concurrence sur UPDATE
INV-56-07-format-single-source Formats définis uniquement en §5.1 Constantes et regex centralisées dans merkle-proof-result.dto.ts ; toute validation référence ces constantes C7 TC-INV-07 : campagne format/bornes exhaustive Faible
INV-56-08-no-secret-cleartext Aucun secret en clair persisté Le flux manipule uniquement des hashes hex et métadonnées ; revue de code + TC-INV-08 C3, C7 Inspection payloads + traces Faible — flux lecture seule de hashes
INV-56-09-security-alert-on-first-corruption-detection SECURITY_ALERT CRITICAL uniquement à la première transition vers CORRUPTED UPDATE ... WHERE state != 'CORRUPTED' RETURNING * : si rowCount > 0 → première détection → émettre alerte ; sinon pas d'alerte C5 + C6 TC-NOM-11 : émission unique ; TC-ERR-10 : anti-flood vérifié Moyen — atomicité UPDATE + alerte

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-56-01 Flux F-01 complet : résolution → validation → vérification → construction DTO available C3, C4, C7 Payload conforme §5.1, tous champs présents Faible
CA-56-02 MerkleProofVerifier.verifyProof() + tri lexicographique hashPair(sorted(a,b)) MerkleProofVerifier (existant) TC-NOM-04 : recalcul externe == merkleRoot Faible
CA-56-03 Script standalone verify-merkle-proof-offchain.ts (JS) avec SHA-256 pur, sans API/BDD C12 TC-NOM-04 : exécution script réussie Faible
CA-56-04 JSON.stringify(result).length < 10240 vérifié dans tests perf C7 TC-NOM-09 : P95 taille < 10 KB Faible — payload borné par merklePath.length ≤ 20
CA-56-05 Benchmark getMerkleProof() sur dataset figé, P95 ≤ 10 ms C3 TC-NOM-10 : rapport perf horodaté Moyen — dépend de l'env benchmark
CA-56-06 Flux F-02 : construction DTO pending avec estimatedAvailableAt UTC Z C3, C9 TC-NOM-02 : payload pending conforme RFC3339 Faible
CA-56-07 Appel verifyProof() systématique avant tout retour available, tracé dans logs C3 TC-NOM-05 : trace décision vérification Faible
CA-56-08 ProofAvailabilityStateMachine avec matrice explicite + tests exhaustifs des transitions C5 TC-INV-06 : toutes transitions ; TC-ERR-09 : interdictions Faible
CA-56-09 Validation centralisée format/bornes §5.1/§5.2 + cas limite treeSize=1/merklePath=[] C3, C7 TC-INV-07 : campagne exhaustive ; TC-NOM-12 : cas unitaire Faible
CA-56-10 Gate finalized_at IS NOT NULL obligatoire avant transition PENDING→AVAILABLE C3 TC-NOM-06 : pending malgré vérification OK si non finalisé Faible
CA-56-11 Flux F-04 : computedRoot != merkleRoot → persist CORRUPTED + ERR-56-04 C3, C5 TC-ERR-08 : trace DB + réponse Moyen
CA-56-12 État terminal CORRUPTED : pas de transition sortante, pas d'alerte C5 TC-ERR-10 : état inchangé, 0 alerte Faible
CA-56-13 finalized_at IS NULL AND window_end IS NULL → ERR-56-05 C3, C9 TC-ERR-03 : réponse d'erreur dédiée Faible
CA-56-14 SECURITY_ALERT severity=CRITICAL à la première transition vers CORRUPTED C5, C6 TC-NOM-11 : capture événement audit Moyen
CA-56-15 Anti-flood : pas de SECURITY_ALERT sur relectures état CORRUPTED C5, C6 TC-ERR-10 + TC-NOM-11 : compteur alertes = 1 Faible

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau de test visé
TC-NOM-01 INV-56-01, INV-56-04, CA-56-01 F-01 complet, DTO available Payload JSON, assertion champs Unit + Integration
TC-NOM-02 F-02, CA-56-06 F-02, ETA Calculator Payload pending, format RFC3339 Z Unit + Integration
TC-NOM-03 INV-56-02 Deux appels séquentiels + deux concurrents, même snapshot Égalité structurelle champ-à-champ Integration
TC-NOM-04 F-03, CA-56-02, CA-56-03 Script off-chain SHA-256, hashPair(sorted(a,b)) computedRoot == merkleRoot Unit (script standalone)
TC-NOM-05 INV-56-05, CA-56-07 Trace appel verifyProof avant réponse available Logger spy / trace corrélée Unit
TC-NOM-06 §5.4, CA-56-10 Finality gate finalized_at Réponse pending malgré preuve valide Unit + Integration
TC-NOM-07 §5.3 ETA recalcul sur fenêtre suivante estimatedAvailableAt mis à jour Unit
TC-NOM-08 INV-56-03 DTO whitelist propriétés Object.keys(result) strict Unit
TC-NOM-09 CA-56-04 JSON.stringify().length P95 < 10240 bytes Perf
TC-NOM-10 CA-56-05 Mesure latence getMerkleProof() P95 ≤ 10 ms Perf
TC-NOM-11 INV-56-09, CA-56-14, CA-56-15 Première corruption → alerte, relectures → 0 alerte Compteur SECURITY_ALERT Unit + Integration
TC-NOM-12 §5.1, CA-56-09 treeSize=1, merklePath=[] accepté status='available' Unit
TC-ERR-01 ERR-56-01 Regex UUID v4 Rejet, code ERR-56-01 Unit
TC-ERR-02 ERR-56-01 Lookup eventId inexistant Rejet, code ERR-56-01 Unit + Integration
TC-ERR-03 ERR-56-05 window_end IS NULL Rejet, code ERR-56-05 Unit + Integration
TC-ERR-04 F-06, INV-56-06, INV-56-09 Corruption structurelle → CORRUPTED + alerte Trace DB, SECURITY_ALERT Unit + Integration
TC-ERR-05 ERR-56-03 Hash non conforme Rejet, code ERR-56-03 Unit
TC-ERR-06 ERR-56-03 treeSize/leafIndex hors bornes Rejet, code ERR-56-03 Unit
TC-ERR-07 ERR-56-03 algo/version non conformes Rejet, code ERR-56-03 Unit
TC-ERR-08 ERR-56-04, INV-56-05 computedRoot != merkleRoot → CORRUPTED Trace DB + réponse ERR-56-04 Unit + Integration
TC-ERR-09 §5.4, INV-56-06 AVAILABLE→PENDING refusé Exception / état conservé Unit
TC-ERR-10 §5.4, INV-56-06, INV-56-09 CORRUPTED terminal, 0 alerte ERR-56-03, état inchangé, 0 alerte Unit + Integration
TC-ERR-11 §5.4, ERR-56-04 AVAILABLE→CORRUPTED sur mismatch Trace DB, ERR-56-04, puis ERR-56-03 Integration
TC-INV-06 INV-56-06, CA-56-08 Toutes transitions autorisées/interdites Matrice exhaustive Unit
TC-INV-07 INV-56-07, CA-56-09 Campagne format/bornes §5.1/§5.2 Rapport Unit
TC-INV-08 INV-56-08 Inspection payloads + persistance Aucun secret en clair Unit (revue)
TC-NEG-01..14 §7 négatifs/adversariaux Validation entrée + anti-flood Rejets explicites Unit + Integration
TC-NR-01..07 §6 non-régression Baseline vs candidate Stabilité contrats Integration

6. Gestion des erreurs

Code Condition de déclenchement Mécanisme Observable
ERR-56-01 eventId ne matche pas regex UUID v4, ou événement inexistant en DB Validation en entrée de getMerkleProof() + lookup EventResolver retourne null Exception avec code ERR-56-01, aucune donnée de preuve
ERR-56-02 Événement dans batch non finalisé avec ETA calculable Flux F-02, retour structuré {status:'pending', estimatedAvailableAt} Pas d'exception, réponse nominale pending
ERR-56-03 (a) Données Merkle structurellement invalides lors de première détection → transition CORRUPTED. (b) État déjà CORRUPTED → lecture seule (a) validateStructure() échoue → transitionTo(CORRUPTED) + SECURITY_ALERT. (b) Lecture proofAvailabilityState == CORRUPTED → rejet immédiat Exception ERR-56-03, trace DB
ERR-56-04 computedRoot != merkleRoot à l'auto-vérification verifyProof() retourne valid: falsetransitionTo(CORRUPTED) + SECURITY_ALERT Exception ERR-56-04, transition persistée
ERR-56-05 Pending sans ETA : finalized_at IS NULL AND window_end IS NULL Flux F-05, vérification dans ETACalculator Exception ERR-56-05

Propagation : toutes les exceptions sont des MerkleProofException (extends Error) avec un code contractuel. Pas de catch silencieux (cf. anti-catch-absorb). L'appelant (ProofEnvelopeService) reçoit l'exception telle quelle.

7. Impacts sécurité

Risque Mitigation Composant
Énumération d'eventId via distinction 404 types Message uniforme ERR-56-01 pour eventId invalide ET inexistant (anti-énumération) C3, C8
Flood SECURITY_ALERT Anti-flood : UPDATE WHERE state != 'CORRUPTED' RETURNING * — rowCount=0 → pas d'alerte C5, C6
Injection SQL via eventId Paramétrage TypeORM (prepared statements), validation UUID v4 regex en entrée C3, C4
Exposition données métier DTO whitelist strict (MerkleProofResult), aucun spread, aucun payload brut C7
Secret en clair Flux manipule uniquement hashes hex et métadonnées publiques — aucun secret cryptographique C3
Timing attack sur comparaison root MerkleProofVerifier utilise constantTimeCompare() (existant PD-237) MerkleProofVerifier
Concurrence sur transition CORRUPTED UPDATE ... WHERE proof_availability_state != 'CORRUPTED' atomique (row-level lock PostgreSQL) C5

8. Hypothèses techniques

ID Hypothèse Impact si faux
HT-56-01 La table anchor_batch_events (PD-55) contient une colonne permettant de résoudre eventId → eventHash (soit event_hash directe, soit jointure vers table source) Résolution eventId→eventHash impossible ; nécessite ajout colonne ou jointure alternative
HT-56-02 MerkleProofVerifier.computeRootFromProof() (PD-237) utilise le tri lexicographique sorted(a,b) compatible avec la spec PD-56 §5.3 F-03 Incompatibilité vérification off-chain ; nécessite adaptateur ou refactoring verifier
HT-56-03 anchor_batches.window_end est nullable (NULL si batch encore en collecte) pour distinguer F-02 de F-05 Si non-nullable, la distinction F-02/F-05 nécessite un autre indicateur
HT-56-04 anchor_batches.tree_id permet la jointure vers merkle_trees pour accéder à finalized_at La gate de finalité requiert un chemin de jointure alternatif
HT-56-05 Ajout colonne proof_availability_state sur merkle_leaves est acceptable (pas de conflit PD-237) Migration DDL en conflit ; résolution via coordination
HT-56-06 anchor_batches.finalized_at est nullable timestamptz (confirmé par entity PD-55 ligne 140) — (vérifié)

9. Points de vigilance (risques, dette, pièges)

  1. Résolution eventId → eventHash (critique) : l'entity AnchorBatchEvent (PD-55) expose eventId (UUID) mais pas de colonne eventHash visible. Si la colonne n'existe pas, la résolution passera par une jointure vers la table source métier de l'événement probatoire. À valider en Go/No-Go avant implémentation.

  2. Convention de tri dans computeRootFromProof() : le code PD-237 mentionne sorted(a,b) dans un commentaire mais aussi un mécanisme basé sur l'index de feuille. Vérifier la cohérence exacte avec la spec PD-56 §5.3 (tri lexicographique pur hashPair(sorted(a,b))).

  3. Migration DDL : utiliser varchar avec CHECK constraint plutôt qu'un type ENUM PostgreSQL natif, conformément au REX PD-282. Séquence migration : (1) ALTER TABLE ADD COLUMN proof_availability_state VARCHAR DEFAULT 'PENDING' (2) commitTransaction() (3) CREATE INDEX (4) ALTER TABLE ADD CONSTRAINT CHECK. La migration DOIT appeler commitTransaction() AVANT toute clause WHERE utilisant la nouvelle valeur (REX PD-282, PD-279). Default 'PENDING' est non-volatile → pas de table rewrite (PostgreSQL 11+).

  4. Anti-flood multi-instance : Conformément à INV-56-09, SECURITY_ALERT DOIT être émis une seule fois par eventId. Utiliser SELECT ... FOR UPDATE dans une transaction pour garantir l'unicité même en multi-instance. Pattern : BEGIN → SELECT leaf FOR UPDATE → CHECK state != 'CORRUPTED' → UPDATE state = 'CORRUPTED' → EMIT SECURITY_ALERT → COMMIT. Si state = 'CORRUPTED' déjà, retourner ERR-56-03 SANS alerte.

  5. Performance P95 ≤ 10 ms : le flux nominal nécessite 2-3 queries SQL séquentielles (event lookup, leaf lookup, tree/batch lookup). Avec les index existants (idx_merkle_leaves_hash, idx_anchor_batch_event_event_id), c'est réaliste. Optimisation possible : jointure unique leaf+tree en une query.

  6. proofAvailabilityState initial : la migration met default 'PENDING' sur les leaves existantes. C'est correct car les leaves existantes n'ont pas encore été vérifiées par PD-56. La première invocation de getMerkleProof() déterminera l'état réel.

  7. Cas treeSize=1 avec merklePath=[] : cas valide contractuel (§5.1). Le MerkleProofVerifier.verifyProof() avec proof=[] doit retourner valid: true si leaf == root. À vérifier dans l'implémentation existante.

9bis. Contraintes techniques

Contrainte Source Détail
Framework test Existant backend Jest (jest.config.ts existant dans le repo, pas Vitest)
Module system Existant CJS (CommonJS) — le backend ProbatioVault utilise "module": "commonjs" dans tsconfig
Variables CI Pipeline GitLab DATABASE_URL (PostgreSQL test), CI=true — documentées dans .gitlab-ci.yml
VARCHAR + CHECK REX PD-282 Pas d'ENUM PostgreSQL natif pour proof_availability_state — VARCHAR + CHECK constraint
Sémantique d'écriture Spec §5.7 Flux majoritairement lecture. Écriture UNIQUEMENT sur première détection corruption (transition vers CORRUPTED). Aucune écriture en flux nominal (pas de persistance de transition PENDING→AVAILABLE)
Anti-flood Spec INV-56-09 SECURITY_ALERT émis UNIQUEMENT lors de la première détection (rowCount > 0 sur UPDATE WHERE state != 'CORRUPTED'). Les appels sur état CORRUPTED existant retournent ERR-56-03 SANS alerte

Dépendances inter-PD

PD Statut Fourniture Consommation PD-56
PD-54 DONE Construction arbre Merkle, hashPair(sorted(a,b)) MerkleProofVerifier.verifyProof(), computeRootFromProof()
PD-55 DONE Tables anchor_batch_events, anchor_batches Résolution eventId → leafHash via C4
PD-237 DONE Tables merkle_trees, merkle_leaves (6 colonnes) Lecture arbres + feuilles. Note : PD-237 ne fournit PAS proof_availability_state → migration PD-56 requise (C10)

10. Hors périmètre

  • Exposition REST endpoint (pas de route HTTP — service interne uniquement).
  • Construction d'arbre Merkle (PD-54).
  • Ancrage blockchain (PD-55).
  • Persistance Merkle structurelle initiale (PD-237) — PD-56 consomme et met à jour l'état uniquement.
  • Mécanismes de transport d'observabilité (outbox, bus, retry).
  • Auto-résolution de CORRUPTED (résolution manuelle uniquement).
  • Rate-limiting (pas d'endpoint REST).
  • Intégration ProofEnvelopeService (consommateur PD-56, pas producteur).

11. Mécanismes cross-module

Aucune modification d'autres modules. PD-56 est un service interne consommé par ProofEnvelopeService. Il lit les données de : - vault_merkle.merkle_leaves et vault_merkle.merkle_trees (PD-237) — lecture + mise à jour proof_availability_state - vault_blockchain.anchor_batch_events et vault_blockchain.anchor_batches (PD-55) — lecture seule

Pas de guard cross-route, pas de middleware, pas d'intercepteur sur d'autres modules.

12. Périmètre de test

Niveau de test In scope Hors scope (justification)
Unitaire C1 (enum), C3 (service avec mocks DB), C5 (state machine), C6 (audit), C7 (DTO), C8 (exceptions), C9 (ETA)
Intégration C3 + C4 + C5 + DB réelle (merkle_leaves, merkle_trees, anchor_batches)
E2E Flux complet getMerkleProof(eventId) avec données persistées par PD-237 + PD-55 Hors scope si fixtures PD-55/PD-237 non disponibles — utiliser fixtures snapshot
Perf TC-NOM-09 (taille), TC-NOM-10 (latence) Opposabilité CA-56-05 liée à benchmark officiel non encore figé
Sécurité TC-NEG-01 (injection), TC-NEG-14 (flood), TC-INV-08 (no secret) Pentest complet hors scope PD-56

Tous les niveaux de test sont couverts, avec réserve sur l'opposabilité perf (benchmark officiel).