Aller au contenu

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

  1. Decoupage en composants
  2. Flux d'implementation
  3. Mapping invariants → mecanismes techniques
  4. Mapping criteres d'acceptation → mecanismes + tests
  5. Mapping erreurs → BlockchainErrorCode
  6. Contraintes techniques
  7. Securite
  8. Hypotheses d'implementation
  9. 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_idtx_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 :

  1. Les codes d'erreur ajoutes sont des nouvelles entrees dans l'enum, pas des renames
  2. La colonne signer_address est nullable donc les batches existants restent valides
  3. Le ConfirmationTracker garde le meme comportement par defaut (la politique de confirmation utilise les valeurs de config existantes par defaut)
  4. Le TransactionResult DTO ajoute un champ optionnel signerAddress?
  5. Le ProofArtifactDto ajoute un champ optionnel blockchain?
  6. L'export de CustodyService depuis BlockchainModule est additif

9.4 Checklist pre-merge

  • npm run migration:validate — zero erreur
  • npm run migration:run — migration appliquee avec succes
  • npm test — tous les tests PD-52 et PD-55 passent (non-regression)
  • npm run test:e2e — tests E2E passent
  • Grep getRepeatableJobs|removeRepeatableByKey dans les fichiers PD-177 → zero match
  • Grep private.?key|mnemonic|seed dans les DTOs et reponses API PD-177 → zero match
  • Grep crypto\.randomUUID dans les fichiers PD-177 generant des UUID → present
  • Grep pv-test- dans les fixtures de test PD-177 → toutes les cles ephemeres prefixees
  • Le runbook wallet-recovery-procedure.md existe et est versionne
  • Coverage >= 85% sur les fichiers PD-177

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