Skip to content

feat: reconcile chain and DB escrow state to repair drift - #184

Merged
meshackyaro merged 1 commit into
trustflow-protocol:mainfrom
CillaSam:feature/issue-166-escrow-chain-db-reconciliation
Aug 16, 2026
Merged

feat: reconcile chain and DB escrow state to repair drift#184
meshackyaro merged 1 commit into
trustflow-protocol:mainfrom
CillaSam:feature/issue-166-escrow-chain-db-reconciliation

Conversation

@CillaSam

Copy link
Copy Markdown
Contributor

Description

Implements a deterministic reconciler that periodically diffs on-chain escrow state against the DB and repairs drift caused by missed events or partial writes, as requested in the issue.

Closes #166

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🐛 Bug fix (non-breaking change which fixes an issue)

Component

  • Backend (Node.js API)

Changes Made

  • New escrow-reconciliation module: EscrowReconciliationService diffs every DB escrow linked to a contractEscrowId against its on-chain state (status + amount), repairing drift in a single write per escrow by trusting chain as the source of truth.
  • EscrowChainStateClient abstraction + SorobanEscrowChainStateClient implementation, reading contract storage via Soroban RPC getContractData. Falls back to an in-memory simulated store when TRUSTFLOW_CONTRACT_ID is unset (the default in local dev/tests/CI), mirroring how the IPFS pin providers degrade without credentials — keeps the reconciler fully testable without network access.
  • EscrowReconciliationWorkerService runs the sweep on an interval (ESCROW_RECONCILIATION_SWEEP_INTERVAL_MS, default 10 min; <= 0 disables it), following the same lifecycle pattern as GigExpiryWorkerService.
  • EscrowReconciliationController (JWT-guarded) exposes POST /escrow-reconciliation/run, GET /escrow-reconciliation/runs, and GET /escrow-reconciliation/runs/:runId, with Swagger docs.
  • Since full contract-storage enumeration isn't available without an off-chain indexer, "escrow exists on-chain but missing from the DB" (a missed creation event) is only checked for contractEscrowIds a caller supplies explicitly (e.g. an ops backfill list) — passed via the run endpoint's contractEscrowIds body field. Found records are backfilled into the DB automatically.
  • Extended EscrowService with findAll, findByContractEscrowId, linkContractEscrowId, applyChainState (the reconciler's escape hatch for writing chain-verified state, bypassing the release()/raiseDispute() transition guards by design), and createFromChainState.
  • Fixed a latent id-collision bug in EscrowService.create(): esc-${Date.now()} could silently overwrite an existing row if two escrows were created within the same millisecond. Now uses the same timestamp-uuid pattern already used for migration run ids and dispute saga ids elsewhere in this codebase.

Architectural decision (documented per the issue's task list)

There's no contract source/ABI in this repo to verify against, so the Soroban client assumes escrows are stored as persistent contract-data entries keyed by [symbol("Escrow"), string(contractEscrowId)], mirroring the event topic shape event-processor.service.ts already parses. If the deployed contract's actual storage layout differs, only soroban-escrow-chain-state.client.ts needs to change — the rest of the reconciler depends solely on the EscrowChainStateClient interface.

Testing

Automated Testing

  • Unit tests added/updated
  • All tests passing locally

Added specs for the new service, worker, controller, chain client (both simulated and mocked-RPC paths), and the new EscrowService methods — 281 tests passing across 31 suites.

Manual Testing

  • Tested locally: npm run lint:check, npm run format:check, npx tsc --noEmit, npm run test:ci, npm run build all pass clean.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Additional Notes

EscrowService is still an in-memory store (as is the rest of this backend), so "DB" here means the same in-memory Map the other modules already use — the reconciliation logic and DI boundaries are written so a real DB-backed EscrowService and a real deployed contract could be swapped in without touching the reconciler itself.

Adds a deterministic reconciler that diffs each DB-tracked escrow
against its on-chain counterpart (read via Soroban RPC contract data,
falling back to an in-memory simulated store when no contract is
configured, mirroring the IPFS pin providers' credential fallback) and
repairs status/amount drift by trusting chain as the source of truth.
A background worker runs it on an interval; a guarded REST endpoint
also triggers it on demand and can backfill DB rows for contract IDs
supplied explicitly, covering escrows created on-chain whose creation
event was missed.

Also fixes a latent collision in EscrowService's id generation
(esc-${Date.now()} could collide and silently overwrite a row when two
escrows were created in the same millisecond) using the same
timestamp+uuid pattern already used for migration run and dispute saga
ids elsewhere in this codebase.

Closes trustflow-protocol#166
@CillaSam
CillaSam requested a review from meshackyaro as a code owner August 16, 2026 12:54

@meshackyaro meshackyaro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is well-scoped, adds a deterministic reconciler (chain vs DB) with a worker, controller, Soroban chain client (with a simulated mode), tests, and small EscrowService API additions. Tests are thorough and CI-local commands in the description look good.

Again, this is a well-implemented feature with strong tests and good documentation in code/comments. Great job @CillaSam!

@meshackyaro
meshackyaro merged commit 0dfbd74 into trustflow-protocol:main Aug 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Chain↔Database Escrow State Reconciliation

2 participants