Skip to content

Feature/oracle provenance worker - #469

Merged
techisigu merged 7 commits into
Refract-Protocol:mainfrom
agudajohn793:feature/oracle-provenance-worker
Oct 24, 2026
Merged

techisigu merged 7 commits into
Refract-Protocol:mainfrom
agudajohn793:feature/oracle-provenance-worker

Conversation

@agudajohn793

Copy link
Copy Markdown
Contributor

Closes #247
Closes #248
Closes #249
Closes #250

Summary

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Docs
  • Tests / CI

Checklist

  • I read CONTRIBUTING.md
  • Tests added/updated for the change
  • Local gate passes (lint / typecheck or fmt + clippy + test / build)
  • No secrets, keys, or .env values committed
  • Behavioural changes are documented in the README where relevant

Notes for reviewers

agudajohn793 and others added 4 commits October 21, 2026 12:26
Payout decisions rest on median, weighted quorum and anomaly thresholds,
but there was no consensus module to test and nothing beyond hand-written
examples. This adds a pure consensus module and checks its invariants over
generated adversarial inputs with fast-check.

src/oracle/consensus/consensus.ts
- median(), meetsQuorum(), deviationTolerance()/isAnomalous(), and
  evaluateConsensus() which screens invalid/stale/duplicate sources,
  rejects outliers around the median, enforces a minimum source count and
  a weighted quorum, and returns a trust tier plus a deterministic 64-bit
  decision id.
- Duplicated source ids are invalid in every copy (no double voting), and
  every source that reported counts toward the quorum denominator, so bad
  data cannot lower the bar.

consensus.property.spec.ts asserts, among others:
- median: bounded by min/max, order-independent, monotone, exact under
  translation, stays inside the honest range while honest values
  outnumber adversarial ones, rejects non-finite input
- quorum: scale-invariant, monotone in agreeing weight and in ratio,
  never passes with zero/non-finite total weight
- anomaly threshold: symmetric, monotone in distance and tolerance
- evaluateConsensus: never throws on hostile input (NaN, Infinity,
  duplicates, empty ids), order-independent, deterministic, only agrees
  when count and weighted quorum are met, never counts stale data, and a
  minority cannot pull the value outside honest range ± tolerance

The suite caught a real bug during development: averaging the middle pair
as a/2 + b/2 underflows on subnormals and returned 0 for [5e-324 x4].
That case is now a regression example.

