Aller au contenu

PD-55 — Plan d'implémentation

Métadonnées

Champ Valeur
Story ID PD-55
Titre Worker ancrage blockchain périodique
Complexité High
Fichiers estimés 18-22
Dépendances PD-52 ✅, PD-53 ✅, PD-237 ✅, PD-21 ✅

Phase 0 — Go/No-Go

Vérification des dépendances

Story Statut Composant vérifié Action
PD-52 ✅ DONE TransactionService.sendAnchorTransaction() Utiliser directement
PD-52 ✅ DONE ConfirmationTracker.track() Utiliser pour finalité
PD-53 ✅ DONE Smart contract Merkle anchor Déployé sur testnet
PD-237 ✅ DONE MerkleTreeService.persistTree() Utiliser pour persistance
PD-21 ✅ DONE BaseProcessor (BullMQ) Étendre pour le worker

Vérification des prérequis

Prérequis Vérifié Notes
Tables requises ⚠️ Créer anchor_batches, anchor_batch_events
Services dépendants MerkleTreeService, TransactionService, JobsService
Configuration BullMQ Module jobs existant avec Redis
Entités événements ⚠️ Table audit_log existante, à lier

Go/No-Go : ✅ GO — Toutes les dépendances sont disponibles. Créer les nouvelles tables.


Architecture

Diagramme de flux

┌─────────────────────────────────────────────────────────────────────────┐
│                        BullMQ Scheduler (10 min)                        │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│                     BlockchainAnchorProcessor                           │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────────────────┐   │
│  │ 1. Collect    │→ │ 2. Build      │→ │ 3. Anchor                 │   │
│  │    Events     │  │    Batch      │  │    On-chain               │   │
│  └───────────────┘  └───────────────┘  └───────────────────────────┘   │
│         │                   │                       │                   │
│         ▼                   ▼                       ▼                   │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────────────────┐   │
│  │ ProofEvent    │  │ MerkleTree    │  │ TransactionService        │   │
│  │ Service       │  │ Service       │  │ + ConfirmationTracker     │   │
│  └───────────────┘  └───────────────┘  └───────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│                        4. Finalize & Link                               │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────────────────┐   │
│  │ AnchorBatch   │→ │ Event Links   │→ │ Audit Log                 │   │
│  │ Service       │  │ (batch↔event) │  │ Journalisation            │   │
│  └───────────────┘  └───────────────┘  └───────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

États du lot (AnchorBatch)

PENDING → BUILDING → SUBMITTED → CONFIRMED → FINALIZED
                 ↘            ↘
                  FAILED       PENDING_FINALITY

Contraintes techniques

Dépendances inter-PD

Story Statut Nature de la dépendance
PD-52 ✅ DONE TransactionService, ConfirmationTracker
PD-53 ✅ DONE Smart contract ABI (si appel direct)
PD-237 ✅ DONE MerkleTreeService pour persistance arbres
PD-21 ✅ DONE BaseProcessor, JobsService

Framework de test

  • Runner : Jest (existant dans le projet)
  • Tests d'intégration : Mocks Redis + DB in-memory (TypeORM SQLite)
  • Variables CI : DATABASE_URL, REDIS_URL, CI=true

Stratégie de test pour triggers DB (ECT-01) :

Le trigger d'immutabilité (FINALIZED) n'est pas testable avec SQLite in-memory (support limité des triggers). Stratégie retenue :

  1. Tests unitaires : Guard applicatif dans AnchorBatchService.update() vérifiant batch.status !== 'FINALIZED' avant toute modification
  2. Tests d'intégration CI : Utiliser @testcontainers/postgresql pour exécuter T55-16 avec une vraie instance PostgreSQL
  3. Double protection : Le trigger DB est la ligne de défense finale, le guard applicatif est la protection primaire

Configuration CI pour T55-16 :

