Repository navigation
Oracle Production Deployment
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
- Appendices
This document provides production-grade deployment guidance for the Sigvara Oracle service. It covers container orchestration with Docker Compose, reverse proxy configuration using Caddy or Nginx, environment configuration for blockchain RPCs and external integrations, scaling patterns, health checks, monitoring, security hardening, backup and disaster recovery procedures, and performance tuning recommendations. The guidance is grounded in the repository’s Oracle implementation and deployment artifacts.
The Oracle service is a Node.js application packaged as a container image and orchestrated via Docker Compose. Key deployment-related files include:
- Container build definition
- Compose file defining service, networking, and volumes
- Reverse proxy examples for public exposure (Caddy and Nginx)
- Application entrypoint and HTTP API
- Persistent state storage module
graph TB
subgraph "Host"
Caddy["Caddy"]
Nginx["Nginx"]
end
subgraph "Docker Network sigvara-mesh"
Oracle["Oracle Service<br/>port 3030"]
StateVol["Volume oracle_state<br/>(/data/oracle-state.json)"]
end
Chain["Blockchain RPC"]
ExternalRPC["External RPC / ERC-8004"]
Caddy --> Oracle
Nginx -.-> Oracle
Oracle --> Chain
Oracle --> ExternalRPC
Oracle --> StateVol
Diagram sources
- docker-compose.oracle.yml:1-24
- Caddyfile.oracle.example:37-151
- nginx-checker.conf.example:23-82
- index.js:86-130
Section sources
- Oracle service process: Node.js HTTP server exposing read endpoints (/health, /metrics, /score, /evidence, /badge), write endpoints (/attest, /flag, /link, /epoch), and checker-specific endpoints (/divergence).
- Persistent state store: JSON-backed durable store for attestations, flags, links, payment events, cooldowns, divergence records, and scan checkpoints.
- Blockchain integration: Reads/writes to the primary chain and optional external registry feeds.
- Reverse proxies: Caddy example for public read surface; Nginx example for checker operator.
Key responsibilities:
- Orchestrate epoch scoring, payment scanning, and on-chain proposals/finalizations.
- Provide auditable evidence and scores over HTTP.
- Persist non-reproducible state to a mounted volume.
- Expose metrics for Prometheus scraping.
Section sources
The Oracle runs inside a container bound to host loopback. A reverse proxy exposes only the necessary public paths while keeping administrative endpoints private. State persists to a named volume. The service communicates with one or more blockchain RPCs and optionally an external RPC for ERC-8004 data.
sequenceDiagram
participant Client as "Client"
participant Caddy as "Caddy"
participant Oracle as "Oracle Service"
participant Store as "State Store"
participant Chain as "Blockchain RPC"
participant Ext as "External RPC"
Client->>Caddy : GET /score/ : didHash
Caddy->>Oracle : reverse_proxy : 3030
Oracle->>Chain : getAgentInfo(), getTotalScore()
Oracle->>Ext : externalScoreFor() (optional)
Oracle-->>Caddy : JSON score
Caddy-->>Client : 200 OK
Note over Oracle,Store : Attestations, flags, payments, divergences persisted to /data
Diagram sources
- Service name and container name are defined for stable DNS resolution within Docker networks.
- The service binds to host loopback only, minimizing exposure.
- Environment variables are loaded from an .env file.
- A named volume persists attestation/flag/payment state across restarts and rebuilds.
- The service joins a shared external network used by other components (for example, CounterAudit).
Operational notes:
- Ensure the external network exists before starting the stack.
- Mount a persistent volume at /data to avoid losing state.
- Keep admin endpoints off the public internet; use loopback access or a secure management plane.
Mainnet:
-
docker-compose.mainnet.ymlis the Arc mainnet arrangement. The primary, the checker and the watcher are three profiles of one file;COMPOSE_PROFILESin a host's.envpicks one, and the three belong on three separate hosts. - It builds from
SIGVARA_REF, a full 40-character commit hash. A deploy is a change to that line followed bydocker compose up -d; a restart reruns the same image and fetches nothing. - The signing key is a file secret, read through
ORACLE_PRIVATE_KEY_FILE, so it is not in the container's environment. - Each container runs as the unprivileged
nodeuser on a read-only root filesystem with every capability dropped and a 768 MB memory limit. The only writable places are/dataand/tmp. Only the primary joins the edge network it shares with the reverse proxy.
Section sources
- Base image: Node 24 Alpine, pinned by digest (the VPS compose files, which fetch one pinned commit at container start, still run
node:20-alpine). - Deterministic dependency installation using package-lock.json (
npm ci --omit=dev --ignore-scripts). - Source copied wholesale; .dockerignore excludes secrets, local state, tests and probe scripts.
- Build-time static check validates that all local requires resolve, preventing runtime MODULE_NOT_FOUND errors.
- The commit the image was built from is passed as
SIGVARA_COMMIT, which/healthreports. When it is given, the build stops unless it is a full 40-character hash. - The process runs as the unprivileged
nodeuser,/datais created owned by that user, and aHEALTHCHECKprobes/health. - Entrypoint runs index.js.
Best practices:
- Pin base image versions and lockfiles.
- Run the image with minimal privileges and read-only root filesystem where possible (the mainnet compose file does).
- Use multi-stage builds if you need to separate build tools from runtime.
Section sources
Public read surface:
- Only explicitly listed paths are proxied: /health, /score/, /evidence/, /badge/*.
- POST /attest is proxied with CORS headers and no-store cache policy.
- Read endpoints set short Cache-Control to absorb bursts without serving stale data.
- X-Forwarded-For is preserved so per-client rate limiting works behind the proxy. The oracle believes the header only from loopback or from a peer listed in
TRUSTED_PROXIES. Caddy reaches the oracle from a Docker bridge address, so setTRUSTED_PROXIESto the proxy network (for example172.16.0.0/12); without it every visitor shares one rate-limit bucket. - Security headers include HSTS, nosniff, referrer policy, and Server header removal.
Important behaviors:
- The oracle listens on 127.0.0.1:3030; Caddy reaches it via Docker service name, not host loopback.
- Administrative endpoints (/flag, /flag/resolve, /link, /epoch) remain loopback-only.
- /metrics is intentionally excluded from the public proxy.
Section sources
Use this when the host already runs Nginx and you need to expose checker endpoints without running another web server.
Exposed endpoints:
- /health: liveness and watcher polling
- /divergence and /divergence/:didHash: disagreement collection
- /evidence/: DID-scoped evidence
Security:
- Write endpoints (/attest, /flag, /link, /epoch) are not exposed publicly.
- Security headers and server_tokens off are applied.
Deployment steps:
- Place the config under sites-available, symlink into sites-enabled, test with nginx -t, reload, and obtain TLS via certbot.
Section sources
Required variables:
- RPC_URL: Primary blockchain RPC endpoint
- ORACLE_PRIVATE_KEY: Oracle wallet private key, or ORACLE_PRIVATE_KEY_FILE, the path of a file that holds it (set one, not both; a file secret keeps the key out of the container's environment)
- IDENTITY_ADDRESS: Identity contract address
- REPUTATION_ADDRESS: Reputation contract address
Optional variables:
- EPOCH_HOURS: Epoch interval in hours
- HOST/PORT: Bind address and port (default 127.0.0.1:3030)
- FROM_BLOCK: Starting block for scans
- LOG_CHUNK_SIZE: Batch size for log processing
- ORACLE_ADMIN_TOKEN: Admin token for protected endpoints
- ORACLE_TOKEN_: Additional named tokens
- ORACLE_SCOPES_: Restricts that token to some of attest, flag, link and epoch
- EXPECTED_CHAIN_ID: The chain this oracle is for (5042 on Arc mainnet, 5042002 on the testnet). With it set, nothing is read for scoring or signed while the RPC reports another chain, and a state file saved on another chain is refused
- TRUSTED_PROXIES: Addresses or CIDR ranges whose X-Forwarded-For is believed, besides loopback
- TX_WAIT_SECONDS, MAX_FEE_GWEI, MAX_TX_COST: How long a transaction is waited for before one replacement, and the fee and total-cost ceilings the oracle will sign under
- MAX_CLOCK_SKEW_SECONDS: How far the chain's clock may be from the host's before an epoch is not run
- FEE_REGISTRY_ADDRESS: Optional fee gating registry
- EXTERNAL_RPC, EXTERNAL_IDENTITY_ADDRESS, EXTERNAL_REPUTATION_ADDRESS: ERC-8004 feed configuration
- ORACLE_MODE=checker: Enable checker mode
- DIVERGENCE_TOLERANCE: Allowed divergence threshold
- PRIMARY_EVIDENCE_URL: URL to primary oracle’s evidence endpoint
- JOBS_MAX_SPAN: Maximum block span for jobs queries
- ATTEST_COOLDOWN_MS: Cooldown between attestations
- FLAG_HALF_LIFE_DAYS: Decay half-life for flags
- ORACLE_STATE_PATH: Path to state file (default /data/oracle-state.json)
Startup validation:
- Missing required variables cause immediate failure.
- Invalid epoch intervals or tolerance values abort startup.
- Any numeric setting that is set and is not a plain number in range aborts startup, with every offending setting named. A blank value means unset and takes the default.
- An
ORACLE_MODEother thanprimaryorchecker, or both forms of the signing key, abort startup, as does a non-loopback bind with no write credential. - Checker mode rejects fee gating configuration to prevent double-charging.
Section sources
Health endpoint:
- Returns 200 when the state path is writable, last persistence succeeded, and the last successful epoch is within two epochs’ duration. Before the first success the two epochs are counted from process start, so a primary that has never completed an epoch reports 503 once it has been up that long.
- Reports uptime, epoch timing, store writability, last persist status, where the state was loaded from (
stateSource), attest cooldown, current epoch status, commit info, the chain the RPC was last seen on beside the configured one (chainId,expectedChainId), external feed status and chain id, payment scan status, and jobs feed status.
Metrics endpoint:
- Exposes Prometheus-compatible text metrics including request counts, epoch counters, rate-limit hits, refused or unconfirmed transactions, epoch-fee charges, and cache statistics.
Operational guidance:
- Monitor timeSinceLastEpochMs against EPOCH_HOURS to detect stalled epochs.
- Alert on storeWritable=false or statePersisted=false.
- Treat a
stateSourceother thanfileafter a restart as a state that was recovered from the.bakor started empty. - Track externalFeed status and externalChainId to detect misconfiguration.
Section sources
State contents:
- Attestations, flags, links, attestation cooldowns, payment events, used payment transactions, divergence records, scan checkpoints, and timestamps.
Persistence model:
- Atomic writes via temp file + rename + fsync to avoid partial corruption. The file is written in pieces of about 1 MB, so a save does not need several copies of it in memory, and the file it replaces is kept as
<state>.bak(a hard link, so it costs no copy). - The file records a
schemaVersionand thechainIdit belongs to. - Periodic pruning of expired flags, payment events, and divergences to bound growth.
- Writability probe integrated into /health.
Backup recommendations:
- Snapshot the named volume or the underlying directory containing oracle-state.json regularly.
- Copy the state file off the host on a schedule. The
.bakis always one save behind and sits on the same disk. - Include both the state file and any related logs in your backup strategy.
- Test restore procedures periodically to ensure recoverability.
Disaster recovery:
- Restore the state file to the same path and restart the service.
- On start, a state file that exists but cannot be read falls back to the
.bak(the bad file is kept as<state>.corrupt-<time>and/healthreportsstateSource: "backup"). The process stops instead if there is no usable backup, if the file was written by a newer version, or if it was saved on a different chain from the one the RPC reports. - Verify /health indicates statePersisted=true and storeWritable=true.
- Re-run a manual epoch if needed to catch up after recovery.
Section sources
Horizontal scaling:
- Each Oracle instance maintains its own state and proposes independently. For redundancy, run multiple instances behind a load balancer that routes read traffic and uses a single writer or coordinated writers.
- If multiple writers exist, ensure nonce coordination and avoid duplicate charges; the code guards against double-charging in checker mode but operators must design their deployment carefully. Instances that share one wallet share a nonce, and the contract holds one pending score per agent, so a second operator should run as a checker (
ORACLE_MODE=checker) with its own key, state and RPC provider rather than as a second proposer.
Load balancing strategies:
- Route reads through Caddy/Nginx with rate limiting and caching headers.
- Use connection pooling and keep-alive to RPC providers.
- Prefer batched RPC calls; the client library batches concurrent calls automatically.
Network considerations:
- Keep admin endpoints loopback-only.
- Use a shared Docker network for inter-service communication rather than host loopback.
Section sources
- Restrict public paths to read-only endpoints; keep write endpoints behind authentication and loopback.
- Enforce CORS only for trusted origins.
- Set security headers (HSTS, nosniff, referrer policy) and remove server identification.
- Validate and fail fast on missing or malformed configuration at startup.
- Keep the signing key out of the environment with
ORACLE_PRIVATE_KEY_FILE, and limit each integration's token to the writes it needs withORACLE_SCOPES_<NAME>. - Rate limit high-cost endpoints (/score, /evidence, /attest). A request target that is not a canonical path gets a 400 before routing, so a proxy that matches on a normalised path cannot be used to reach a route it did not allow.
- Use least-privilege containers and restrict filesystem writes to the state volume (the mainnet compose file runs read-only with every capability dropped).
Section sources
- Scrape /metrics with Prometheus.
- Alert on:
- epochsFailed > 0
- proposeErrors, finalizeErrors
- rateLimitHits spikes
- paymentScanCanaryFailures
-
sigvara_oracle_tx_refused_total(a fee cap that trips, or a transaction sent and never seen mined) andsigvara_oracle_fee_charges_total{result="skipped"}(proposals are being refused) - timeSinceLastEpochMs exceeding EPOCH_HOURS * 2
- storeWritable=false or statePersisted=false
- stateSource other than
fileafter a restart
- Integrate with log aggregation for structured logs from Caddy and the Oracle service.
Section sources
The Oracle service depends on:
- Node.js runtime and standard libraries
- dotenv for environment loading
- ethers for blockchain interactions
- Internal modules for chain access, external registries, scoring, merkle proofs, response caching, badge generation, job registry scanning, and metrics
graph LR
Index["index.js"] --> Chain["chain.js"]
Index --> External["external.js"]
Index --> Scoring["scoring.js"]
Index --> Merkle["merkle.js"]
Index --> Payments["payments.js"]
Index --> Store["store.js"]
Index --> Jobs["jobs.js"]
Index --> Metrics["metrics.js"]
Index --> Badge["badge.js"]
Index --> ResponseCache["response-cache.js"]
Diagram sources
Section sources
- Epoch batching: Phase-one reads are chunked to avoid RPC bursts; phase-two writes are sequential due to nonce constraints.
- RPC batching: Concurrent calls issued in the same tick are batched by the client library.
- Evidence caching: Large responses are cached to reduce recomputation cost, and Merkle leaves and per-agent roots are cached for evidence that has not changed.
- Payment event pruning: Old events are pruned based on decay curves to bound state size.
- Flag decay: Flags decay faster than payment evidence to reflect their nature.
- State saves: The state file is streamed in pieces. Measured on Node 20, the peak memory of one save fell from 449 MB to 189 MB with 100,000 payments, and from 700 MB to 280 MB with 200,000. The mainnet compose file limits a container to 768 MB, so size memory for the peak of a save, not the size of the file.
Tuning guidelines:
- The epoch reads agents 32 at a time and its pre-write guard reads 32 at a time; both are constants in
runEpochInner, so changing them means editing the code. The settings areEPOCH_HOURS,LOG_CHUNK_SIZE,PAYMENT_SCAN_CHUNK,PAYMENT_SCAN_PACE_MSandPAYMENT_SCAN_CONFIRMATIONS. - Tune EPOCH_HOURS to balance freshness and RPC costs.
- Configure JOBS_MAX_SPAN to cap job registry scans.
- Monitor RPC throttling and adjust pacing or provider tier accordingly.
Section sources
Common issues and resolutions:
- Missing required environment variables: The process exits immediately with an error message.
- Stalled epochs: Check /health timeSinceLastEpochMs and metrics epochsFailed; verify RPC connectivity and fee gating configuration. An epoch does not run while the RPC reports a chain other than
EXPECTED_CHAIN_ID, or a clock more thanMAX_CLOCK_SKEW_SECONDSfrom the host's; the log says which. - State persistence failures: Inspect storeWritable and statePersistError in /health; ensure the volume is mounted and has sufficient space. While a save is failing,
/attest,/flag,/flag/resolveand/linkanswer 202 withdurable: false.sigvara_oracle_attest_total{result="accepted_not_durable"}counts the attestations andsigvara_oracle_writes_not_durable_totalcounts the other three. - Rate limiting: High rateLimitHits indicate excessive requests; adjust client behavior; the oracle's limit is a constant in
http-helpers.js(60 requests per caller per minute in each route's bucket), not a setting. If every visitor behind a proxy is limited at once,TRUSTED_PROXIESis missing. - Checker divergence: Review /divergence endpoints and compare evidence roots to identify discrepancies.
Recovery steps:
- Restart the service after fixing configuration or permissions.
- Trigger a manual epoch via /epoch if appropriate.
- Restore state from backups if corruption is detected.
Section sources
Deploying the Oracle service in production requires careful attention to container orchestration, reverse proxy exposure, environment configuration, state persistence, and observability. By following the guidance above—binding to loopback, exposing only safe public paths, enabling robust health checks and metrics, and implementing disciplined backup and recovery—you can operate a resilient, auditable, and scalable Oracle service.
- RPC_URL
- ORACLE_PRIVATE_KEY
- ORACLE_PRIVATE_KEY_FILE
- IDENTITY_ADDRESS
- REPUTATION_ADDRESS
- EXPECTED_CHAIN_ID
- EPOCH_HOURS
- HOST
- PORT
- FROM_BLOCK
- LOG_CHUNK_SIZE
- ORACLE_ADMIN_TOKEN
- ORACLE_TOKEN_
- ORACLE_SCOPES_
- TRUSTED_PROXIES
- TX_WAIT_SECONDS
- MAX_FEE_GWEI
- MAX_TX_COST
- MAX_CLOCK_SKEW_SECONDS
- FEE_REGISTRY_ADDRESS
- EXTERNAL_RPC
- EXTERNAL_IDENTITY_ADDRESS
- EXTERNAL_REPUTATION_ADDRESS
- ORACLE_MODE
- DIVERGENCE_TOLERANCE
- PRIMARY_EVIDENCE_URL
- JOBS_MAX_SPAN
- ATTEST_COOLDOWN_MS
- FLAG_HALF_LIFE_DAYS
- ORACLE_STATE_PATH
Section sources
- Agent Registration and Lifecycle
- Contract Overview and Architecture
- Economic Security Model and Incentives
- Evidence Verification and Merkle Proofs
- Key Management and Rotation
- Maturity Mechanism and Score Release
- Proposal Workflow and Challenge System
- Score Storage and Data Model
- SigvaraIdentity Contract
- SigvaraOracleBond Contract
- SigvaraReputation Contract
- SigvaraStaking Contract
- Slashing Lifecycle and Dispute Resolution
- Stake Management Functions
- Status Management and Control
- Token and Fee Contracts
- Unbonding Period and Withdrawal Security
- Anti-Manipulation and Security Measures
- Community Flag Processing
- Evidence Sources and Verification
- External Trust Integration
- Merkle Proof System
- Oracle Score Computation
- Payment-Backed Attestations
- Scoring Model and Formulas
- Agent Creation and Key Management
- Agent Operations
- Agent Registration Workflow
- Challenge-Response Authentication
- Gate Access Control
- Integration Patterns and Examples
- SDK Overview and Setup
- Verifier Functions