Skip to content

Oracle Score Computation

RunTimeAdmin edited this page Nov 6, 2026 · 33 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, imports payer-signed outcomes from the other operators, 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 chunks sized in calls, 1.5 seconds apart, 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, in a fixed order that does not depend on when each was credited (architecture.md:97-100); 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.
  • External factor: read once per snapshot, from the same payers and to the same limits on both operators, so they read the same rows. A registry read the node cannot answer skips the agent in the same way (external.js:20-24, external.js:178-191, index.js:473-490).
  • Checker audit reconstruction: when auditing a pending proposal, the checker recomputes at the proposal’s recorded time, over the evidence as it stood then, so comparisons reflect what the primary should have computed. It keeps the payments settled by proposedAt, and reads an outcome a payer signed after that as unreported when that is what makes the root match the one the proposal committed to (evidence-as-of.js). For a proposal made before evidence had a fixed order, it tries the operator's stored order too, since that proposal committed to its proposer's (evidence-as-of.js:40-44, index.js:351-362). With a payment chain on, it also tries each whole-minute cut-off in the half hour before the proposal (evidence-as-of.js:32-38, payment-chains.js:261-270).
  • Payment chains: off by default, and a chain in shadow, the default mode, changes no score (payment-chains.js:37-39, payment-chains.js:225-231). A payment from a chain that is on enters a score once older than that chain's delay, with its amount converted into the scored chain's fee units for the arithmetic only (payment-chains.js:299-322, payment-chains.js:324-338).
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, and its paceMs argument is a wait between chunks only.
  • Phase one reads agent info, pending score, and coverage in parallel per chunk. chunkForReads sizes a chunk in calls so it fits one JSON-RPC batch of at most 20, the limit the providers in chain.js and external.js keep to (MAX_BATCH_CALLS, batchMaxCount): 10 agents at two reads each, or 6 at three with fee gating active. The epoch waits READ_PACE_MS, 1.5 seconds, between chunks (epoch-policy.js:295-308, read-retry.js:8-12).
  • Phase two batches guard reads per chunk, waiting READ_PACE_MS before every chunk since its first follows phase one, but processes writes sequentially due to nonce sharing.
  • Payer score lookups are deduplicated and read 10 at a time, 1.5 seconds apart (PAYER_READ_CHUNK, PAYER_READ_PACE_MS), each group awaited together to exploit ethers batching (index.js:427-433, index.js:458-466).
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 finalizes an agreed score or proposes into an empty slot only once the primary is CHECKER_GRACE_MINUTES late (epoch-policy.js:148-222).
  • 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, over the evidence as it stood then, to isolate evidence differences from clock drift and from payments that settled after the proposal.
  • Divergences include evidence roots and event counts to diagnose why operators disagree, and whether the checker's evidence as of the proposal matched the proposal's root (evidenceMatchesProposal, ownEvidenceRootAtProposal). A match with different scores is an arithmetic disagreement, the serious kind.
  • 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 passed by the grace?"}
FinalizeOrSkip --> |Yes| Finalize["finalizeScore()"]
FinalizeOrSkip --> |No| Skip["skip, or defer to the primary"]
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. A log read a node refuses for its size is asked for in halves (log-spans.js).
  • Payer score lookups are deduplicated and awaited concurrently, 10 at a time and 1.5 seconds apart.
  • 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.
  • A node that cannot answer for the ERC-8004 registry is thrown rather than read as a score of 0, and the agent is skipped for the epoch. Only a real revert counts as the registry refusing (external.js:178-191).
  • Watcher webhook delivery failures are logged without stopping the polling loop. Its alerts and logs carry the node's own words with any URL removed, and do not repeat what a checker sent instead of JSON (watcher.js:77-80, watcher.js:91-97).

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 or when the request carries the payer's signed outcome (payerOutcome, which needs no token), and anything other than success: true or a payerOutcome needs a token.
  • /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.
  • /outcomes: Lists the payer-signed outcomes this oracle has taken, oldest first, for other operators to import; 404 unless PAYER_OUTCOMES and PAYER_OUTCOMES_PUBLISH are on.
  • /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; answers 502 with code: "rpc_error" when the node cannot answer for the chain or the ERC-8004 registry.
  • Token-gated endpoints: POST /epoch, /flag, /flag/resolve and /link, plus /attest for an outcome the payer did not sign. 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, evidenceMatchesProposal, and compareWith URL; confirm both operators have the same payment evidence and the same payer-signed outcomes (node scripts/check-live.mjs outcomes).
  • State not persisting: /health shows storeWritable and statePersisted, and the log says where the state file is and what went wrong writing it. Check disk space.
  • 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