services:
  postgres:
    image: postgres:15
    env: { POSTGRES_DB: test, POSTGRES_USER: test, POSTGRES_PASSWORD: test }

Compatibilité ESM/CJS

  • Aucune dépendance ESM-only identifiée
  • Projet en CommonJS avec configuration Jest standard

Configuration par défaut (seuils spec)

Paramètre Valeur par défaut Source
ANCHOR_CYCLE_INTERVAL_MS 600000 (10 min) CA-55-01
ANCHOR_CYCLE_TOLERANCE_MS 120000 (±2 min) CA-55-01
FINALITY_CONFIRMATIONS 30 Définition "Finalité" §3
FINALITY_TIMEOUT_MS 300000 (5 min) Définition "Finalité" §3
ESCALADE_WARNING_MS 3600000 (1h) ERR-55-05
ESCALADE_CRITICAL_MS 7200000 (2h) ERR-55-05
BATCH_MAX_EVENTS 10000 §9
BACKLOG_MAX_EVENTS 100000 §9.1
CIRCUIT_BREAKER_THRESHOLD 150000 §9.1

Ces valeurs sont injectées via ConfigService (module @nestjs/config).


Code Contracts

CC-55-01 : Entité AnchorBatch

/**
 * Lot d'ancrage blockchain.
 *
 * @invariant INV-55-02: Un événement ne peut être dans qu'un seul lot finalisé
 * @invariant INV-55-04: Un lot finalisé est immutable
 *
 * CONTRAINTE D'IMMUTABILITÉ (ECT-01) :
 * Une fois en status FINALIZED, toute tentative d'UPDATE sur les colonnes
 * status, merkle_root, tx_id, finalized_at DOIT être rejetée.
 * Implémentation : trigger DB ou contrainte CHECK sur status='FINALIZED'.
 *
 * NOTE : Cette immutabilité couvre également les relations anchor_batch_events :
 * - Aucun DELETE/INSERT sur anchor_batch_events pour un batch FINALIZED
 * - Le périmètre événementiel du lot est figé à la finalisation
 */
interface AnchorBatchContract {
  // Identifiant unique (UUID v4)
  batchId: string;

  // Racine Merkle (hex64 lowercase, sans 0x)
  merkleRoot: string;

  // Référence vers MerkleTree (PD-237)
  treeId: string | null;

  // Transaction blockchain
  txId: string | null;
  chainId: number;
  blockNumber: number | null;

  // États
  status: 'PENDING' | 'BUILDING' | 'SUBMITTED' | 'PENDING_FINALITY' | 'FINALIZED' | 'FAILED';

  // Timestamps (ISO 8601 UTC ms)
  windowStart: Date;
  windowEnd: Date;
  createdAt: Date;
  submittedAt: Date | null;
  finalizedAt: Date | null;

  // Nombre d'événements
  eventCount: number;

  // Erreur si FAILED
  errorReason: string | null;
}

CC-55-02 : Entité AnchorBatchEvent (relation)

/**
 * Relation lot ↔ événement probatoire.
 *
 * @invariant INV-55-02: Contrainte unique sur (eventId, batchStatus='FINALIZED')
 * @invariant INV-55-03: Traçabilité complète événement → lot → tx
 */
interface AnchorBatchEventContract {
  // Clé primaire composite
  batchId: string;
  eventId: string;

  // Type d'événement (liste fermée V1)
  eventType: 'DOCUMENT_SIGNED' | 'DOCUMENT_CERTIFIED' | 'AUDIT_LOG_ENTRY';

  // Hash de l'événement (hex64)
  eventHash: string;

  // Position dans le lot (ordre canonique)
  leafIndex: number;

  // Timestamp de l'événement (ISO 8601 UTC ms)
  eventCreatedAt: Date;
}

CC-55-03 : Service AnchorBatchService

