Repository navigation
Oracle consensus governance: payout holds, point-in-time reconstruction, mirror detection, provider offboarding - #474
Merged
OxDev-max merged 6 commits intoOct 23, 2026
Conversation
…t-Protocol#272) Adds the consensus evidence layer (append-only ledger, versioned source and quorum-policy config, pure consensus engine). On top of it, adds an evidence-completeness precheck that runs before settlement. A triggered claim whose decision has gaps (missing raw payload, fewer contributors than expected, a warm-up source, a pending signature) moves to `payout_held` with a structured gap list. It is not paid and not silently denied. Held claims are re-evaluated on a configurable cadence. They release automatically once a decision is complete and still triggered. They escalate to an operator queue on deadline, on a contradicting verdict, or when the policy expires while held. All transitions are ledger and audit events, so the queue survives restarts. The gate fails closed: if the ledger cannot be written, the payout is blocked and retried on the next scan. Feeds without a quorum policy are not governed and settle exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rotocol#271) Adds reconstructConsensusState(), a pure function that rebuilds a feed's consensus state at an ISO timestamp or a ledger sequence from persisted evidence only. The result includes the quorum policy version in force, per-source eligibility, the contributing readings, the recomputed decision, and the decision actually recorded at the time. Only events recorded at or before T are visible. Ledger timestamps are strictly increasing, so the answer is stable as the ledger grows and after payload bodies are purged. The API (GET /api/v1/ops/consensus/feeds/:feedId/state, operator or auditor role) and the CLI (`npm run oracle:ops -- reconstruct`) share the function and a canonical serialiser, so their output is byte-identical. A seeded property test checks that reconstruction reproduces every live decision across randomised histories. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct-Protocol#273) Each ingested reading now stores a fingerprint: sha256 of the feed, the canonical value and a coarse time bucket. A periodic batch scan, run off the hot path, compares every source pair on a feed over a sliding window. It aligns the pair at the best lag, so a delay can't hide a copy, and counts only buckets where the series moved, so flat markets prove nothing. A pair is flagged only on sustained near-exact agreement across several distinct values. A flagged pair gets a persisted independence_penalty with the window, lag, statistics and matched-reading evidence. Consensus then scales down the lighter source's weight. That source also stops counting toward the minSources quorum and toward a payout's expectedSources. Reconstruction replays penalties as of T. Penalties clear when the pair diverges or a source is retired. Results are exposed in a trust-status view, as Prometheus metrics, and in a read-only `oracle-ops mirror-backtest` command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ontinuity (Refract-Protocol#274) Retiring a source is now a designed operation and never a delete: (a) a new source version marks it retired, removing it from consensus and rejecting new readings; (b) its stored credential is deleted through a pluggable credential store (in-memory, or AWS Secrets Manager); (c) raw payload bodies older than the licence retention are purged while every hash, value and decision reference stays in the ledger; (d) each step is recorded with actor, reason and purge scope. planOffboarding() is a pure dry run shared by the API and `oracle-ops offboard-plan`. Execution follows the same plan. Bodies behind open payout holds, or shared with an active source, are retained. Bodies still inside retention are purged later by a scheduled sweep. Removing a source that would drop a feed below quorum needs explicit acknowledgement. Partial failures are resumable and idempotent under the same offboarding id. Re-onboarding appends a new version, so each historical instant maps to one configuration. Past decisions reconstruct byte-identically after a purge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@Sundriveauto 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! 🚀 |
…nce-consensus-holds # Conflicts: # package.json # src/app.module.ts # src/claim/claim.service.ts # src/oracle/oracle.controller.ts # src/oracle/oracle.scheduler.spec.ts # src/oracle/oracle.service.spec.ts # src/oracle/oracle.service.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a consensus evidence layer for oracle feeds and builds four governance features on it. Each issue is a separate commit:
0531b906c3b0c3da81ed5c8212afEverything lives in
src/oracle/consensus/. Operator docs are indocs/runbooks/oracle-consensus.md.Shared foundation (first commit)
evidence-ledger.ts). Every fact is an immutable event: source versions, quorum-policy versions, readings, signature results, decisions, holds, penalties, purges and audit entries.EVIDENCE_LEDGER_PATHand are reloaded on boot. The CLI reads the same file.ledger-view.ts) and a pure consensus engine (consensus.engine.ts: window filter, eligibility, quorum, weighted median). Live decisions and historical reconstruction use the same functions, so there is one definition of "state at T".consensus-role.guard.ts).OPS_API_TOKENgrants theoperatorrole (read/write). Tokens inAUDITOR_API_TOKENS(CSV) grant the read-onlyauditorrole. Comparison is timing-safe.#272 Payout holds on incomplete or immature evidence
assessEvidenceCompleteness()is a pure function that returns structured gaps:missing_raw_payloadinsufficient_sources: fewer independent contributors than the policy'sexpectedSourcessource_in_warmupsignature_pendingClaimService.processPayoutnow callsClaimHoldService.gate()before settling. A triggered claim with gaps moves topayout_heldwith the gap list and a human-readable reason. It is not paid and not silently denied.payout_held → released → settled, andpayout_held → escalated → approved | denied, withextend_holdreturning a claim topayout_held. A hold can't be mistaken for a denial.CLAIM_HOLD_REEVALUATE_INTERVAL_MS:deadline_exceeded(CLAIM_HOLD_MAX_DURATION_MS),verdict_contradicted(newer evidence says "don't pay"; never auto-denied), orpolicy_expired_while_held.GET /api/v1/ops/consensus/holds?status=escalatedandGET /holds/:holdIdPOST /holds/:holdId/dispositionwith a requiredactorandnotenpm run oracle:ops -- holds#271 Point-in-time consensus state reconstruction
reconstructConsensusState(events, { feedId, at | ledgerSeq })is a pure function of persisted events. It never uses live config, caches or provider calls. It returns:no_decisionwith a reasonconsistencyfield:matches_recorded,differs_from_recordedorno_decision_recorded_at_tledger=Lresolves T to the last event stamped with ledger ≤ L and excludes events stamped with a later ledger.GET /api/v1/ops/consensus/feeds/:feedId/state?at=<ISO>|ledger=<seq>(operator or auditor).npm run oracle:ops -- reconstruct --feed … --at …. It shares the canonical serialiser with the API, and a test asserts the two outputs are byte-identical.matches_recorded. A mutation check confirmed the suite fails if reconstruction can see events recorded after T. Decisions recorded before theindependentflag existed still reconstruct asmatches_recorded.Sample reconstruction output (CLI)
{"consistency":"matches_recorded", "decision":{"outcome":"decision","policyVersion":1,"reason":"triggered: 0.915 lt 0.95","triggered":true,"value":0.915, "contributing":[{"effectiveWeight":1,"independent":true,"readingId":"rd_385163c19445f885b2929f4d","sourceId":"binance","value":0.918}, "…"]}, "contributingReadings":[{"ledgerSeq":51234500,"payloadHash":"9bc27804…","readingId":"rd_c1af435da5ad35e4d7c655bc","signature":"pending","sourceId":"kraken","value":0.915}, "…"], "independencePenalties":[], "knowledgeCutoffSeq":11, "query":{"at":"2026-10-21T14:00:03.002Z","feedId":"StablecoinDepeg","ledgerSeq":null}, "quorumPolicy":{"expectedSources":3,"maxReadingAgeMs":300000,"minSources":2,"minTotalWeight":2,"trigger":{"op":"lt","threshold":0.95},"version":1, "…"}, "recordedDecision":{"decisionId":"dc_3030dc24f264636fa17b7ae9","atIso":"2026-10-21T14:00:03.002Z","outcome":"decision","triggered":true,"value":0.915, "…"}, "resolvedAtIso":"2026-10-21T14:00:03.002Z", "sources":[{"eligible":true,"immature":false,"independent":true,"mirrorOf":null,"providerId":"binance","reason":null,"sourceId":"binance","status":"active","version":1,"weight":1, "…"}, "…"]}Pretty-printed and trimmed (
…). The real output is a single canonical line.#273 Cloned or mirrored source detection
Fingerprint. Each ingested reading stores
sha256(feedId, canonical value, time bucket). Values are canonicalised to 12 significant digits, so formatting and float noise don't defeat matching.Batch detection (
MIRROR_SCAN_INTERVAL_MS), off the hot path. It compares every source pair within a feed over a sliding window:MIRROR_MAX_LAG_BUCKETS, so delaying a copy doesn't hide it.MIRROR_RELATIVE_TOLERANCE, so small jitter doesn't hide it either.Penalty. A flagged pair gets a persisted
independence_penaltywith the window, lag, statistics and up to 10 matched reading pairs as evidence. Consensus then:MIRROR_PENALTY_FACTOR;minSourcesquorum and toward a payout'sexpectedSources.Reconstruction replays penalties as of T. A penalty clears when the pair diverges or a source is retired; quiet periods never clear it.
Operator view:
GET /api/v1/ops/consensus/trust-statusshows sources, declared upstream, and suspected pairs with evidence.Metrics:
refract_oracle_suspected_mirror_pairs{feed}andrefract_oracle_mirror_penalty_changes_total{feed,action}.Backtest:
npm run oracle:ops -- mirror-backtestreplays detection over the whole ledger and writes nothing.Tests: an exact mirror; a near-mirror that is delayed and jittered; agreement on a flat feed; independent noisy sources tracking one market; a categorical feed that toggles; a cross-feed false-positive check; clearing; the quorum effect; reconstruction replay; the payout gate; metrics.
Backtest analysis output (synthetic: 24 h, 4 sources, "reseller" copies CoinGecko with a 15 s delay)
Honest pairs top out around 26% matches against the 90% threshold, so the margin is wide.
#274 Provider offboarding
POST /api/v1/ops/consensus/sources/:id/offboardruns four stages:retired. It leaves consensus, and new readings from it are rejected with409. It is never deleted.SourceCredentialStore: AWS Secrets Manager (SOURCE_CREDENTIALS_BACKEND=aws-secrets-manager) or in-memory.retentionDaysare deleted. Hashes, values, readings and decision references all stay.Details:
dryRun: trueornpm run oracle:ops -- offboard-plangives the exact plan. Both use the same pureplanOffboarding(), and a test asserts that execution follows the dry-run plan exactly.409unlessacknowledgeQuorumLossis set.GET /payloads/:hashshows whether a body is held, whether it matches its hash, or when and why it was purged.Dry-run output (CLI)
{ "alreadyRetired": false, "credential": { "action": "revoke", "credentialRef": "refract/sources/acme-data" }, "feedId": "StablecoinDepeg", "generatedAt": "2026-10-21T15:00:00.000Z", "openDisputes": [], "payloads": { "alreadyPurged": 0, "purge": [{ "hash": "08aee184…f668d", "storedAt": "2026-10-01T00:00:04.000Z" }], "retain": [{ "hash": "4baf00b7…fbe1aa", "purgeAfter": "2026-10-25T00:00:04.000Z", "reason": "within_license_retention", "storedAt": "2026-10-18T00:00:04.000Z" }] }, "preserved": { "decisions": 2, "payloadHashes": 2, "readings": 2 }, "providerId": "acme-data", "quorum": { "dropsBelowQuorum": false, "minSources": 2, "policyVersion": 1, "remainingEligibleSources": 3 }, "retireAsVersion": 2, "sourceId": "acme" }Type of change
Checklist
main; see below..envvalues committeddocs/runbooks/oracle-consensus.md)Verification
claim-hold.service.spec.tsreconstruction.spec.ts, which includes HTTP tests via@nestjs/testing+ supertest for RBAC, byte-identical API vs CLI output, and the offboarding dry runmirror-detection.spec.tsoffboarding.spec.tsnpx jest, compared per suite with--json:3943531: 23 tests pass; 5 suites pass and 33 fail.tscoversrc/oracle/consensus/**,scripts/oracle-ops.tsandsrc/metrics/metrics.service.tsis clean.eslinton all changed files is clean.npm run oracle:ops -- reconstruct | holds | mirror-backtest | offboard-planwere run end to end throughts-node. The samples above come from those runs.Notes for reviewers
npm run typecheckandnpm run buildalready fail onmain: 37 syntax errors inpolicy/policy.service.ts,claim/claim.scheduler.tsandhealth/health.controller.ts. 33 test suites already fail to compile, includingclaim.service.spec.ts. There are also missing modules (./auth,./claims,./redis,./audit), andOpsAuthGuardreads anopsconfig key thatconfiguration.tsdoesn't define. That is why the consensus endpoints have their own small role guard instead ofOpsAuthGuard. CI will stay red until those are fixed.ClaimServiceintegration. The gate is injected with@Optional(), so existing wiring keeps working. I verified hold → release → settle-once throughClaimService.processTriggered()with the broken neighbours mocked. That spec isn't committed, because it can't type-check untilpolicy.service.tsandclaim-settlement.service.tsare fixed. I can add it once they are.3943531("symmetrical"). It was already on this branch and is not in upstreammain, so it appears in this PR's diff.Not done in this PR:
schema.sqlor migration was added, because the repo's Postgres wiring isn't functional onmain(for example,PG_POOLis imported but never exported). The ledger interface is small (append,events), so a Postgres adapter can replace it.OracleServicestill produces single-source readings. Multi-source readings enter throughPOST /api/v1/ops/consensus/readings. Until a feed has a quorum policy, it isn't governed and behaviour is unchanged.mirror-backteston real data before enabling penalties in production.Closes #271
Closes #272
Closes #273
Closes #274
🤖 Generated with Claude Code