PD-177 — Plan d'implementation detaille
Version : 1.1 (corrections Gate 5 v1 — 8 ecarts resolus : NE-05-01 a NE-05-08) Date : 2026-02-23 Statut : DRAFT — En attente re-gate v2 Dependances : PD-52 (done), PD-55 (done), PD-53 (interface), PD-245 (interface)
Table des matieres
- Decoupage en composants
- Flux d'implementation
- Mapping invariants → mecanismes techniques
- Mapping criteres d'acceptation → mecanismes + tests
- Mapping erreurs → BlockchainErrorCode
- Contraintes techniques
- Securite
- Hypotheses d'implementation
- Points de vigilance
1. Decoupage en composants
1.1 Composants modifies (code existant)
| ID | Composant | Fichier source | Nature de la modification |
| C-01 | BlockchainErrorCode | src/modules/blockchain/constants/error-codes.ts | Ajout de 3 codes : INVALID_CUSTODY_MODE, SIGNATURE_FAILED, PROOF_LINK_INCOMPLETE (aliases ou nouveaux codes — voir §5) |
| C-02 | ConfirmationTracker | src/modules/blockchain/transaction/confirmation.tracker.ts | Timeout configurable par reseau (900s), remplacement du calcul MAX_POLLING_ATTEMPTS * POLLING_INTERVAL_MS |
| C-03 | AnchorBatch entity | src/modules/anchor/entities/anchor-batch.entity.ts | Ajout colonne signer_address (VARCHAR 42, nullable pre-migration) |
| C-04 | BlockchainAnchorProcessor | src/modules/anchor/processors/blockchain-anchor.processor.ts | Confirmation dynamique par reseau (suppression CONFIRMATION_BLOCKS=12 hardcode), ajout signer_address dans flow |
| C-05 | BlockchainAdapterService | src/modules/anchor/services/blockchain-adapter.service.ts | Retourner signer_address dans TransactionResult, propager network correctement dans waitForConfirmation |
| C-06 | TransactionService | src/modules/blockchain/transaction/transaction.service.ts | Ajout signer_address dans TransactionResult retourne |
| C-07 | blockchain.config.ts | src/modules/blockchain/blockchain.config.ts | Ajout confirmationTimeoutMs par reseau dans NetworkConfig |
| C-08 | TransactionResult (DTO) | src/modules/blockchain/dto/transaction.dto.ts | Ajout champ signerAddress?: string |
| C-09 | ProofArtifactDto | src/modules/anchor/dto/proof-artifact.dto.ts | Ajout champ optionnel blockchain: 'ethereum_l2' |
| C-10 | BlockchainModule | src/modules/blockchain/blockchain.module.ts | Export CustodyService pour permettre le gate S2 par PD-177 |
| C-11 | AnchorBatchService | src/modules/anchor/services/anchor-batch.service.ts | Propager signer_address lors de submitBatch, protection append-only (refus UPDATE/DELETE applicatif) |
| C-12 | anchor.constants.ts | src/modules/anchor/constants/anchor.constants.ts | Ajout NETWORK_CONFIRMATION_POLICY map chainId → |
1.2 Composants nouveaux
| ID | Composant | Fichier cible | Responsabilite |
| N-01 | CustodyModeGuard | src/modules/blockchain/custody/custody-mode.guard.ts | Gate PD-177 : refuse initialisation si mode != S2 |
| N-02 | SecretLeakInterceptor | src/modules/blockchain/security/secret-leak.interceptor.ts | Intercepte les reponses/logs contenant des patterns de secrets, fail-closed |
| N-03 | WalletOperationalService | src/modules/blockchain/wallet/wallet-operational.service.ts | Facade PD-177 : unicite wallet, restriction ancrage-only, delegation custody, journalisation |
| N-04 | AnchorProofValidator | src/modules/anchor/validators/anchor-proof.validator.ts | Validation du lien complet merkle_root ↔ tx_hash ↔ confirmation ; code erreur PROOF_LINK_INCOMPLETE |
| N-05 | WalletRecoveryService | src/modules/blockchain/wallet/wallet-recovery.service.ts | Procedure de reprise documentee et testable (FN-177-04) |
| N-06 | Migration AddSignerAddress | src/database/migrations/YYYYMMDDHHMMSS-PD177-AddSignerAddress.ts | ALTER TABLE anchor_batches ADD COLUMN signer_address VARCHAR(42) |
| N-07 | NetworkConfirmationPolicy | src/modules/blockchain/constants/network-confirmation-policy.ts | Map contractualisee : {137: {confirmations: 12, timeoutMs: 900000}, 42161: {confirmations: 30, timeoutMs: 900000}} |
| N-08 | AnchorExclusivityGuard | src/modules/blockchain/wallet/anchor-exclusivity.guard.ts | Garantit que seul le flux d'ancrage peut emettre des transactions (INV-177-03) |
1.3 Composants documentaires
| ID | Composant | Fichier cible | Contenu |
| D-01 | Procedure de reprise | docs/runbooks/wallet-recovery-procedure.md | Procedure de reprise versionnee (INV-177-18) |
| D-02 | Registre des rotations | docs/runbooks/wallet-rotation-log-template.md | Template de consignation de rotation (INV-177-19) |
2. Flux d'implementation
2.1 Phases d'implementation (ordre strict)
Phase 0 : Prerequis (pas de code, validation config)
└─ Verifier que la config existante supporte les valeurs PD-177
(confirmations Polygon=12, Arbitrum=30, timeout=900s)
Phase 1 : Fondations (aucune dependance inter-composants PD-177)
├─ [C-01] Ajout error codes dans BlockchainErrorCode
├─ [N-07] NetworkConfirmationPolicy (constantes)
├─ [C-07] Enrichir NetworkConfig avec confirmationTimeoutMs
└─ [C-08] Ajouter signerAddress dans TransactionResult DTO
Phase 2 : Schema et persistance
├─ [N-06] Migration AddSignerAddress
├─ [C-03] Enrichir AnchorBatch entity
└─ Validation locale migration (INV-177-20)
Phase 3 : Services core
├─ [N-01] CustodyModeGuard (gate S2)
├─ [N-02] SecretLeakInterceptor
├─ [C-02] ConfirmationTracker (timeout configurable)
├─ [C-06] TransactionService (retour signer_address)
├─ [N-03] WalletOperationalService
└─ [N-08] AnchorExclusivityGuard
Phase 4 : Integration ancrage
├─ [C-05] BlockchainAdapterService (signer_address + network)
├─ [C-04] BlockchainAnchorProcessor (confirmations dynamiques)
├─ [C-11] AnchorBatchService (signer_address, append-only)
├─ [C-09] ProofArtifactDto (champ blockchain)
└─ [N-04] AnchorProofValidator
Phase 5 : Continuite et resilience
├─ [N-05] WalletRecoveryService
└─ [D-01, D-02] Documentation operationnelle
Phase 6 : Wiring module
└─ [C-10] BlockchainModule exports + AnchorModule providers
2.2 Graphe de dependances
N-07 ──► C-07 ──► C-02 (timeout configurable utilise la config enrichie)
C-01 ──► N-02 (interceptor utilise les error codes)
C-01 ──► N-04 (proof validator utilise PROOF_LINK_INCOMPLETE)
C-08 ──► C-06 (TransactionService retourne le DTO enrichi)
C-08 ──► C-05 (Adapter retourne le DTO enrichi)
N-06 ──► C-03 (entity apres migration)
C-03 ──► C-11 (batch service ecrit signer_address)
N-01 ──► N-03 (WalletOperationalService utilise le guard)
N-03 ──► C-04 (processor delegue a WalletOperationalService)
2.3 Diagrammes Mermaid
2.3.1 Graphe de dependances des composants
graph TD
subgraph "Phase 1 — Fondations"
C01["C-01 BlockchainErrorCode"]
N07["N-07 NetworkConfirmationPolicy"]
C07["C-07 blockchain.config.ts"]
C08["C-08 TransactionResult DTO"]
end
subgraph "Phase 2 — Schema"
N06["N-06 Migration AddSignerAddress"]
C03["C-03 AnchorBatch entity"]
end
subgraph "Phase 3 — Services core"
N01["N-01 CustodyModeGuard"]
N02["N-02 SecretLeakInterceptor"]
C02["C-02 ConfirmationTracker"]
C06["C-06 TransactionService"]
N03["N-03 WalletOperationalService"]
N08["N-08 AnchorExclusivityGuard"]
end
subgraph "Phase 4 — Integration ancrage"
C05["C-05 BlockchainAdapterService"]
C04["C-04 BlockchainAnchorProcessor"]
C11["C-11 AnchorBatchService"]
C09["C-09 ProofArtifactDto"]
N04["N-04 AnchorProofValidator"]
end
subgraph "Phase 5 — Continuite"
N05["N-05 WalletRecoveryService"]
D01["D-01 Runbook reprise"]
D02["D-02 Registre rotations"]
end
subgraph "Phase 6 — Wiring"
C10["C-10 BlockchainModule"]
end
N07 --> C07
C07 --> C02
C01 --> N02
C01 --> N04
C08 --> C06
C08 --> C05
N06 --> C03
C03 --> C11
N01 --> N03
N03 --> C04
C06 --> C05
C05 --> C04
C11 --> C04
N08 --> N03
C04 --> C10
N03 --> C10
2.3.2 Sequence d'ancrage (flux principal)
sequenceDiagram
participant Worker as BlockchainAnchorProcessor<br/>(C-04)
participant Wallet as WalletOperationalService<br/>(N-03)
participant Guard as CustodyModeGuard<br/>(N-01)
participant ExclGuard as AnchorExclusivityGuard<br/>(N-08)
participant Adapter as BlockchainAdapterService<br/>(C-05)
participant TxService as TransactionService<br/>(C-06)
participant Custody as CustodyService<br/>(PD-52)
participant KMS as AWS KMS
participant Tracker as ConfirmationTracker<br/>(C-02)
participant BatchSvc as AnchorBatchService<br/>(C-11)
participant Validator as AnchorProofValidator<br/>(N-04)
Worker->>Wallet: submitMerkleRoot(merkleRoot, network)
Wallet->>Guard: assertMode(S2)
Guard-->>Wallet: OK
Wallet->>ExclGuard: assertAnchorContext()
ExclGuard-->>Wallet: OK
Wallet->>Adapter: submitMerkleRoot(merkleRoot, network)
Adapter->>TxService: sendAnchorTransaction(data, network)
TxService->>Custody: getStrategy().signTransaction(tx)
Custody->>KMS: kms:Sign(ECC_SECG_P256K1)
KMS-->>Custody: signature
Custody-->>TxService: signedTx
TxService-->>Adapter: TransactionResult{hash, signerAddress}
Adapter->>Tracker: track(txHash, network)
Note over Tracker: NetworkConfirmationPolicy<br/>Polygon: 12 blocs / 900s<br/>Arbitrum: 30 blocs / 900s
Tracker-->>Adapter: confirmation{blockNumber, confirmations}
Adapter-->>Wallet: TransactionResult enrichi
Wallet->>BatchSvc: submitBatch(merkleRoot, txHash, signerAddress, chainId)
Note over BatchSvc: Append-only, validation<br/>signerAddress non-null
BatchSvc-->>Wallet: AnchorBatch persiste
Wallet->>Validator: validate(merkleRoot, txHash, confirmation)
Validator-->>Wallet: OK (lien complet)
Wallet-->>Worker: ProofArtifactDto{blockchain: 'ethereum_l2'}
2.3.3 Sequence anti-fuite (SecretLeakInterceptor)
sequenceDiagram
participant Service as Service blockchain
participant Interceptor as SecretLeakInterceptor<br/>(N-02)
participant Channel as Canal sortie<br/>(log / API / trace)
participant Audit as AuditService
Service->>Interceptor: output (reponse ou log)
Interceptor->>Interceptor: scanForSecrets(data)
alt Pattern secret detecte
Interceptor->>Audit: logEvent('SECURITY_INCIDENT', ...)
Interceptor--xChannel: BLOQUE (throw SECRET_LEAK_DETECTED)
else Aucun pattern
Interceptor->>Channel: output transmis
end
3. Mapping invariants → mecanismes techniques
| Invariant | Enonce resume | Mecanisme technique | Observable (verification) |
| INV-177-01 | Un seul wallet actif | CustodyService.getStrategy() retourne une seule strategie ; WalletOperationalService (N-03) expose getOfficialAddress() qui delegue a cette unique strategie | Appels multiples a getOfficialAddress() retournent la meme adresse ; un seul CustodyStrategy actif dans le container NestJS |
| INV-177-02 | Transaction associee au wallet actif | TransactionService.sendAnchorTransaction() (C-06) lit walletService.getAddress() au moment de l'emission et l'ecrit dans TransactionResult.signerAddress | Chaque TransactionResult contient signerAddress ; le champ signer_address de anchor_batches correspond a l'adresse active au moment de submittedAt |
| INV-177-03 | Wallet exclusivement ancrage | AnchorExclusivityGuard (N-08) : interceptor sur les services de transaction qui verifie que le callsite est le flux d'ancrage. sendRawTransaction n'est pas expose. Le TransactionService n'a pas de methode publique generique | Grep du code : aucune methode publique sendRaw* ou sendTransaction generique ; le guard rejette tout appel non-ancrage |
| INV-177-04 | Champ blockchain dans preuve composite | ProofArtifactDto (C-09) enrichi avec blockchain: 'ethereum_l2' ; le passthrough tezos est assure par BlockchainAdapterService.submitMerkleRoot() (C-05) qui filtre en entree : si network !== 'polygon' && network !== 'arbitrum', le service retourne sans action (no-op explicite avec log BLOCKCHAIN_PASSTHROUGH). Les requetes tezos ne traversent jamais le flux PD-177. | Champ blockchain present et valeur ethereum_l2 dans tout artefact de preuve genere par PD-177 ; log BLOCKCHAIN_PASSTHROUGH emis pour toute requete tezos |
| INV-177-05 | Interface CustodyStrategy respectee | Aucune modification de l'interface (PD-52) ; WalletOperationalService (N-03) delegue aux 5 methodes existantes | Test de conformite : les 5 methodes (initialize, getAddress, signMessage, signTransaction, validateCapability) sont appelables sans rupture |
| INV-177-06 | Worker ne signe jamais directement | BlockchainAnchorProcessor (C-04) appelle blockchainService.submitMerkleRoot() qui delegue a CustodyService.getStrategy().signTransaction() — jamais de crypto.sign() dans le processor | Grep : aucun import crypto ni appel signTransaction directement dans le processor ; traçabilite des appels dans les mocks de test |
| INV-177-07 | Mode S2 uniquement en production PD-177 | CustodyModeGuard (N-01) : verifie config.custodyMode === 'S2' au demarrage, throw BlockchainError(INVALID_CUSTODY_MODE) sinon | Log d'initialisation : mode actif = S2 ; test avec S1/S3 → echec explicite |
| INV-177-08 | Pas de cle privee en clair | 1. SecretLeakInterceptor (N-02) scanne les outputs (logs, reponses API, errors) ; 2. BlockchainError.sanitizeContext() existant (PD-52) ; 3. La strategie S2 KMS ne manipule jamais de cle privee en local | TC-SEC-01 : analyse des logs apres cycle complet → zero match sur les patterns de secrets |
| INV-177-09 | Fail-closed sur detection de fuite | SecretLeakInterceptor (N-02) : si pattern secret detecte dans un output, throw BlockchainError(SECRET_LEAK_DETECTED) avant ecriture sur le canal + creation d'un evenement d'audit securite via AuditService | TC-SEC-02 : injection de secret dans un log → operation bloquee + incident securite cree avec timestamp |
| INV-177-10 | Compromission serveur insuffisante | Architecture S2 : la cle privee est dans AWS KMS (ECC_SECG_P256K1), jamais exportable. Le code applicatif ne detient que le kmsKeyId (ARN). La signature requiert un appel KMS authentifie (IAM role) | TC-SEC-03 : sans credentials IAM, signTransaction() echoue → aucune signature produite |
| INV-177-11 | crypto.randomUUID() pour identifiants PD-177 | Tout code nouveau PD-177 utilise crypto.randomUUID(). Verification statique : grep des fichiers PD-177 pour uuid / v4 / randomUUID | TC-177-16 : analyse statique + test runtime : chaque UUID genere par PD-177 est conforme UUIDv4 |
| INV-177-12 | Prefixe pv-test-* pour cles de test | Toute cle ephemere de test PD-177 est prefixee pv-test-. Convention appliquee dans les fixtures de test | TC-177-17 : enumeration des cles de test → toutes prefixees pv-test- |
| INV-177-13 | signer_address dans persistance | 1. Migration N-06 ajoute colonne signer_address VARCHAR(42) — nullable pour compatibilite avec les batches PD-55 existants ; 2. AnchorBatchService.submitBatch() ecrit toujours signer_address pour les nouveaux batches PD-177 — garantie applicative de non-nullite : le service valide signerAddress != null avant save() et throw BlockchainError(PROOF_LINK_INCOMPLETE) si absent ; 3. Champs minimaux : anchorId, merkleRoot, signerAddress, chainId, txHash, submittedAt (ISO 8601 UTC ms), status | TC-177-06 : lecture de l'entree append-only → tous les champs presents ; test specifique : submitBatch() sans signerAddress → erreur PROOF_LINK_INCOMPLETE |
| INV-177-14 | Liaison deterministe tx ↔ append-only | Chaque anchor_batches a un tx_id unique non-null quand status >= SUBMITTED ; la relation batch_id → tx_id est 1:1 | TC-177-11 : pour N ancrages, N entrees distinctes dans anchor_batches avec tx_id unique |
| INV-177-15 | Reconstruction tierce de la chaine de preuve | ProofArtifactDto (C-09) contient : merkle_root, tx_id, chain_id, block_number, events[].merkle_proof ; le tiers verifie sur un noeud public sans secret | TC-177-12 : le tiers reconstruit merkle_root depuis les feuilles et verifie tx_id on-chain |
| INV-177-16 | Confirmation par reseau | NetworkConfirmationPolicy (N-07) : {137 → 12 confirmations / 900s, 42161 → 30 confirmations / 900s} ; ConfirmationTracker (C-02) utilise cette politique ; au-dela du timeout → statut non-finalise | TC-177-05 : Polygon confirme a 12 blocks ; Arbitrum a 30 blocks ; TC-177-13 : transaction non confirmee dans le timeout → non finalisee |
| INV-177-17 | Echec explicite et traçable | Chaque erreur utilise BlockchainError avec un BlockchainErrorCode deterministe + journalisation via AuditService | TC-ERR-03/04/05 : chaque erreur produit un code, un log, et aucun faux succes |
| INV-177-18 | Procedure de reprise testee | WalletRecoveryService (N-05) + D-01 runbook ; un exercice produit un rapport horodate | TC-177-15 : execution du runbook → rapport avec timestamp + statut |
| INV-177-19 | Rotation preserve auditabilite | signer_address historique dans anchor_batches ; les preuves pre-rotation restent verifiables (adresse historique intacte) | TC-177-14 : preuves signees avant rotation verifiables avec l'ancienne adresse |
| INV-177-20 | Validation locale des migrations | La migration N-06 est validee localement avant integration : npm run migration:validate ; pas d'index partiel problematique | TC-177-18 : migration:validate + migration:run sans erreur PostgreSQL |
| INV-177-21 | Pas d'API BullMQ depreciees | Code PD-177 utilise getJobSchedulers() / removeJobScheduler() (non deprecie) ; jamais getRepeatableJobs / removeRepeatableByKey | TC-177-19 : grep du code PD-177 pour APIs depreciees → zero match |
4. Mapping criteres d'acceptation → mecanismes + tests
| CA | Enonce resume | Mecanisme(s) | Test(s) | Observable |
| CA-177-01 | Une seule adresse wallet active | WalletOperationalService.getOfficialAddress() delegue a CustodyService.getStrategy().getAddress() | TC-177-01 | Appels consecutifs retournent la meme adresse ; pas de second wallet dans le container |
| CA-177-02 | S2 aboutit, S1/S3 echouent | CustodyModeGuard (N-01) dans WalletOperationalService.initialize() ; throw INVALID_CUSTODY_MODE si mode != S2 | TC-177-02, TC-ERR-01 | Log d'initialisation S2 : OK ; S1/S3 : BlockchainError avec code INVALID_CUSTODY_MODE |
| CA-177-03 | Interface CustodyStrategy respectee | Aucune modification de l'interface PD-52 ; les 5 methodes sont testees via mock + contrat | TC-177-03 | Les 5 methodes executees sans erreur de signature TypeScript |
| CA-177-04 | Transaction d'ancrage retourne tx_hash non vide | TransactionService.sendAnchorTransaction() retourne TransactionResult.hash non vide | TC-177-04 | result.hash est un string de 66 caracteres (0x + 64 hex) |
| CA-177-05 | Confirmation par reseau dans la fenetre | ConfirmationTracker.track() avec NetworkConfirmationPolicy ; Polygon 12/900s, Arbitrum 30/900s | TC-177-05 | Transaction finalisee avec confirmations >= seuil et timestamp < timeout |
| CA-177-06 | Contenu append-only conforme et immutable | AnchorBatchService ecrit les champs obligatoires (INV-177-13) ; refus applicatif d'UPDATE/DELETE sur status=FINALIZED | TC-177-06 | Lecture : tous champs presents + horodatage ISO 8601 UTC ms ; tentative de modification → rejet |
| CA-177-07 | Preuve composite avec blockchain=ethereum_l2 | ProofArtifactDto.blockchain = 'ethereum_l2' ; tx_id coherent avec registre | TC-177-07 | Champ blockchain present et valeur correcte ; tx_id correspond au batch |
| CA-177-08 | Aucun secret en clair dans les logs | SecretLeakInterceptor (N-02) + BlockchainError.sanitizeContext() | TC-SEC-01 | Analyse exhaustive des logs applicatifs : zero pattern de cle privee |
| CA-177-09 | INSUFFICIENT_FUNDS → echec + alerte | TransactionService throw BlockchainError(INSUFFICIENT_FUNDS) ; l'erreur est propagee et journalisee | TC-ERR-03 | Code erreur INSUFFICIENT_FUNDS dans les logs ; alerte operationnelle emise ; pas de FINALIZED |
| CA-177-10 | GAS_PRICE_CEILING_EXCEEDED → echec + alerte | GasEstimatorService.assertUnderCeiling() throw BlockchainError(GAS_PRICE_CEILING_EXCEEDED) | TC-ERR-04 | Code erreur dans les logs ; transaction non emise ; alerte journalisee |
| CA-177-11 | Indisponibilite RPC → code erreur, pas de finalise | RpcProviderService failover puis BlockchainError(RPC_UNAVAILABLE) | TC-ERR-05 | Code RPC_UNAVAILABLE ; aucun batch marque FINALIZED |
| CA-177-12 | Reorg → statut non-finalise | ConfirmationTracker.handleReorgDetection() + checkAbandonedTransaction() → TRANSACTION_ABANDONED | TC-ERR-06 | Statut != FINALIZED ; evenement reorg trace dans les logs |
| CA-177-13 | Procedure de reprise documentee et testee | WalletRecoveryService (N-05) + runbook D-01 ; exercice produit rapport | TC-177-15 | Rapport de reprise avec timestamp, statut explicite (reussi/echoue) |
| CA-177-14 | UUID conformes crypto.randomUUID() | Analyse statique du code PD-177 ; tests runtime | TC-177-16 | Grep : pas de uuid.v4() ou equivalent ; UUIDs generes conformes au format |
| CA-177-15 | Prefixe pv-test-* pour cles de test | Convention dans les fixtures de test PD-177 | TC-177-17 | Enumeration des cles de test → toutes prefixees pv-test- |
| CA-177-16 | Migration valide localement | npm run migration:validate + npm run migration:run sur PostgreSQL local | TC-177-18 | Commandes executees sans erreur ; pas d'index partiel problematique |
| CA-177-17 | BullMQ sans API depreciees | Utilisation exclusive de getJobSchedulers(), removeJobScheduler() ; pas de getRepeatableJobs / removeRepeatableByKey | TC-177-19 | Grep du code PD-177 : zero API depreciee |
5. Mapping erreurs → BlockchainErrorCode
5.1 Resolution NE-01 (divergences spec vs code existant)
La spec PD-177 utilise des noms de codes d'erreur qui different du BlockchainErrorCode existant (PD-52). La strategie retenue est d'ajouter des aliases dans l'enum pour maintenir la compatibilite avec la spec sans casser le code existant.
| Code spec PD-177 | Code existant PD-52 | Decision | Justification |
INVALID_CUSTODY_MODE | CUSTODY_MODE_INVALID | Ajouter alias INVALID_CUSTODY_MODE = 'INVALID_CUSTODY_MODE' | Le code spec a une semantique differente : PD-177 refuse S1/S3, PD-52 refuse un mode non fonctionnel. Un alias clarifie le contexte. |
SIGNATURE_FAILED | SIGNATURE_VERIFICATION_FAILED | Ajouter alias SIGNATURE_FAILED = 'SIGNATURE_FAILED' | PD-52 couvre la verification post-signature ; PD-177 couvre l'echec de signature cote custody. Semantique distincte. |
SECRET_EXPOSURE_DETECTED | SECRET_LEAK_DETECTED | Reutiliser SECRET_LEAK_DETECTED | Semantique identique. Le test PD-177 verifie que SECRET_LEAK_DETECTED est emis. Le mapping dans les tests fait la correspondance. |
PROOF_LINK_INCOMPLETE | (n'existe pas) | Ajouter PROOF_LINK_INCOMPLETE = 'PROOF_LINK_INCOMPLETE' | Code manquant. Necessaire pour ERR-177-07. |
TRANSACTION_REORGED_OR_ABANDONED | TRANSACTION_REORG + TRANSACTION_ABANDONED | Ajouter TRANSACTION_REORGED_OR_ABANDONED = 'TRANSACTION_REORGED_OR_ABANDONED' | La spec fusionne les deux cas. Le code interne continue de distinguer reorg/abandon mais le code expose utilise le code fusionne. |
5.2 Tableau complet ERR → Code
| Cas d'erreur | BlockchainErrorCode utilise | Emetteur |
| ERR-177-01 | INVALID_CUSTODY_MODE (nouveau) | CustodyModeGuard (N-01) |
| ERR-177-02 | SIGNATURE_FAILED (nouveau) | WalletOperationalService (N-03) catch des erreurs custody |
| ERR-177-03 | INSUFFICIENT_FUNDS (existant) | TransactionService (C-06) |
| ERR-177-04 | GAS_PRICE_CEILING_EXCEEDED (existant) | GasEstimatorService (existant) |
| ERR-177-05 | RPC_UNAVAILABLE (existant) | RpcProviderService (existant) |
| ERR-177-06 | TRANSACTION_REORGED_OR_ABANDONED (nouveau) | ConfirmationTracker (C-02) |
| ERR-177-07 | PROOF_LINK_INCOMPLETE (nouveau) | AnchorProofValidator (N-04) |
| ERR-177-08 | SECRET_LEAK_DETECTED (existant) | SecretLeakInterceptor (N-02) |
5.3 Modification de error-codes.ts
// Ajouts PD-177 dans BlockchainErrorCode
export enum BlockchainErrorCode {
// ... codes PD-52 existants inchanges ...
// PD-177: Custody mode gate (ERR-177-01)
INVALID_CUSTODY_MODE = 'INVALID_CUSTODY_MODE',
// PD-177: Signature failure at custody level (ERR-177-02)
SIGNATURE_FAILED = 'SIGNATURE_FAILED',
// PD-177: Incomplete proof chain (ERR-177-07)
PROOF_LINK_INCOMPLETE = 'PROOF_LINK_INCOMPLETE',
// PD-177: Reorg or abandonment (ERR-177-06)
TRANSACTION_REORGED_OR_ABANDONED = 'TRANSACTION_REORGED_OR_ABANDONED',
}
6. Contraintes techniques
6.1 Dependances inter-stories
| Story | Nature dependance | Impact PD-177 |
| PD-52 (done) | Foundation : CustodyStrategy, WalletService, TransactionService, ConfirmationTracker, error-codes, config | PD-177 enrichit mais ne modifie pas les interfaces. Les modifications sont additives (nouveaux champs, nouvelles constantes). |
| PD-55 (done) | Foundation : AnchorBatch entity, BlockchainAnchorProcessor, AnchorBatchService, BlockchainAdapterService. Atomicite heritee : PD-55 fournit les mecanismes d'atomicite (advisory lock PostgreSQL pg_advisory_xact_lock(batch_id) + transaction englobante dans submitBatch()). PD-177 herite de ces mecanismes sans les modifier — le processor BullMQ BlockchainAnchorProcessor conserve concurrency=1 et le flux transactionnel reste sequentiel. L'enrichissement PD-177 (signer_address, confirmations dynamiques) se fait a l'interieur de la transaction existante. | PD-177 enrichit le modele (signer_address), modifie le processor (confirmations dynamiques), enrichit l'adapter (signer_address + network). L'atomicite du lien tx→append-only est garantie par PD-55. |
| PD-53 (interface) | Smart contract MerkleAnchorContract.sol : definit le format de payload accepte on-chain | PD-177 utilise le format de payload existant via PayloadValidator.generateAnchorPayload(). Pas de modification requise tant que PD-53 ne change pas l'ABI. |
| PD-245 (interface) | Format preuve multi-chain : champ blockchain: 'ethereum_l2' \| 'tezos' | PD-177 ajoute blockchain = 'ethereum_l2' dans ProofArtifactDto. Les preuves tezos sont hors perimetre (passthrough). |
6.2 Framework de test
- Unite : Jest (existant, configure dans
package.json) - Integration : Jest avec
*.integration.spec.ts pattern - E2E : Jest avec
test/jest-e2e.json - Mocking :
ioredis-mock pour BullMQ en tests unitaires jest.fn() pour les services (pattern existant) mockQueryRunner pour les transactions TypeORM - Coverage : Seuils existants maintenus (80% branches, 85% functions/lines/statements)
- Path aliases :
@/, @config/, @common/, @modules/ (resolus par moduleNameMapper)
6.3 ESM/CJS
- Le projet utilise CommonJS (
"module": "nodenext" dans tsconfig mais NestJS 10 + ts-jest = CJS de facto) ethers@6.16.0 est ESM-first mais compatible CJS via les exports maps jose@6.1.3 est ESM et est mocke via __mocks__/jose.ts - PD-177 ne change pas le mode de module : tous les fichiers PD-177 sont CJS conformes au reste du projet
6.4 Base de donnees
- ORM : TypeORM 0.3.17
- Schema :
vault_blockchain - Migration : TypeORM CLI (
npm run migration:generate/run/validate) - Prerequis PostgreSQL local : La validation de la migration (INV-177-20) necessite une instance PostgreSQL locale accessible. Les commandes
npm run migration:validate et npm run migration:run se connectent a la base definie par DATABASE_URL dans .env local (ex: postgresql://postgres:postgres@localhost:5432/probatiovault_dev). Ce prerequis est obligatoire pour la Phase 2 du plan et la checklist pre-merge (§9.4). TypeScript et ESLint ne detectent pas les erreurs SQL — seul PostgreSQL les signale a l'execution (cf. learning PD-55 : "pas de subquery dans les index partiels"). - Attention INV-177-20 : la migration PD-177 ne doit PAS utiliser d'index partiel avec syntaxe non supportee. L'ajout de colonne
signer_address est une operation simple (ALTER TABLE ADD COLUMN). - Immutabilite append-only (INV-177-13/14) : protegee au niveau applicatif (AnchorBatchService refuse UPDATE/DELETE sur batches FINALIZED). La protection WORM au niveau base est hors perimetre PD-177 (definie dans la spec §3).
6.5 BullMQ
- Version :
bullmq@^5.1.0 avec @nestjs/bullmq@11.0.4 - INV-177-21 : le code existant PD-55 utilise deja
getJobSchedulers() / removeJobScheduler() (non deprecie). PD-177 ne doit pas introduire getRepeatableJobs / removeRepeatableByKey. - Le processor
BlockchainAnchorProcessor utilise @Processor('anchor') et WorkerHost — pas de changement d'API.
7. Securite
7.1 Modele de menace PD-177
| Menace | Invariant | Mitigation | Verification |
| Execution de code arbitraire Node.js sur le serveur applicatif | INV-177-10 | La cle privee reside dans AWS KMS (non exportable). Le code applicatif ne detient que l'ARN KMS. La signature requiert une authentification IAM. | TC-SEC-03 : sans credentials IAM, signature impossible |
| Fuite de cle privee dans les logs | INV-177-08 | SecretLeakInterceptor (N-02) intercepte les outputs ; BlockchainError.sanitizeContext() masque les patterns sensibles | TC-SEC-01 : zero pattern secret dans les logs apres cycle complet |
| Fuite de cle privee dans les traces d'erreur | INV-177-08 | BlockchainError ne stocke jamais de secret dans context ; le sanitizeContext() est applique dans toLog() | TC-SEC-01 : stack traces analysees → zero secret |
| Fuite de cle privee dans la persistance | INV-177-08 | Les colonnes de anchor_batches ne contiennent que des hash, adresses, et metadonnees publiques. Aucune colonne de type secret. | Revue du schema : aucune colonne private_key, seed, mnemonic |
| Fuite de cle privee dans les reponses API | INV-177-08 | SecretLeakInterceptor (N-02) applique sur les controllers ; les DTOs ne contiennent que des donnees publiques | TC-SEC-01 : reponses API analysees → zero secret |
| Injection de transaction non-ancrage | INV-177-03 | AnchorExclusivityGuard (N-08) verifie que seul le flux d'ancrage peut emettre ; pas de methode publique sendRawTransaction | TC-177-09 : requetes non-ancrage rejetees explicitement |
| Detection de fuite → non-reaction | INV-177-09 | SecretLeakInterceptor fail-closed : bloque l'operation + cree un incident securite | TC-SEC-02 : injection de secret → blocage + incident cree |
7.2 Mecanisme anti-fuite (SecretLeakInterceptor — N-02)
Architecture :
[Service output] → SecretLeakInterceptor → [Channel (log/API/trace)]
|
├── Pattern match? → BLOCK + throw BlockchainError(SECRET_LEAK_DETECTED)
│ + AuditService.logEvent('SECURITY_INCIDENT', ...)
│
└── No match → PASS
Patterns detectes (extension de BlockchainError.sanitizeContext()) : - Cle privee hex : /^0x[a-fA-F0-9]{64}$/ - Mnemonic : /^([a-z]+\s){11,23}[a-z]+$/ - Keywords : /mnemonic|seed|private.?key|secret|password/i - KMS key material : /^[A-Za-z0-9+/]{43,}={0,2}$/ (base64 >= 32 bytes)
Points d'application : 1. NestJS Interceptor global sur les controllers blockchain 2. Logger wrapper pour les services blockchain (override de Logger.log/error/warn) 3. BlockchainError.context (existant, PD-52)
7.3 Fail-closed (INV-177-09)
Le comportement fail-closed est garanti par un NestJS Response Interceptor qui scanne l'Observable apres next.handle() via un operateur RxJS tap :
intercept(context, next): Observable<any> {
return next.handle().pipe(
tap(data => this.scanForSecrets(data)), // scan la reponse AVANT serialisation HTTP
catchError(err => { // scan aussi les erreurs
this.scanForSecrets(err);
throw err;
})
);
}
Sequence d'execution : 1. La requete traverse le handler NestJS normalement (next.handle()) 2. L'Observable retourne par le handler est pipe avec un operateur tap qui scanne le contenu 3. Si un pattern secret est detecte dans data (reponse) ou err (erreur) : a. throw new BlockchainError(SECRET_LEAK_DETECTED) — bloque la serialisation HTTP (NestJS n'envoie pas la reponse originale) b. AuditService.logEvent('SECURITY_INCIDENT', ...) — cree l'incident securite de maniere synchrone 4. Le throw est lui-meme sanitize (le BlockchainError ne contient pas le secret) 5. Si aucun pattern detecte, la reponse est transmise normalement au client
Pourquoi fail-closed : Le scan se fait dans le pipe RxJS avant que NestJS ne serialise la reponse HTTP vers le client. Si un secret est detecte, l'exception remplace la reponse originale — le canal HTTP ne recoit jamais le contenu sensible. Pour les logs, le Logger wrapper applique le meme scan de maniere synchrone avant console.log().
8. Hypotheses d'implementation
| ID | Hypothese | Impact si invalidee |
| HI-01 | Le mode S2 (AWS KMS) est le seul mode custody autorise pour toute la duree de PD-177. | Si S1 ou S3 sont actives avant completion PD-177, le CustodyModeGuard doit etre assoupli et les tests adaptes. |
| HI-02 | Les reseaux cibles sont Polygon (chainId=137 en production, 80002 en testnet) et Arbitrum (chainId=42161 en production, 421614 en testnet). Les seuils de confirmation s'appliquent au chainId de production (137 → 12, 42161 → 30). | La NetworkConfirmationPolicy (N-07) doit mapper les chainIds testnet ET mainnet. En testnet, les memes seuils logiques s'appliquent mais avec les chainIds testnet (80002, 421614). |
| HI-03 | Le registre append-only est la table vault_blockchain.anchor_batches existante (PD-55). Il n'y a pas de table separee "registre". | Si un registre separe est requis, une nouvelle entity et migration seraient necessaires. La spec §3 parle de "registre append-only" base sur la persistance existante. |
| HI-04 | La protection append-only est applicative (refus d'UPDATE/DELETE par le service). La protection WORM au niveau base est hors perimetre PD-177 (spec §3). | Si une protection DB-level est requise, un trigger PostgreSQL doit etre ajoute (similaire au trigger d'immutabilite FINALIZED de PD-55). |
| HI-05 | Le format de payload pour le smart contract PD-53 est celui genere par PayloadValidator.generateAnchorPayload() existant OU celui de BlockchainAdapterService.encodeMerklePayload(). | Si PD-53 change l'ABI du smart contract, le payload encoder doit etre adapte. |
| HI-06 | La politique de confirmations 137 → 12/900s, 42161 → 30/900s s'applique aussi en testnet avec les chainIds testnet correspondants. | La NetworkConfirmationPolicy mappe par NetworkType ('polygon'/'arbitrum') plutot que par chainId brut, ce qui abstrait la difference testnet/mainnet. |
| HI-07 | Les codes d'erreur ajoutes (§5) sont des aliases distincts et non des renames des codes existants. Les codes PD-52 restent inchanges. | Si un rename est prefere, il faut un refactoring plus large avec impact sur les tests PD-52 existants (interdit par la contrainte "ne pas modifier les tests"). |
| HI-08 | L'horodatage "ISO 8601 UTC avec millisecondes" utilise le format toISOString() natif de JavaScript (2026-02-23T14:30:00.000Z). | Si un format different est requis (e.g., sans Z, avec offset), un formatter explicite est necessaire. |
| HI-09 | Le champ blockchain dans ProofArtifactDto est un ajout optionnel qui ne casse pas la validation existante des artefacts PD-55. | Si le champ est requis (non optionnel), les artefacts PD-55 existants deviendraient invalides → migration de donnees necessaire. |
| HI-10 | Le TransactionResult dans le processor (blockchain-anchor.processor.ts) est distinct du TransactionResult dans le DTO (transaction.dto.ts). PD-177 enrichit les deux. | Si une unification est requise, un refactoring plus large du processor est necessaire (impact PD-55). |
| HI-11 | Determinisme S2 KMS multi-instance : En mode S2, toutes les instances de l'application utilisent le meme kmsKeyId (ARN) via la configuration partagee (blockchain.config.ts). La derivation de l'adresse Ethereum depuis une cle KMS secp256k1 est mathematiquement deterministe : meme cle KMS = meme cle publique = meme adresse Ethereum. Il n'y a donc aucun risque de divergence d'adresse wallet entre instances. Cela garantit INV-177-01 (unicite wallet) sans mecanisme de coherence inter-processus explicite. | Si le mode S1 (cle locale) ou S3 (HSM) etait utilise en multi-instance, un mecanisme de consensus serait necessaire car chaque instance pourrait deriver une cle differente. Ce scenario est exclu par HI-01 (S2 uniquement pour PD-177). |
9. Points de vigilance
9.1 Risques techniques
| Risque | Probabilite | Impact | Mitigation |
Migration signer_address sur table avec donnees existantes : les batches PD-55 existants n'auront pas de signer_address | Certain | Faible | Colonne nullable ; le code PD-177 ne remplit signer_address que pour les nouveaux batches. Un backfill peut etre fait ulterieurement si necessaire. |
Divergence TransactionResult processor vs DTO : deux types TransactionResult coexistent (processeur et DTO) | Certain | Moyen | Documenter clairement la distinction. Le processor utilise son propre type interne ; l'adapter fait la conversion. Ne pas tenter d'unifier pour ne pas impacter PD-55. |
Timeout confirmation 900s vs polling 3s × 100 = 300s : le timeout actuel de ConfirmationTracker est 300s, pas 900s | Certain | Critique | C-02 doit remplacer MAX_POLLING_ATTEMPTS par un calcul base sur le timeout de la politique reseau : Math.ceil(timeoutMs / POLLING_INTERVAL_MS). Pour 900s/3s = 300 iterations. |
| Hardcoded CONFIRMATION_BLOCKS=12 dans le processor | Certain | Critique | C-04 doit lire la politique de confirmation depuis NetworkConfirmationPolicy au lieu du hardcode. Le chainId doit etre propage dans executeConfirmPhase. |
| Network hardcode 'polygon' dans BlockchainAdapterService.waitForConfirmation() | Certain | Critique | C-05 doit propager le network retourne par submitMerkleRoot() vers waitForConfirmation(). Le TransactionResult interne du processor doit contenir le network. |
| SecretLeakInterceptor faux positifs : des valeurs legitimes pourraient matcher les patterns de secrets | Possible | Moyen | Les patterns sont conservateurs (cle privee = exactement 0x + 64 hex). Les faux positifs sur des hash normaux ne matcheront pas car les hash d'ancrage ne commencent pas par 0x dans le context. Tests de non-regression avec des valeurs legitimes. |
9.2 Points de coordination inter-equipes
| Point | Equipe | Action requise |
| La migration N-06 doit etre deployee AVANT le nouveau code applicatif | SRE | Coordonner le deploiement : migration d'abord, puis code |
| La politique de confirmations (N-07) doit etre validee par l'equipe produit/compliance | Product | Confirmer : Polygon=12/900s, Arbitrum=30/900s |
| Le runbook de reprise (D-01) doit etre valide par l'equipe SRE | SRE | Review du runbook avant merge |
| Les credentials IAM pour KMS doivent etre disponibles en environnement de test | SRE | Verifier que l'environnement de test a un IAM role avec acces au KMS key |
9.3 Non-regression PD-52/PD-55
Les modifications PD-177 ne doivent pas casser les tests existants PD-52 et PD-55. Verification :
- Les codes d'erreur ajoutes sont des nouvelles entrees dans l'enum, pas des renames
- La colonne
signer_address est nullable donc les batches existants restent valides - Le
ConfirmationTracker garde le meme comportement par defaut (la politique de confirmation utilise les valeurs de config existantes par defaut) - Le
TransactionResult DTO ajoute un champ optionnel signerAddress? - Le
ProofArtifactDto ajoute un champ optionnel blockchain? - L'export de
CustodyService depuis BlockchainModule est additif
9.4 Checklist pre-merge
Annexe A : Detail des fichiers modifies/crees
Fichiers modifies
| Fichier | Lignes estimees modifiees | Nature |
src/modules/blockchain/constants/error-codes.ts | +15 | Ajout 4 codes dans l'enum |
src/modules/blockchain/blockchain.config.ts | +10 | Ajout confirmationTimeoutMs dans NetworkConfig |
src/modules/blockchain/transaction/confirmation.tracker.ts | +30, -5 | Timeout configurable par reseau |
src/modules/blockchain/transaction/transaction.service.ts | +5 | Ajout signerAddress dans retour |
src/modules/blockchain/dto/transaction.dto.ts | +3 | Ajout champ optionnel signerAddress |
src/modules/blockchain/blockchain.module.ts | +3 | Export CustodyService |
src/modules/anchor/entities/anchor-batch.entity.ts | +8 | Ajout colonne signer_address |
src/modules/anchor/processors/blockchain-anchor.processor.ts | +20, -5 | Confirmations dynamiques + signer_address |
src/modules/anchor/services/blockchain-adapter.service.ts | +15, -5 | Retour signerAddress, propagation network |
src/modules/anchor/services/anchor-batch.service.ts | +15 | Ecriture signer_address, protection append-only |
src/modules/anchor/dto/proof-artifact.dto.ts | +8 | Ajout champ blockchain |
src/modules/anchor/constants/anchor.constants.ts | +5 | Import de la politique de confirmation |
Fichiers crees
| Fichier | Lignes estimees | Nature |
src/modules/blockchain/custody/custody-mode.guard.ts | ~40 | Guard S2-only |
src/modules/blockchain/security/secret-leak.interceptor.ts | ~80 | Interceptor anti-fuite |
src/modules/blockchain/wallet/wallet-operational.service.ts | ~120 | Facade PD-177 |
src/modules/blockchain/wallet/anchor-exclusivity.guard.ts | ~50 | Guard exclusivite ancrage |
src/modules/blockchain/constants/network-confirmation-policy.ts | ~30 | Politique de confirmations |
src/modules/anchor/validators/anchor-proof.validator.ts | ~60 | Validation lien de preuve |
src/modules/blockchain/wallet/wallet-recovery.service.ts | ~80 | Service de reprise |
src/database/migrations/YYYYMMDDHHMMSS-PD177-AddSignerAddress.ts | ~25 | Migration DB |
docs/runbooks/wallet-recovery-procedure.md | ~100 | Runbook de reprise |
docs/runbooks/wallet-rotation-log-template.md | ~30 | Template rotation |
Annexe B : Mapping complet Tests → Composants
| Test | Composants impliques | Type de test |
| TC-177-01 | N-03, C-10 | Unite |
| TC-177-02 | N-01, N-03 | Unite |
| TC-177-03 | (interface CustodyStrategy PD-52) | Contrat |
| TC-177-04 | C-06, C-05, N-03 | Integration |
| TC-177-05 | C-02, N-07 | Integration |
| TC-177-06 | C-03, C-11, N-06 | Integration |
| TC-177-07 | C-09, C-11 | Unite |
| TC-177-08 | C-06, N-03 | Integration |
| TC-177-09 | N-08, N-03 | Unite |
| TC-177-10 | C-04, C-05 | Unite |
| TC-177-11 | C-11, C-06 | Integration |
| TC-177-12 | C-09, N-04 | Integration |
| TC-177-13 | C-02, N-07 | Integration |
| TC-177-14 | N-03, C-03 | Integration |
| TC-177-15 | N-05, D-01 | Manuel/Script |
| TC-177-16 | (analyse statique) | Statique |
| TC-177-17 | (analyse statique) | Statique |
| TC-177-18 | N-06 | Migration |
| TC-177-19 | (analyse statique) | Statique |
| TC-ERR-01 | N-01, C-01 | Unite |
| TC-ERR-02 | N-03, C-01, N-02 | Unite |
| TC-ERR-03 | C-06, C-01 | Unite |
| TC-ERR-04 | (GasEstimator existant), C-01 | Unite |
| TC-ERR-05 | (RpcProvider existant), C-01 | Unite |
| TC-ERR-06 | C-02, C-01 | Integration |
| TC-ERR-07 | N-04, C-01 | Unite |
| TC-ERR-08 | N-02, C-01 | Unite |
| TC-SEC-01 | N-02 | Securite |
| TC-SEC-02 | N-02 | Securite |
| TC-SEC-03 | (architecture S2 KMS) | Securite |