/**
 * Gestion des lots d'ancrage.
 *
 * @invariant INV-55-06: Aucun événement perdu en cas d'échec
 * @invariant INV-55-07: Continuité temporelle des fenêtres
 * @invariant INV-55-10: Journalisation de chaque transition
 *
 * JOURNALISATION OBLIGATOIRE (ECT-05) :
 * Chaque méthode modifiant l'état d'un lot DOIT appeler AuditService.log()
 * avec un AuditEntry structuré :
 *   - action: 'ANCHOR_BATCH_CREATED' | 'ANCHOR_BATCH_FINALIZED' | 'ANCHOR_BATCH_FAILED'
 *   - entityType: 'anchor_batch'
 *   - entityId: batchId
 *   - payload: { status, eventCount, merkleRoot?, txId?, errorReason? }
 *
 * Injection : constructor(private readonly auditService: AuditService)
 */
interface AnchorBatchServiceContract {
  /**
   * Crée un nouveau lot avec les événements éligibles.
   * @returns null si aucun événement (no-op tracé)
   */
  createBatch(windowStart: Date, windowEnd: Date): Promise<AnchorBatch | null>;

  /**
   * Finalise un lot après confirmation blockchain.
   * @throws si lot non en état PENDING_FINALITY
   */
  finalizeBatch(batchId: string, txResult: TransactionResult): Promise<void>;

  /**
   * Marque un lot en échec (réversible pour retry).
   *
   * ATOMICITÉ (ECT-02) :
   * Cette méthode DOIT s'exécuter dans une transaction DB unique :
   * 1. UPDATE anchor_batches SET status='FAILED', error_reason=reason
   * 2. DELETE FROM anchor_batch_events WHERE batch_id=batchId
   * Les deux opérations sont atomiques : si l'une échoue, rollback complet.
   * Les événements du lot FAILED retournent au pool des événements éligibles.
   */
  failBatch(batchId: string, reason: string): Promise<void>;

  /**
   * Récupère les événements éligibles d'un lot.
   */
  getBatchEvents(batchId: string): Promise<AnchorBatchEvent[]>;

  /**
   * Exporte l'artefact de preuve externe (JSON §8).
   */
  exportProofArtifact(batchId: string): Promise<ProofArtifactDto>;
}

CC-55-04 : Service ProofEventService

/**
 * Collecte des événements probatoires éligibles.
 *
 * @invariant INV-55-09: Validation stricte des entrées
 * @invariant INV-55-13: Ordre canonique (created_at ASC, event_id ASC)
 *
 * JOURNALISATION OBLIGATOIRE (ECT-05) :
 * La méthode markEventsAnchored() DOIT appeler AuditService.log()
 * avec un AuditEntry structuré :
 *   - action: 'EVENTS_ANCHORED'
 *   - entityType: 'proof_event'
 *   - entityId: batchId
 *   - payload: { eventIds: string[], count: number }
 *
 * Injection : constructor(private readonly auditService: AuditService)
 */
interface ProofEventServiceContract {
  /**
   * Collecte les événements éligibles non ancrés dans la fenêtre.
   * Tri canonique appliqué.
   *
   * @param windowStart Début de fenêtre (inclusif)
   * @param windowEnd Fin de fenêtre (exclusif)
   * @param limit Max 10000 événements (découpage si plus)
   */
  collectEligibleEvents(
    windowStart: Date,
    windowEnd: Date,
    limit?: number,
  ): Promise<ProofEventDto[]>;

  /**
   * Vérifie qu'un événement n'est pas déjà ancré.
   */
  isEventAnchored(eventId: string): Promise<boolean>;

  /**
   * Marque les événements comme ancrés (lien vers batchId).
   */
  markEventsAnchored(eventIds: string[], batchId: string): Promise<void>;
}

CC-55-05 : Processor BlockchainAnchorProcessor

