Skip to content

Oracle Score Computation

RunTimeAdmin edited this page Oct 29, 2026 · 9 revisions

Oracle Score Computation

**Referenced Files in This Document** - [index.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/index.js) - [scoring.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/scoring.js) - [epoch-policy.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/epoch-policy.js) - [store.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/store.js) - [response-cache.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/response-cache.js) - [metrics.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/metrics.js) - [jobs.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/jobs.js) - [watcher.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/watcher.js)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion

Introduction

This document explains the oracle’s score computation engine: how epochs are scheduled, how reputation is computed and batched, how determinism and state are enforced, how primary and checker operators interact, and how divergence detection, challenge handling, metrics, caching, and error handling work together. It also summarizes the API surface used for attestation, evidence retrieval, job reading, health, and administrative functions.

The system is designed so that two independent operators scoring the same agent from the same on-chain evidence produce identical scores. Deterministic time sources, pure scoring functions, chunked RPC access, durable state, and explicit divergence reporting form the core guarantees.

Project Structure

The oracle implementation lives under oracle/. The most relevant files for score computation are:

  • index.js — HTTP server entry point, epoch scheduler, payment scanning, operator mode control, and orchestration of chain reads, scoring, and proposals/finalizations.
  • scoring.js — Pure reputation factors and total score computation.
  • epoch-policy.js — Epoch scheduling validation, action decisions, checker decision logic, the deadline of a pending proposal (finalizableAtOf), and concurrency helpers.
  • store.js — Durable state for attestations, flags, links, payment events, divergences, and scan checkpoints.
  • response-cache.js — Byte-bounded LRU cache for serialized responses such as evidence proofs.
  • metrics.js — Prometheus-style counters and gauges for monitoring.
  • jobs.js — ERC-8183 job registry reader that converts escrow verdicts into evidence compatible with the existing scoring pipeline.
  • watcher.js — External process that polls a checker’s divergence records and alerts before challenge windows close.
graph TB
Client["Client / Auditor"] --> HTTP["HTTP Server<br/>index.js"]
HTTP --> Chain["Chain Layer<br/>chain.js (external)"]
HTTP --> Store["Durable State<br/>store.js"]
HTTP --> Scoring["Score Engine<br/>scoring.js"]
HTTP --> Policy["Epoch Policy<br/>epoch-policy.js"]
HTTP --> Cache["Response Cache<br/>response-cache.js"]
HTTP --> Metrics["Metrics<br/>metrics.js"]
HTTP --> Jobs["ERC-8183 Jobs<br/>jobs.js"]
Watcher["Divergence Watcher<br/>watcher.js"] --> HTTP
Loading

Diagram sources

Section sources

Core Components

  • Epoch scheduler and runner: validates interval, guards against overlapping runs, scans payments, batches agent reads, computes scores, proposes or finalizes on-chain, and tracks outcomes.
  • Deterministic scoring engine: pure functions for fee, success, age, external, community, and propagation factors; total score is the sum of clamped factor values.
  • Operator policy: decides whether to propose, finalize-then-propose, skip, or diverge based on pending proposals, challenge window, chain clock, and tolerance.
  • Durable store: persists attestations, flags, links, payment events, divergences, and scan checkpoints with atomic writes and decay-aware pruning.
  • Response cache: byte-bounded LRU cache keyed by revision token for expensive evidence responses.
  • Metrics: Prometheus-style exposition for epoch, proposal, finalization, payment scan, checker, and cache performance.
  • Job reader: optional ERC-8183 escrow evidence converted into the same evidence shape used by payments.
  • Divergence watcher: read-only polling service that escalates actionable disputes before challenge windows expire.

Section sources

Architecture Overview

