Skip to content

feat(cache): make cow_cache reorg-safe and let stale terminal rows heal (COW-1183) - #126

Merged
yvesfracari merged 4 commits into
mainfrom
pedro/cow-1183-programmatic-orders-indexer-make-cow_cache-reorg-safe-and
Jul 27, 2026
Merged

feat(cache): make cow_cache reorg-safe and let stale terminal rows heal (COW-1183)#126
yvesfracari merged 4 commits into
mainfrom
pedro/cow-1183-programmatic-orders-indexer-make-cow_cache-reorg-safe-and

Conversation

@yvesfracari

Copy link
Copy Markdown
Contributor

Closes COW-1183. Design agreed in the Linear discussion — the short version: reorg detection can't work here (block handlers start at "latest" and spend minutes-to-hours catching up after every deploy, during which the API answers from the real tip while our cursor is behind), so terminal statuses are instead trust-gated until they're provably beyond the chain's reorg window.

What changed

Trust rule (src/application/helpers/orderbook/trust.ts, unit-tested): a cached terminal status is only served as final when

  1. validTo passed more than W ago (fills are impossible after validTo, so old orders — the whole backfill flood — are final with zero re-fetching), or
  2. a fetch re-confirmed the status more than W after terminal_since (wall clock is immune to indexer lag: the API can only report a fill after it happened in real time).

Anything else is soft: served, but re-fetched through the existing batched miss paths. A fetch that contradicts a cached terminal status (reorg revert) deletes the cache row.

Self-healing poll set (orderStatusTracker.ts): terminal discreteOrder rows are re-polled until their cache entry hardens — necessary because a status written before the fork block survives Ponder's rollback. API-open contradictions downgrade the row to open and clear executed amounts; terminal-to-terminal flips update status and amounts; TWAP parent totals refresh either way. Open orders keep priority under the per-block cap; cascade-cancelled rows are excluded (their truth is on-chain, and the API is silent about them).

Per-chain window (reorgSafetyWindowSeconds in src/chains/*.ts): finality rounded up plus margin — mainnet 20 min, Gnosis/Avalanche/Sepolia 5, Polygon/BNB 15, L2s 20. Erring long only keeps orders in an already-batched poll longer.

Healing (cache_version): rows written before a column existed are treated as misses with the stale data kept as fallback — generalizes and deletes the executedFee == null hack. Healing is lazy (on next read). setup.ts backfills terminal_since = fetched_at and versions existing rows idempotently.

Coverage: all three cached populations uniformly — order_uid_cache discrete statuses, composable_order owner rows (reconcileOpenCachedRows now refreshes soft-terminal rows too, and persists reverts), and flash-loan enrichment rows (write switched from DO NOTHING to upsert so re-fetches advance the cooling-off anchor).

Verification

  • pnpm test — 171 passed (7 new trust-rule tests, 3 new client tests for soft re-fetch / trusted serve / revert deletion, 3 updated to the new semantics)
  • pnpm typecheck, pnpm lint — clean
  • The CASE WHEN ... = excluded.status terminal_since-preservation upsert validated against a real Postgres 16 (schema-qualified refs in ON CONFLICT DO UPDATE are accepted; same-status re-fetch keeps the anchor, advances fetched_at)

Known residuals (accepted in design review)

  • Off-chain soft cancels after a row hardens stay invisible (pre-existing gap, now narrower — soft-window cancels are caught; can't produce wrong executed amounts)
  • A late on-chain cancel of an already-expired order that reorgs can flip the cancelled/expired label — cosmetic, both terminal with zero executed amounts
  • /trades-based hardening noted as a future optimization if the soft set ever grows enough to matter

classifyCachedRow decides whether a cow_cache terminal row is final
(trusted), needs re-fetching (soft), or is a plain miss. Two-tier rule
from COW-1183: validTo older than the reorg window proves finality
(fills can't happen after validTo); otherwise the row stays soft until
a fetch re-confirms the status more than a window after terminal_since.
CACHE_VERSION gates lazy healing of rows cached before a column existed.
reorgSafetyWindowSeconds on ChainConfig (finality rounded up plus
margin), exposed as REORG_SAFETY_WINDOW_SECONDS keyed by chain ID with
a conservative 20-minute fallback.
Cache rows gain valid_to (order_uid_cache), terminal_since and
cache_version, with an idempotent backfill in setup.ts. Reads in
fetchOrderStatusByUids, getCachedFlashLoanEnrichment and
reconcileOpenCachedRows go through classifyCachedRow: trusted rows are
served as before, soft rows re-fetch with the cached data as fallback,
and a fetch that contradicts a cached terminal status (reorg revert)
deletes the row. terminal_since survives same-status re-fetches so the
cooling-off clock isn't reset. The cache_version gate replaces the
fulfilled-with-null-executedFee special case.
A terminal status written before a fork block survives Ponder's
rollback, so OrderStatusTracker now re-polls terminal rows until the
trust rule hardens them. API-open contradictions downgrade the row to
open and clear executed amounts; terminal-to-terminal flips update
status and amounts. Open orders keep priority under the per-block cap;
cascade-cancelled rows are excluded to avoid ping-ponging with the
parent-cancelled sweep.
@linear-code

linear-code Bot commented Jul 24, 2026

Copy link
Copy Markdown

COW-1183

@yvesfracari
yvesfracari merged commit ff0fd96 into main Jul 27, 2026
2 checks passed
@yvesfracari
yvesfracari deleted the pedro/cow-1183-programmatic-orders-indexer-make-cow_cache-reorg-safe-and branch July 27, 2026 21:22
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.

1 participant