/**
 * Worker BullMQ d'ancrage périodique.
 *
 * @invariant INV-55-01: Seuls les hashes sont ancrés
 * @invariant INV-55-05: Calcul déterministe (RFC 8785)
 * @invariant INV-55-10: Journalisation de chaque transition
 * @invariant INV-55-11: Trace no-op si aucun événement
 */
interface BlockchainAnchorProcessorContract {
  /**
   * Point d'entrée du job.
   * Orchestration complète : collect → build → anchor → finalize.
   */
  process(job: Job<AnchorJobData>): Promise<JobResult>;

  /**
   * Clé d'idempotence = windowStart ISO.
   */
  getIdempotencyKey(job: Job<AnchorJobData>): string;
}

CC-55-06 : DTO ProofArtifactDto (§8 spec)

/**
 * Artefact de preuve externe exportable.
 * Format JSON UTF-8 conforme §8.1 de la spécification.
 *
 * @invariant INV-55-08: Vérifiable sans accès privilégié
 */
interface ProofArtifactDto {
  version: '1.0';
  lot_id: string;       // UUID canonical
  merkle_root: string;  // hex64 lowercase sans 0x
  tx_id: string;        // 0x + 64 hex lowercase (66 chars)
  chain_id: number;     // EIP-155
  block_number: number;
  events: Array<{
    event_id: string;      // UUID canonical
    event_hash: string;    // hex64 lowercase
    merkle_index: number;  // position dans l'ordre canonique
    merkle_proof: string[]; // siblings hex64
  }>;
}

Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    subgraph "BullMQ Scheduler"
        CRON["⏱ Cron 10 min"]
    end

    subgraph "Processor"
        BAP["BlockchainAnchorProcessor<br/>(CC-55-05)"]
    end

    subgraph "Services métier"
        PES["ProofEventService<br/>(CC-55-04)"]
        ABS["AnchorBatchService<br/>(CC-55-03)"]
        AAS["AnchorAlertService<br/>(Phase 4 - tâche 14)"]
    end

    subgraph "Dépendances PD existantes"
        MTS["MerkleTreeService<br/>(PD-237)"]
        TXS["TransactionService<br/>(PD-52)"]
        CFT["ConfirmationTracker<br/>(PD-52)"]
        SC["Smart Contract<br/>(PD-53)"]
        BULL["BaseProcessor / JobsService<br/>(PD-21)"]
    end

    subgraph "Entités"
        AB["AnchorBatch<br/>(CC-55-01)"]
        ABE["AnchorBatchEvent<br/>(CC-55-02)"]
        PAD["ProofArtifactDto<br/>(CC-55-06)"]
    end

    subgraph "Infrastructure"
        REDIS["Redis (BullMQ)"]
        PG["PostgreSQL"]
        AUDIT["AuditService"]
    end

    CRON -->|déclenche job| BAP
    BAP -->|collecte événements| PES
    BAP -->|crée/finalise lots| ABS
    BAP -->|construit arbre| MTS
    BAP -->|envoie tx| TXS
    BAP -->|suit finalité| CFT
    TXS -->|appelle| SC
    BAP -->|étend| BULL

    ABS -->|persiste| AB
    ABS -->|persiste| ABE
    ABS -->|exporte| PAD
    ABS -->|journalise| AUDIT
    PES -->|journalise| AUDIT

    AAS -->|détecte gaps| ABS
    AAS -->|alerte| AUDIT

    CRON -.->|queue| REDIS
    AB -.->|stocké| PG
    ABE -.->|stocké| PG

Diagramme de séquence — Cycle d'ancrage nominal