The oracle runs an HTTP server that exposes endpoints for attestation, evidence, jobs, health, metrics, and administrative operations. Internally it coordinates:

  1. Configuration and startup checks for RPC, private key, addresses, epoch interval, checker mode, divergence tolerance, and optional fee registry.
  2. Chain initialization and optional external identity/reputation feeds.
  3. Loading of persistent state for attestations, flags, links, payment events, divergences, and scan checkpoints.
  4. Scheduled epoch execution that:
    • Scans verified payments across a block range.
    • Reads registered agents and their on-chain status concurrently in chunks.
    • Applies bond and slash filters.
    • Computes deterministic scores using chain time.
    • Proposes or finalizes according to policy.
    • In checker mode, compares its own score with the pending proposal and records divergence when needed.
  5. Caching of expensive evidence responses.
  6. Exposure of Prometheus metrics.
sequenceDiagram
participant Scheduler as "Epoch Scheduler"
participant Index as "index.js"
participant Chain as "Chain Layer"
participant Store as "store.js"
participant Scoring as "scoring.js"
participant Policy as "epoch-policy.js"
participant Contract as "SigvaraReputation"
Scheduler->>Index : Trigger runEpoch()
Index->>Store : Load persistent state
Index->>Chain : getRegisteredAgents()
Index->>Index : runPaymentScan(agents)
Index->>Chain : mapChunked(agent reads)
Index->>Policy : decideAction(...) or decideCheckerAction(...)
alt Propose path
Index->>Scoring : computeScore({now=chainNow})
Index->>Contract : proposeReputation(...), or proposeIfEmpty(...) in checker mode
Contract-->>Index : tx result
else Finalize path
Index->>Contract : finalizeReputation(didHash)
Contract-->>Index : tx result
end
Index->>Store : persistState()
Loading

Diagram sources

Detailed Component Analysis

Epoch Processing Pipeline

The epoch pipeline enforces safe timing, idempotent behavior, and bounded RPC pressure:

  • Interval validation prevents misconfiguration from running epochs continuously.
  • A global guard prevents overlapping epoch runs.
  • Before anything is read for scoring or signed, the epoch checks which chain the RPC serves (against EXPECTED_CHAIN_ID when set, and against the chain the state file was saved on), warns about transactions from the oracle wallet still pending from an earlier epoch, and compares the chain's latest block time with the host clock. A gap past MAX_CLOCK_SKEW_SECONDS (600 by default) means one of the two is wrong, and no epoch runs. A failed check aborts that epoch, which is counted, and the next tick tries again.
  • Payment scanning runs before scoring so the epoch reflects chain state rather than per-operator HTTP delivery.
  • Phase one performs concurrent reads in fixed-size chunks to avoid RPC bursts.
  • Phase two applies sequential write guards while batching guard reads.
  • Slashed agents are pruned; uncovered agents are skipped when fee gating is active.
  • Scores are computed using chain time, not host time.
  • Proposals and finalizations are tracked with metrics and robust error handling.
flowchart TD
Start(["Epoch start"]) --> ValidateInterval["Validate EPOCH_HOURS"]
ValidateInterval --> GuardOverlap{"Already running?"}
GuardOverlap --> |Yes| Skip["Skip trigger"]
GuardOverlap --> |No| PreFlight["Check chain id and clock skew"]
PreFlight --> ScanPayments["runPaymentScan(agents)"]
ScanPayments --> ReadAgents["mapChunked(agentInfo, pendingScore, covered)"]
ReadAgents --> FilterAgents["Filter slashed/uncovered/errors"]
FilterAgents --> GuardBatch["Guard chunk: fresh info + bonded"]
GuardBatch --> Decide{"Primary or Checker?"}
Decide --> |Primary| PrimaryFlow["decideAction -> propose/finalize/skip"]
Decide --> |Checker| CheckerFlow["decideCheckerAction -> compare/diverge/finalize/skip"]
PrimaryFlow --> Compute["computeScore(now=chainNow)"]
CheckerFlow --> Compute
Compute --> Persist["persistState()"]
Persist --> End(["Epoch end"])
Loading

Diagram sources

Section sources

Deterministic Computation Requirements