Also restores package.json, which was committed empty in Refract-Protocol#165. Contents
are rebuilt from the package-lock.json root entry (which already lists
fast-check) plus the previous scripts and Jest config, so `npm ci` works
again. Adds `test:property`, `dev` and `typecheck` scripts (the latter two
are already documented in the README). The Jest config now runs ESM-only
transitive deps of @stellar/stellar-sdk v16 (@noble/*, uint8array-extras)
through babel-jest, using the plugin already in devDependencies; this
also un-breaks the existing scval-encoders suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On-chain readings were a bare i128, so consumers and auditors could not
tell a corroborated value from a single-source one without trusting the
backend. oracle-encoding.ts now packs provenance into a u128 that is
published alongside the value.

Layout (MSB first): version (4) | trust tier (4) | agreeing source
count (8) | reserved (8) | source timestamp, Unix seconds (40) |
consensus decision id (64). Version 0 is never valid, so zero means
"no provenance".

- packProvenance / unpackProvenance with strict validation: unknown
  versions, set reserved bits, out-of-range fields and inconsistent
  tier/count pairs (e.g. Corroborated with one source) are rejected.
  Unverified readings cannot be encoded at all.
- provenanceFromDecision maps a ConsensusDecision to provenance; the
  source timestamp is the earliest agreeing observation, floored to
  seconds, so a value never looks fresher than its oldest input.
- encodeReadingForChain + buildUpdateReadingArgs produce ScVal args for
  update_reading(coverage_type: u32, value: i128, provenance: u128).
  The contract has to accept the third argument before the relayer
  switches over.
- OracleReading gains an optional `provenance`. The decision id is kept
  as 16 hex chars rather than a bigint so readings stay JSON-safe for
  the WebSocket feed.
- Golden XDR fixture for the u128 argument, added to the regenerate
  script (existing fixtures are unchanged).
- docs/oracle-provenance.md documents the layout for contract authors
  and auditors.

Tests cover the documented bit layout, a fast-check round-trip over all
valid provenance, every rejection path, decision → provenance mapping,
the ScVal types and the golden fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Oracle polling, consensus and claim settlement ran inside the API
process, so request traffic and API restarts could delay or interrupt
payouts. They now run in a separate worker; the API only serves reads
and the WebSocket feed.

Process roles (src/runtime/process-role.ts)
- PROCESS_ROLE=api | worker | all. OracleScheduler and ClaimScheduler
  do nothing unless the role runs the pipeline.
- Unset means `all` (one process, old behaviour) so existing
  deployments keep paying out until they are split; main.ts logs a
  warning in that case and refuses PROCESS_ROLE=worker.

Worker (src/worker.ts, src/worker/)
- Nest application context (no HTTP app) built from WorkerModule: data
  layer, Stellar, metrics, alerting, reading bus, policy/oracle/claim
  modules. No throttler, API-key guard or request-serving modules.
- Tiny node:http listener on WORKER_HEALTH_PORT (default 4002):
  /health returns 503 when a pipeline stage has not succeeded within
  WORKER_STALE_AFTER_MS (PipelineHeartbeat), so the orchestrator can
  restart the worker alone; /metrics exposes Prometheus metrics.
- Graceful shutdown shared with main.ts (runtime/graceful-shutdown.ts);
  ClaimScheduler still drains an in-flight settlement first.

Worker → API channel (src/oracle/reading-bus.ts)
- ReadingBus with a Redis implementation (latest reading per coverage
  type in a hash + pub/sub for live updates) and an in-memory one for
  PROCESS_ROLE=all / sandbox. A split role without Redis fails at boot
  instead of serving a frozen feed. Messages are validated on receipt.
- OracleScheduler publishes readings to the bus; OracleFeedRelay (API)
  subscribes and broadcasts non-low readings via OracleGateway, which
  gets back the broadcastAlert() / welcome message its spec expects.

Deployment
- docker-compose: `backend` runs with PROCESS_ROLE=api, new `worker`
  service runs dist/worker.js with its own healthcheck; both share one
  env anchor.
- npm scripts start:worker, start:worker:dev, start:worker:prod.
- README documents the tiers and the one-worker-per-relayer-key rule.

Pre-existing breakage fixed in files touched here:
- app.module.ts: removed a truncated import line and a duplicated
  imports block left by a bad merge (the file did not parse).
- claim.scheduler.ts: removed two stray closing braces (did not parse).
- oracle.gateway.ts: OnModuleDestroy is imported from @nestjs/common.

Note: oracle.module.ts / claim.module.ts / oracle.service.ts are empty on
main, so neither entrypoint boots until those are restored. When they
are, OracleModule should import OracleFeedModule rather than declare
OracleGateway itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Oct 21, 2026

Copy link
Copy Markdown

@agudajohn793 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

techisigu and others added 3 commits October 24, 2026 11:08
…(Simple agent)

- docker-compose.yml: merged [markers] — model resolved the conflict markers
- package.json: merged [markers] — model resolved the conflict markers
- src/app.module.ts: merged [markers] — model resolved the conflict markers
- src/claim/claim.scheduler.ts: merged [markers] — model resolved the conflict markers
- src/main.ts: merged [markers] — model resolved the conflict markers
- src/metrics/metrics.service.ts: merged [markers] — model resolved the conflict markers
- src/oracle/oracle-encoding.ts: merged [markers] — model resolved the conflict markers
- src/oracle/oracle-reading.ts: merged [markers] — model resolved the conflict markers
- src/oracle/oracle.gateway.ts: merged [markers] — model resolved the conflict markers
- src/oracle/oracle.scheduler.ts: merged [markers] — model resolved the conflict markers

Co-authored-by: Simple Agent <agent@simple.dev>
…(Simple agent)

- package.json: merged [markers] — model resolved the conflict markers
- src/claim/claim.scheduler.ts: merged [markers] — model resolved the conflict markers

Co-authored-by: Simple Agent <agent@simple.dev>
…(Simple agent)

- package.json: merged [markers] — model resolved the conflict markers
- src/claim/claim.scheduler.ts: merged [markers] — model resolved the conflict markers

Co-authored-by: Simple Agent <agent@simple.dev>
@techisigu
techisigu merged commit 66b53fe into Refract-Protocol:main Oct 24, 2026
1 of 5 checks 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

2 participants