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: false → transitionTo(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)¶
-
Résolution eventId → eventHash (critique) : l'entity
AnchorBatchEvent(PD-55) exposeeventId(UUID) mais pas de colonneeventHashvisible. 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. -
Convention de tri dans
computeRootFromProof(): le code PD-237 mentionnesorted(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 purhashPair(sorted(a,b))). -
Migration DDL : utiliser
varcharavec 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 appelercommitTransaction()AVANT toute clause WHERE utilisant la nouvelle valeur (REX PD-282, PD-279). Default'PENDING'est non-volatile → pas de table rewrite (PostgreSQL 11+). -
Anti-flood multi-instance : Conformément à INV-56-09, SECURITY_ALERT DOIT être émis une seule fois par eventId. Utiliser
SELECT ... FOR UPDATEdans 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. Sistate = 'CORRUPTED'déjà, retourner ERR-56-03 SANS alerte. -
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. -
proofAvailabilityStateinitial : 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 degetMerkleProof()déterminera l'état réel. -
Cas
treeSize=1avecmerklePath=[]: cas valide contractuel (§5.1). LeMerkleProofVerifier.verifyProof()avec proof=[] doit retournervalid: truesi 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).