Determinism is central to multi-operator agreement:

  • Time source: scoring uses chain timestamp converted to milliseconds, not Date.now().
  • Pure scoring: all six factors are pure functions over inputs.
  • Evidence roots: payment events are committed via Merkle roots; divergences record each side’s evidence root.
  • Payer standing cache: cleared each epoch to avoid stale cross-agent trust. A throttled or dropped read of a payer's standing is retried; one that still fails skips the agent for that epoch instead of reading as no standing, which would lower the proposal and make an honest checker record a divergence.
  • Checker audit reconstruction: when auditing a pending proposal, the checker recomputes at the proposal’s recorded time so comparisons reflect what the primary should have computed.
flowchart TD
Input["Agent data + payment events"] --> Now["now = chain.timestamp * 1000"]
Now --> Factors["feeScore, successScore, ageScore,<br/>externalScore, communityScore, propagationScore"]
Factors --> Clamp["Clamp factors to contract caps"]
Clamp --> Sum["total = sum(clamped factors)"]
Sum --> Output["Deterministic score object"]
Loading

Diagram sources

Section sources

Batch Processing of Reputation Calculations

Batching avoids both RPC bursts and sequential round trips:

  • mapChunked limits concurrent mapper calls while preserving input order.
  • Phase one reads agent info, pending score, and coverage in parallel per chunk.
  • Phase two batches guard reads per chunk but processes writes sequentially due to nonce sharing.
  • Payer score lookups are deduplicated and awaited together to exploit ethers batching.
flowchart TD
Agents["Registered agents"] --> Chunk["Split into chunks"]
Chunk --> ParallelReads["Promise.all(agentInfo, pendingScore, covered)"]
ParallelReads --> Collect["Collect agentInfos in order"]
Collect --> GuardBatch["Batch bonded/guard reads"]
GuardBatch --> SequentialWrites["Sequential propose/finalize per agent"]
Loading

Diagram sources

Section sources

Multi-Operator Architecture: Primary and Checker

  • Primary operator proposes new scores and finalizes after the challenge window.
  • Checker operator audits pending proposals independently and only proposes into empty slots or finalizes agreed scores.
  • On main, a pending proposal whose proposer has lost its authority cannot be finalized. The primary replaces it with a fresh proposal, and a checker treats it as an empty slot once a dry run of proposeIfEmpty shows the contract would accept it (counted as proposalsStranded).
  • Divergence detection compares totals and per-factor deltas against a configured tolerance.
  • Divergences are persisted with evidence roots and diagnostic metadata.
  • The watcher monitors checker divergence records and alerts before challenge windows close.
classDiagram
class Primary {
+proposeReputation()
+finalizeScore()
+decideAction(pending, window, now)
}
class Checker {
+proposeIfEmpty()
+comparePendingProposal()
+decideCheckerAction(pending, own, window, now, tolerance)
+recordDivergence()
}
class Policy {
+scoreDivergence(pendingData, own)
+DEFAULT_DIVERGENCE_TOLERANCE
}
class Store {
+recordDivergence()
+getDivergences()
+pruneDivergences()
}
Primary --> Policy : "uses"
Checker --> Policy : "uses"
Checker --> Store : "writes"
Loading

Diagram sources

Section sources

Challenge Handling and Divergence Detection

  • Divergence detection reports both total difference and per-factor deltas.
  • Tolerance validation prevents silent agreement or meaningless disagreement.
  • Checker re-measures at the proposal’s recorded time to isolate evidence differences from clock drift.
  • Divergences include evidence roots and event counts to diagnose why operators disagree.
  • The watcher escalates actionable disputes and issues final warnings as windows close.
flowchart TD
Pending["Pending proposal"] --> Audit["Recompute at proposedAt"]
Audit --> Compare["scoreDivergence(pending, own)"]
Compare --> Tolerance{"Within tolerance?"}
Tolerance --> |No| Record["recordDivergence(evidence)"]
Tolerance --> |Yes| FinalizeOrSkip{"Window elapsed?"}
FinalizeOrSkip --> |Yes| Finalize["finalizeScore()"]
FinalizeOrSkip --> |No| Skip["skip"]
Loading

Diagram sources

Section sources