sequenceDiagram
    participant CRON as BullMQ Scheduler
    participant BAP as BlockchainAnchorProcessor
    participant PES as ProofEventService
    participant ABS as AnchorBatchService
    participant MTS as MerkleTreeService (PD-237)
    participant TXS as TransactionService (PD-52)
    participant CFT as ConfirmationTracker (PD-52)
    participant DB as PostgreSQL
    participant AUDIT as AuditService

    CRON->>BAP: process(job: AnchorJobData)
    activate BAP

    Note over BAP: 1. Collect Events
    BAP->>PES: collectEligibleEvents(windowStart, windowEnd)
    PES->>DB: SELECT events WHERE NOT anchored AND created_at IN window
    DB-->>PES: ProofEventDto[]
    PES-->>BAP: events (triés canoniquement INV-55-13)

    alt Aucun événement
        BAP->>AUDIT: log(action: 'ANCHOR_NOOP', INV-55-11)
        BAP-->>CRON: JobResult { status: 'NOOP' }
    end

    Note over BAP: 2. Build Batch
    BAP->>ABS: createBatch(windowStart, windowEnd)
    ABS->>DB: INSERT anchor_batches (PENDING)
    ABS->>DB: INSERT anchor_batch_events
    ABS->>AUDIT: log(action: 'ANCHOR_BATCH_CREATED')
    ABS-->>BAP: AnchorBatch

    Note over BAP: 3. Construct Merkle Tree
    BAP->>MTS: persistTree(eventHashes)
    MTS-->>BAP: { treeId, merkleRoot }

    Note over BAP: 4. Anchor On-chain
    BAP->>ABS: update batch (SUBMITTED)
    BAP->>TXS: sendAnchorTransaction(merkleRoot)
    TXS-->>BAP: txId

    Note over BAP: 5. Wait for Finality
    BAP->>CFT: track(txId, { confirmations: 30, timeout: 5min })
    CFT-->>BAP: TransactionResult { confirmed: true, blockNumber }

    Note over BAP: 6. Finalize
    BAP->>ABS: finalizeBatch(batchId, txResult)
    ABS->>DB: UPDATE anchor_batches SET status='FINALIZED'
    ABS->>AUDIT: log(action: 'ANCHOR_BATCH_FINALIZED')
    ABS-->>BAP: void

    BAP->>PES: markEventsAnchored(eventIds, batchId)
    PES->>AUDIT: log(action: 'EVENTS_ANCHORED')

    BAP-->>CRON: JobResult { status: 'FINALIZED', batchId }
    deactivate BAP

Diagramme de séquence — Échec et recovery

sequenceDiagram
    participant BAP as BlockchainAnchorProcessor
    participant ABS as AnchorBatchService
    participant TXS as TransactionService (PD-52)
    participant CFT as ConfirmationTracker (PD-52)
    participant DB as PostgreSQL
    participant AUDIT as AuditService
    participant AAS as AnchorAlertService

    Note over BAP: Transaction échoue ou timeout finalité

    alt Erreur TransactionService
        TXS--xBAP: Error (gas, nonce, network)
        BAP->>ABS: failBatch(batchId, reason)
        ABS->>DB: BEGIN TRANSACTION
        ABS->>DB: UPDATE anchor_batches SET status='FAILED'
        ABS->>DB: DELETE anchor_batch_events WHERE batch_id (ECT-02)
        ABS->>DB: COMMIT
        ABS->>AUDIT: log(action: 'ANCHOR_BATCH_FAILED')
        Note over ABS: Événements retournent au pool éligible (INV-55-06)
    end

    alt Timeout ConfirmationTracker
        CFT--xBAP: TimeoutError (>5 min)
        BAP->>ABS: update batch (PENDING_FINALITY)
        BAP->>AAS: escalade(batchId, elapsedTime)

        alt T+1h sans finalité
            AAS->>AUDIT: log(action: 'ESCALADE_WARNING', ERR-55-05)
        end

        alt T+2h sans finalité
            AAS->>AUDIT: log(action: 'ESCALADE_CRITICAL', ERR-55-05)
        end
    end

    alt Gap temporel détecté (INV-55-07)
        AAS->>ABS: detectWindowGaps(since)
        ABS->>DB: SELECT lots FINALIZED ORDER BY window_start
        DB-->>ABS: gaps[]
        AAS->>AUDIT: log(action: 'BATCH_BLOCKED_GAP', ERR-55-08)
        Note over AAS: Lot bloqué jusqu'à résolution manuelle
    end