State Management and Checkpointing

  • Attestations, flags, links, cooldowns, payment events, used-payment transaction keys, divergences, and scan checkpoints are persisted.
  • Flags are stored as timestamps and decayed using the same curve as payments.
  • Payment events and the arrays that hold them are frozen. A credit, a later outcome or a prune replaces the array, which supports revision-based cache invalidation and lets merkle.js cache leaves and roots by identity.
  • Atomic file writes use temp file, fsync, and rename to prevent partial state. The file is written in pieces of about 1 MB rather than as one string, and the file it replaces is kept as <state>.bak.
  • A state file that cannot be read falls back to the .bak (reported as stateSource: backup on /health), and if neither is usable the process stops instead of starting empty. The file records its chain id and a schema version, and one from another chain or from a newer build is refused.
  • Scan checkpoints ensure restarts resume without replaying entire chains.
flowchart TD
Event["Attestation / Flag / Payment / Divergence"] --> Memory["In-memory Map/Set"]
Memory --> Persist["persist(): JSON + fsync + rename"]
Persist --> File["oracle-state.json"]
File --> Load["load(): migrate legacy flags, validate events"]
Load --> Memory
Loading

Diagram sources

Section sources

Metrics Collection, Monitoring, and Alerting

  • Counters track epochs, proposals, finalizations, fee charges, attestations, payment verification, payment scans, checker comparisons, throttling, and cache hits.
  • Gauges expose uptime, last successful epoch timestamp, and active agents.
  • Evidence cache statistics can be included in exposition.
  • The watcher provides operational alerting for checker outages and actionable divergences.
graph TB
Index["index.js"] --> Metrics["metrics.inc()/set()"]
Metrics --> Prometheus["toPrometheusText()"]
Prometheus --> Export["/metrics exposition"]
Watcher["watcher.js"] --> Webhook["Webhook alerts"]
Loading

Diagram sources

Section sources

Performance Optimization and Caching Strategies

  • Chunked RPC reads limit burst pressure and leverage provider batching.
  • Payer score lookups are deduplicated and awaited concurrently.
  • Evidence response cache is byte-bounded LRU with revision invalidation.
  • Payment event arrays are frozen to preserve revision identity.
  • Merkle leaves are built by hand rather than through the ABI coder and cached against each frozen event, and roots against each frozen array, so an agent whose evidence has not changed reuses its root (5,000 events took 1,450 ms before and 480 ms with the hand-built encoding, and a cache hit is a lookup).
  • Flag and payment event pruning removes low-weight history safely.
flowchart TD
Request["Evidence request"] --> Key["Key + revision"]
Key --> Cache["response-cache.get(key, revision, build)"]
Cache --> |Hit| Return["Return cached body"]
Cache --> |Miss| Build["Build proof + serialize"]
Build --> Size{"Size <= maxBytes?"}
Size --> |Yes| Store["Store in LRU"]
Size --> |No| ServeOnly["Serve without storing"]
Store --> Return
ServeOnly --> Return
Loading

Diagram sources

Section sources

Error Handling

  • Epoch failures are caught and logged without killing the process; the next tick retries.
  • Payment scan failures do not abort scoring; checkpointing ensures resumption. A scan whose canary fails does not advance its checkpoint, so the range is scanned again at the next epoch.
  • Proposal and finalization errors distinguish rate limiting, slot taken, and real failures.
  • State persistence failures are exposed through health and last-persist tracking.
  • Watcher webhook delivery failures are logged without stopping the polling loop.

Section sources

API Endpoints Summary

The following endpoints are referenced by the codebase. Exact route definitions live in the HTTP layer; this section summarizes behavior derived from the implementation.

  • /attest — Accepts attestation payloads; enforces cooldown and payment verification. A verified payment is credited for any caller sending success: true, but an outcome is stored only for a caller holding an oracle token, and anything other than success: true needs one.
  • /flag — Raises flags against agents (token required); /flag/resolve clears them, and flags decay on a half-life.
  • /evidence/:didHash — Returns Merkle evidence and proofs for an agent; served from response cache when revision matches.
  • /jobs — Reads ERC-8183 job logs and returns assembled job evidence when configured.
  • /health — Reports configuration readiness, state writability, and basic service status. It answers 503 when the state path is not writable, when the last state save failed, or when two epoch intervals pass with no successful epoch (counted from process start until the first one succeeds).
  • /metrics — Exposes Prometheus-style counters and gauges.
  • /divergence — Returns recent divergences recorded by checker mode.
  • /score/:didHash — Retrieves computed score information; may proxy upstream reads and report RPC errors.
  • Token-gated endpoints: POST /epoch, /flag, /flag/resolve and /link, plus /attest for outcomes. Each credential is a named bearer token: ORACLE_ADMIN_TOKEN, which is never restricted, or an ORACLE_TOKEN_<NAME>, which ORACLE_SCOPES_<NAME> can limit to the scopes attest, flag, link and epoch.