Mapping INV/CA → Tâches

Invariant/CA Tâche couvrant Test couvrant
INV-55-01 CC-55-05 (hash only dans payload) T55-15
INV-55-02 CC-55-02 (contrainte unique) T55-08
INV-55-03 CC-55-02, CC-55-03 (traçabilité) T55-02, T55-10
INV-55-04 CC-55-01 (status FINALIZED immutable) T55-16
INV-55-05 CC-55-05 + MerkleTreeService (RFC 8785) T55-03
INV-55-06 CC-55-03 (failBatch réversible) T55-05, T55-13, T55-18
INV-55-07 CC-55-04 (collecte continue) T55-09
INV-55-08 CC-55-06 (artefact exportable) T55-10
INV-55-09 CC-55-04 (validation entrées) T55-11
INV-55-10 CC-55-05 (journalisation) T55-17
INV-55-11 CC-55-05 (trace no-op) T55-04
INV-55-12 Runbook ops (hors code) T55-14
INV-55-13 CC-55-04 (ordre canonique) T55-03
CA-55-01 Config BullMQ (cron 10 min ±2) T55-01
CA-55-02 CC-55-02 (cardinalité 1) T55-02
CA-55-03 CC-55-05 (hash only) T55-15
CA-55-04 CC-55-01 (champs complets) T55-02
CA-55-05 MerkleTreeService (déterminisme) T55-03
CA-55-06 CC-55-03 (failBatch) T55-05
CA-55-07 CC-55-05 (trace no-op) T55-04
CA-55-08 CC-55-06 (export proof) T55-10
CA-55-09 CC-55-02 (rejet double) T55-08
CA-55-10 CC-55-04 + alerting T55-09
CA-55-11 CC-55-04 (validation) T55-11
CA-55-12 Runbook ops T55-14

Tâches d'implémentation

Phase 1 : Modèle de données (CC-55-01, CC-55-02)

# Tâche Agent Fichiers Dépend de
1 Créer entité AnchorBatch agent-developer entities/anchor-batch.entity.ts -
2 Créer entité AnchorBatchEvent agent-developer entities/anchor-batch-event.entity.ts 1
3 Créer migration agent-developer migrations/xxx-anchor-batch.ts 1, 2
4 Tests unitaires entités agent-qa-unit-integration entities/*.spec.ts 1, 2

Phase 2 : Services métier (CC-55-03, CC-55-04)

# Tâche Agent Fichiers Dépend de
5 Créer ProofEventService agent-developer services/proof-event.service.ts 3
6 Créer AnchorBatchService agent-developer services/anchor-batch.service.ts 3, 5
7 Créer DTOs (ProofArtifactDto) agent-developer dto/proof-artifact.dto.ts -
8 Tests unitaires services agent-qa-unit-integration services/*.spec.ts 5, 6

Phase 3 : Worker BullMQ (CC-55-05)

# Tâche Agent Fichiers Dépend de
9 Créer BlockchainAnchorProcessor agent-developer processors/blockchain-anchor.processor.ts 5, 6
10 Configurer job répétable (10 min) agent-developer anchor.module.ts, constants/ 9
11 Tests unitaires processor agent-qa-unit-integration processors/*.spec.ts 9
12 Tests intégration (workflow complet) agent-qa-unit-integration __tests__/anchor.e2e.spec.ts 9, 10

Phase 4 : Intégration & Alerting

# Tâche Agent Fichiers Dépend de
13 Intégrer module anchor dans app agent-developer app.module.ts, anchor.module.ts 10
14 Configurer alertes PagerDuty agent-sre services/anchor-alert.service.ts 6
15 Tests de robustesse (timeout, reorg) agent-qa-unit-integration __tests__/anchor-robustness.spec.ts 12

Détection des trous temporels (ECT-03)

La tâche 14 (alertes PagerDuty) DOIT implémenter une méthode detectWindowGaps() :

/**
 * Détecte les fenêtres temporelles manquantes (INV-55-07).
 * Appelé par le job de monitoring pour vérifier la continuité.
 *
 * @returns Liste des gaps détectés avec timestamps
 */