Operational notes:

  • Reads and writes are limited to 60 requests per caller per minute, in a separate bucket for each kind of route, and writes are metered before they are authorised.
  • A request target that is not already a canonical path gets 400 bad request target, and a handler that throws costs one request rather than the process.
  • Credentials are parsed at startup, and a non-loopback bind with none configured refuses to start.
  • Health and metrics are essential for monitoring liveness, state persistence, and performance.

Section sources

Dependency Analysis

The oracle’s internal dependencies emphasize separation of concerns:

  • index.js orchestrates chain I/O, scoring, policy, store, cache, metrics, and optional job reading.
  • scoring.js has no I/O and depends only on pure math.
  • epoch-policy.js contains pure decision functions and concurrency helpers.
  • store.js encapsulates persistence, decay, and deduplication.
  • response-cache.js is a generic LRU cache with revision semantics.
  • metrics.js is a lightweight counter/gauge module.
  • jobs.js is optional and decoupled behind configuration.
  • watcher.js is external to the oracle process and communicates over HTTP.
graph LR
Index["index.js"] --> Scoring["scoring.js"]
Index --> Policy["epoch-policy.js"]
Index --> Store["store.js"]
Index --> Cache["response-cache.js"]
Index --> Metrics["metrics.js"]
Index --> Jobs["jobs.js"]
Watcher["watcher.js"] --> Index
Loading

Diagram sources

Section sources

Performance Considerations

  • Use chunked RPC reads to avoid provider rate limits and reduce burst pressure.
  • Prefer concurrent awaits within a single tick to benefit from provider batching.
  • Deduplicate payer score lookups and clear caches per epoch.
  • Keep evidence responses small enough to fit in the response cache budget; large responses are served but not cached.
  • Prune old flags and payment events to bound state growth.
  • Monitor metrics for propose/finalize throttling, payment scan canary failures, and evidence cache hit ratios.

Troubleshooting Guide

Common operational issues and diagnostics:

  • Epoch never advances: check EPOCH_HOURS, RPC connectivity, and epochsStarted vs epochsFailed. An epoch aborts, and is counted as failed, when the RPC serves a different chain from EXPECTED_CHAIN_ID or from the state file's, or when the chain clock and the host clock differ by more than MAX_CLOCK_SKEW_SECONDS.
  • Payments not reflected: verify payment scan configuration, asset setting, and paymentScanCanaryFailures; inspect paymentsPulled.
  • Divergences reported: review per-factor deltas, evidence roots, and compareWith URL; confirm both operators have the same payment evidence.
  • State not persisting: check isStatePathWritable, disk space, and lastPersist.ok; inspect getStatePath.
  • High RPC errors: monitor scoreRpcErrors, paymentRpcErrors, and throttle counters; consider provider upgrades or backoff tuning.
  • Checker silence: watcher will alert if the checker is unreachable or silent beyond maxSilenceMs.

Section sources

Conclusion

The oracle’s score computation engine combines deterministic scoring, careful epoch scheduling, chunked RPC access, durable state, and explicit divergence reporting to support a secure multi-operator architecture. Primary operators propose and finalize scores, while checker operators independently audit and alert on disagreements. Metrics and the watcher provide operational visibility, and caching strategies keep expensive evidence responses efficient. Proper configuration of epoch intervals, divergence tolerance, payment verification, and state persistence is essential for reliable operation.

Overview

Clone this wiki locally