async detectWindowGaps(
  since: Date,
  windowSize: number = 600_000, // 10 min en ms
): Promise<Array<{ expectedStart: Date; expectedEnd: Date }>> {
  // 1. Récupérer tous les lots FINALIZED depuis `since`
  // 2. Vérifier la continuité des fenêtres (windowEnd[n] === windowStart[n+1])
  // 3. Retourner les gaps détectés (fenêtres sans lot)
}

Alerte déclenchée si gaps.length > 0 → WARNING (gap < 1h) ou CRITICAL (gap >= 1h).

Traitement ERR-55-08 — Blocage lot si gap non résolu :

Conformément à ERR-55-08 ("lot concerné non validé tant que non résolu"), le processor DOIT :

  1. Avant chaque finalisation : appeler detectWindowGaps(batch.windowStart, batch.windowEnd)
  2. Si gap détecté dans la fenêtre du lot : bloquer la finalisation (état BLOCKED_GAP)
  3. Journaliser : AuditService.log({ action: 'BATCH_BLOCKED_GAP', entityId: batchId, payload: { gaps } })
  4. Résolution : intervention manuelle requise pour valider que le gap est acceptable (événements vides) ou corriger

Le lot passe de BLOCKED_GAPPENDING_FINALITY uniquement après validation manuelle via endpoint admin POST /admin/anchor/batches/:id/resolve-gap.

Phase 5 : Documentation & Export

# Tâche Agent Fichiers Dépend de
16 Endpoint export proof artifact agent-developer anchor.controller.ts 6, 7
17 Tests endpoint agent-qa-unit-integration anchor.controller.spec.ts 16

Estimation

Phase Tâches Effort estimé
Phase 1 4 2-3h
Phase 2 4 4-5h
Phase 3 4 4-5h
Phase 4 3 2-3h
Phase 5 2 1-2h
Total 17 13-18h

Risques et mitigations

Risque Impact Mitigation
Timeout ConfirmationTracker Lot en PENDING_FINALITY trop longtemps Mécanisme escalade ERR-55-05 (T+1h → WARNING, T+2h → CRITICAL)
Reorg blockchain Perte de finalité ConfirmationTracker existant détecte les reorgs
Backlog > 100K Dégradation performance Circuit breaker à 150K (§9.1 spec)
Double ancrage Violation INV-55-02 Contrainte unique DB + vérification pré-ancrage

Notes d'implémentation

Ordre canonique (INV-55-13)

// Tri des événements avant construction Merkle
events.sort((a, b) => {
  const dateCompare = a.createdAt.getTime() - b.createdAt.getTime();
  if (dateCompare !== 0) return dateCompare;
  return a.eventId.localeCompare(b.eventId, 'en', { sensitivity: 'base' });
});

Format created_at (ECT-02-v2)

Tous les timestamps doivent être en ISO 8601 UTC avec précision milliseconde :

const createdAt = event.createdAt.toISOString(); // "2026-02-21T10:30:00.123Z"

Canonicalisation RFC 8785

Utiliser la bibliothèque canonicalize (npm) déjà utilisée par PD-237 :

import canonicalize from 'canonicalize';
const canonicalJson = canonicalize(eventPayload);
const hash = createHash('sha256').update(canonicalJson!).digest('hex');


Références

  • Spécification : PD-55-specification.md (v3)
  • Tests : PD-55-tests.md
  • Dépendances : PD-52, PD-53, PD-237, PD-21