Skip to content

s0racle/s0racle-program

Repository files navigation

s0racle-program

A decentralized, permissionless Solana network-health oracle built with Anchor. Observer nodes from across the globe submit real-time QUIC/TPU probe measurements on-chain. The protocol aggregates them into a single, always-fresh NetworkHealthAccount that any dApp, UI, or smart contract can read in one RPC call.


Table of Contents

  1. Why This Exists
  2. How It Works — High-Level
  3. Program Identity & Deployed Addresses
  4. Architecture & On-Chain Accounts
  5. Instructions — Full Reference
  6. Metrics & Data Model
  7. Health Score Formula
  8. Staleness Model
  9. Slashing Mechanics
  10. Events
  11. Error Codes
  12. PDAs — Seeds & Derivation
  13. Constants
  14. Key Features
  15. Prerequisites & Local Setup
  16. Build & Test
  17. Integrating as a Consumer
  18. Project Structure
  19. Dependency Versions

Why This Exists

Solana's validator network is a global, heterogeneous system. Validators run different client implementations (Agave, Firedancer, Jito, Solana Labs), are distributed across continents, and communicate over QUIC. Today there is no on-chain, trust-minimized source of truth for:

  • What percentage of validators are reachable via QUIC right now?
  • What is the average slot-propagation latency from Asia vs. the EU?
  • Which client implementations dominate the active validator set?
  • Is the network healthy enough to accept time-sensitive transactions?

dApps currently answer these questions by trusting a single off-chain API — a single point of failure and censorship. s0racle-program replaces that with a decentralised network of observer nodes that attest measurements directly on-chain, where the aggregated result is verifiable by anyone.


How It Works — High-Level

Observer (Asia) ──submit_attestation──┐
Observer (EU)   ──submit_attestation──┤──► NetworkHealthAccount (global oracle PDA)
Observer (US)   ──submit_attestation──┘
                                       │
                    crank_aggregation──┘  (permissionless full recompute)

Consumer dApp ──── reads NetworkHealthAccount ───► health_score, reachability, latency, client mix
  1. Observer nodes run off-chain software that probes validators via QUIC every ~10 seconds.
  2. Each observer calls submit_attestation — the instruction validates the measurement, updates the observer's own PDA, and immediately updates the global NetworkHealthAccount incrementally.
  3. Optionally, any wallet can call crank_aggregation to do a full recomputation from all observer accounts (useful after many stale observers drop out).
  4. Any on-chain program or RPC client can read NetworkHealthAccount — a single fixed PDA — to get the current network-health snapshot.

Program Identity & Deployed Addresses

Network Program ID
Devnet 2paXpX8Ze3tvYezviSwQJSSihG3LbrDiD7SNsaFwgTow
Mainnet-beta not yet deployed
Localnet 2paXpX8Ze3tvYezviSwQJSSihG3LbrDiD7SNsaFwgTow

Derived PDAs (fixed addresses once program is deployed)

Account Seed Purpose
RegistryAccount b"registry" Global protocol config — admin key, observer counts, paused flag
NetworkHealthAccount b"network_health" The oracle output — health score, per-region stats, client mix
ObserverAccount (per observer) b"observer" ++ authority_pubkey Per-node state — region, stake, latest attestation

You can derive these off-chain with:

const [registry] = PublicKey.findProgramAddressSync(
  [Buffer.from("registry")],
  PROGRAM_ID
);

const [networkHealth] = PublicKey.findProgramAddressSync(
  [Buffer.from("network_health")],
  PROGRAM_ID
);

const [observerAccount] = PublicKey.findProgramAddressSync(
  [Buffer.from("observer"), observerWallet.toBuffer()],
  PROGRAM_ID
);

Architecture & On-Chain Accounts

RegistryAccount (global config)

Holds protocol-wide configuration. Created once by the admin during initialize.

Field Type Description
authority Pubkey Admin who can update config, slash observers, transfer authority
pending_authority Option<Pubkey> Staged admin for two-step authority transfer
min_stake_lamports u64 Minimum SOL (in lamports) observers must escrow to register
observer_count u16 All-time count of registered observers
active_count u16 Currently active observers
max_observers u16 Hard cap on active observer count
paused bool Emergency pause — blocks submit_attestation and crank_aggregation
version u8 Schema version (currently 1)
bump u8 PDA bump seed

Account size: 99 bytes (including discriminator + 8-byte padding)


ObserverAccount (per observer node)

One PDA per registered observer. Holds their identity, escrow stake, and their most recent measurement.

Field Type Description
authority Pubkey Observer's wallet — signs every attestation
region Region (u8) Geographic region (Asia / US / EU / SouthAmerica / Africa / Oceania / Other)
stake_lamports u64 Current escrowed SOL — can be slashed
registered_at i64 Unix timestamp of registration
last_attestation_slot u64 Slot of most recent submission — used for staleness checks
attestation_count u64 Lifetime attestations submitted by this observer
latest_attestation Attestation The most recent 10-second measurement (see below)
is_active bool False if slashed below minimum stake or voluntarily deregistered
bump u8 PDA bump seed

Account size: 129 bytes


Attestation (embedded struct — not a separate account)

The core measurement captured from a single observer's 10-second QUIC probe round.

Field Type Unit Description
slot u64 slot Solana slot this measurement covers
timestamp i64 unix seconds Wall-clock time of the probe
avg_rtt_us u32 microseconds Mean RTT across all QUIC probes this round
p95_rtt_us u32 microseconds 95th-percentile RTT
slot_latency_ms u32 milliseconds Observed slot-propagation latency
tpu_reachable u16 count Validators that accepted the QUIC probe
tpu_probed u16 count Total validators probed this round
agave_count u16 count Agave-client validators seen
firedancer_count u16 count Firedancer-client validators seen
jito_count u16 count Jito-client validators seen
solana_labs_count u16 count Solana Labs client validators seen
other_count u16 count Validators with unidentified client
reachable_stake_pct u8 % (0–100) % of total stake (by lamports) behind reachable validators

Struct size: 43 bytes


NetworkHealthAccount (the oracle output)

The single PDA that consumers read. Updated incrementally on every submit_attestation and fully recomputed on every crank_aggregation.

Field Type Description
health_score u8 Global health score 0–100 (average across active regions)
tpu_reachability_pct u8 Average TPU reachability % across active regions
avg_slot_latency_ms u32 Average slot latency across active regions (ms)
active_observer_count u16 Number of fresh observer contributions
active_region_count u16 Regions with at least one fresh observer
last_updated_slot u64 Slot of most recent update — check for staleness
last_updated_ts i64 Unix timestamp of most recent update
min_health_ever u8 All-time lowest health score recorded
max_health_ever u8 All-time highest health score recorded
total_attestations u64 Total attestations ever processed
region_scores [RegionScore; 7] Per-region breakdown (see below)
agave_count u8 Global Agave client %
firedancer_count u8 Global Firedancer client %
jito_count u8 Global Jito client %
solana_labs_count u8 Global Solana Labs client %
other_count u8 Global other client %
bump u8 PDA bump seed

Account size: 629 bytes


RegionScore (embedded in NetworkHealthAccount)

One entry per geographic region. Holds both the running totals used for incremental updates and the display averages.

Field Type Description
region Region Which region (Asia / US / EU / etc.)
observer_count u16 Active observer contributions in this region
health_score u8 Average health score for this region
reachability_pct u8 Average TPU reachability % for this region
avg_rtt_us u32 Average RTT (µs) for this region
slot_latency_ms u32 Average slot latency (ms) for this region
last_updated_slot u64 Slot when this region last had a fresh attestation
total_health_score u32 Running sum for incremental averaging
total_reachability_pct u32 Running sum for incremental averaging
total_avg_rtt_us u64 Running sum for incremental averaging
total_slot_latency_ms u64 Running sum for incremental averaging
agave_countother_count u16 Per-client display counts (averaged)
total_agave_counttotal_other_count u32 Running sums
reachable_stake_pct u8 Average stake-weighted reachability % for this region
total_reachable_stake_pct u32 Running sum

Struct size: 75 bytes × 7 regions = 525 bytes


Instructions — Full Reference

1. initialize

Who can call: Admin only (signs and pays for account creation)

Purpose: Bootstrap the protocol. Creates RegistryAccount and NetworkHealthAccount as PDAs. Can only be called once (the PDAs will already exist on any subsequent attempt).

Parameters:

Param Type Description
min_stake_lamports u64 Minimum lamports observers must escrow. Must be > 0
max_observers u16 Hard cap on active observers. Must be > 0

Accounts required:

  • authority — signer, mutable (pays rent for new accounts)
  • registry — PDA [b"registry"], created here
  • network_health — PDA [b"network_health"], created here
  • system_program

Side effects: Initializes RegistryAccount with version = 1, paused = false, all counts = 0. Initializes NetworkHealthAccount with min_health_ever = 255 (sentinel for "no data yet") and all 7 RegionScore entries pre-populated with their respective Region values.


2. register_observer

Who can call: Any wallet with sufficient lamports

Purpose: Registers a new observer node. Escrows min_stake_lamports from the caller's wallet into the ObserverAccount PDA as collateral.

Parameters:

Param Type Description
region Region Geographic region this observer node is located in

Accounts required:

  • observer — signer, mutable (pays escrow + rent)
  • observer_account — PDA [b"observer", observer_pubkey], created here
  • registry — mutable (increments counts)
  • system_program

Guards:

  • Registry must not be paused
  • registry.active_count < registry.max_observers
  • Observer wallet balance ≥ min_stake_lamports

Side effects: Transfers min_stake_lamports SOL into the ObserverAccount PDA. Increments registry.observer_count and registry.active_count. Emits ObserverRegistered event.


3. submit_attestation

Who can call: Any registered, active observer (must sign with their registered wallet)

Purpose: The core instruction. Submits a fresh 10-second QUIC probe measurement. Immediately and incrementally updates NetworkHealthAccount without iterating all observers.

Parameters:

Param Type Constraint Description
tpu_reachable u16 tpu_probed Validators that accepted the QUIC probe
tpu_probed u16 ≥ 10 (MIN_PROBE_COUNT) Total validators probed this round
avg_rtt_us u32 ≤ 10,000,000 µs Mean RTT across all probes
p95_rtt_us u32 ≤ 10,000,000 µs 95th-percentile RTT
slot_latency_ms u32 ≤ 10,000 ms Slot propagation latency observed
agave_count u16 Agave validators counted
firedancer_count u16 Firedancer validators counted
jito_count u16 Jito validators counted
solana_labs_count u16 Solana Labs validators counted
other_count u16 Other/unknown client validators
reachable_stake_pct u8 0–100 % of stake behind reachable validators

Accounts required:

  • authority — signer, mutable (the observer's wallet)
  • observer_account — PDA, mutable (updated with new attestation)
  • network_health — PDA, mutable (immediately updated)
  • registry — read-only (checked for paused state)
  • clock — sysvar

Incremental aggregation logic:

The instruction uses a running-total approach to avoid iterating all observers on every submission:

  1. If the observer had a fresh previous attestation (within STALE_SLOTS = 150 slots), subtract their old contribution from the region's running totals before adding the new one. The observer count stays the same.
  2. If the observer is new to this region or their previous attestation is stale, simply add their new values and increment observer_count.
  3. If the region itself is stale (no update for > 150 slots), clear the region's entire aggregate first before adding.
  4. Recompute per-region averages (set_region_averages), then recompute the global score across all fresh regions.

Side effects: Updates observer_account.latest_attestation, last_attestation_slot, attestation_count. Updates corresponding RegionScore in NetworkHealthAccount. Updates global health_score, tpu_reachability_pct, avg_slot_latency_ms, active_observer_count, active_region_count, client distribution fields. Updates min_health_ever / max_health_ever if applicable. Emits AttestationSubmitted event.


4. crank_aggregation

Who can call: Anyone (permissionless)

Purpose: Full recomputation of NetworkHealthAccount from scratch by iterating all observer accounts passed as remaining_accounts. This is used to "clean up" the global state after many observers go stale, or as a periodic correctness check.

Parameters: None

Accounts required:

  • cranker — signer (anyone; no authority required)
  • network_health — PDA, mutable
  • registry_account — read-only (checked for paused state)
  • clock — sysvar
  • remaining_accounts — a list of all ObserverAccount PDAs to include in the recompute

How it works:

  1. Iterates remaining_accounts; skips any account not owned by this program or that fails deserialization.
  2. Skips observers where is_active == false or last_attestation_slot is older than STALE_SLOTS.
  3. Snapshots each valid observer's data into an in-memory ObserverSnapshot.
  4. Clears all region aggregates in NetworkHealthAccount.
  5. Rebuilds running totals from the snapshots.
  6. Recomputes all averages and the global score.
  7. Requires at least one active region — fails with NoActiveObservers if all observers are stale.

Side effects: Fully overwrites all fields of NetworkHealthAccount. Updates min_health_ever / max_health_ever. Updates global client distribution.


5. deregister_observer

Who can call: The observer themselves OR the registry authority

Purpose: Removes an observer from the network and returns their escrowed stake.

Parameters: None

Accounts required:

  • caller — signer, mutable (observer or admin)
  • observer_wallet — mutable (receives returned stake + rent)
  • observer_account — PDA, mutable, closed (rent reclaimed to observer_wallet)
  • registry — mutable (decrements counts)
  • system_program

Guards:

  • Caller must be the observer's own wallet OR the registry authority
  • Observer must be currently active

Side effects: Returns stake_lamports from the PDA to observer_wallet. Closes the observer_account PDA (reclaims rent). Decrements registry.observer_count and registry.active_count. Emits ObserverDeregistered event.


6. slash_observer

Who can call: Registry authority only

Purpose: Penalises a misbehaving observer by seizing a portion of their escrowed stake into a treasury account. If the observer's remaining stake falls below min_stake_lamports, they are automatically marked inactive.

Parameters:

Param Type Constraint Description
slash_bps u16 0–10,000 Fraction of stake to slash, in basis points (1 bps = 0.01%)

Examples: slash_bps = 5000 slashes 50%; slash_bps = 10000 slashes 100%.

Accounts required:

  • authority — signer (must match registry.authority)
  • observer_wallet — read-only (PDA seed derivation)
  • observer_account — PDA, mutable
  • registry — mutable
  • treasury — mutable, any account (receives slashed lamports)

Guards:

  • slash_bps ≤ 10,000
  • Observer must be active
  • PDA must retain enough lamports to remain rent-exempt after the slash

Auto-deactivation logic:

slash_amount = stake_lamports × slash_bps / 10_000
new_stake = stake_lamports - slash_amount
if new_stake < registry.min_stake_lamports → observer.is_active = false; registry.active_count--

Side effects: Transfers slash_amount lamports from observer_account PDA to treasury. Updates observer_account.stake_lamports. May set is_active = false and decrement registry.active_count. Emits ObserverSlashed event.


7. update_config

Who can call: Registry authority only

Purpose: Updates one or more protocol parameters without redeploying.

Parameters (all optional — pass None to leave unchanged):

Param Type Description
min_stake_lamports Option<u64> New minimum stake requirement
max_observers Option<u16> New observer cap (cannot be set below current active_count)
paused Option<bool> Pause or unpause the protocol

Side effects: Updates the respective fields in RegistryAccount. Emits ConfigUpdated event.


8. propose_authority

Who can call: Current registry authority

Purpose: First step of a two-step authority transfer. Stages a new admin public key.

Parameters:

Param Type Description
new_authority Pubkey The proposed new admin wallet

Side effects: Sets registry.pending_authority = Some(new_authority).


9. accept_authority

Who can call: The wallet specified in registry.pending_authority

Purpose: Second step of authority transfer. The proposed new admin signs to confirm the handoff.

Parameters: None

Guards: Caller must match registry.pending_authority.

Side effects: Sets registry.authority = new_authority. Clears registry.pending_authority = None.


Metrics & Data Model

Region Enum

Observers self-declare their geographic region at registration time. The network currently supports 7 regions:

Variant Value Description
Asia 0 Default
US 1 United States
EU 2 Europe
SouthAmerica 3 South America
Africa 4 Africa
Oceania 5 Australia / Oceania
Other 6 Any other region

Core Network Metrics

Metric Where stored Description
Global health score NetworkHealthAccount.health_score Weighted average 0–100; 100 = perfectly healthy
TPU reachability % NetworkHealthAccount.tpu_reachability_pct % of probed validators that responded via QUIC
Stake-weighted reachability RegionScore.reachable_stake_pct % of total SOL stake behind reachable validators
Avg slot latency NetworkHealthAccount.avg_slot_latency_ms Average slot propagation time (ms) across active regions
Avg RTT RegionScore.avg_rtt_us Average QUIC round-trip time in microseconds for this region
P95 RTT ObserverAccount.latest_attestation.p95_rtt_us 95th-percentile RTT — stored per observer, not aggregated
Active observers NetworkHealthAccount.active_observer_count Count of observers with fresh attestations (within STALE_SLOTS)
Active regions NetworkHealthAccount.active_region_count Regions with at least one fresh observer

Client Distribution Metrics

Tracks which Solana validator client software dominates the active set:

Field Description
agave_count Agave (Anza's fork)
firedancer_count Firedancer (Jump Crypto)
jito_count Jito (MEV-enhanced Agave fork)
solana_labs_count Original Solana Labs client
other_count Unidentified / other

These are available both globally on NetworkHealthAccount and per-region on each RegionScore.

All-Time Records

Field Description
min_health_ever Lowest health score ever recorded (initialized to 255 as a "no data" sentinel)
max_health_ever Highest health score ever recorded
total_attestations Cumulative attestations across all observers, all time

Health Score Formula

Each observer's individual score is computed in utils::compute_health_score:

health_score = (reachability_component) + (latency_component)

reachability_component = reachability_pct × 70 / 100   → contributes 0–70 points
latency_component      = latency_score × 30 / 100        → contributes 0–30 points

latency_score =
  if slot_latency_ms >= 400ms → 0
  else                        → (400 - slot_latency_ms) × 100 / 400

health_score = min(reachability_component + latency_component, 100)

Design rationale:

  • 70% weight on reachability: If a validator cannot be reached via QUIC, transactions simply cannot land. This is a binary failure condition, so it carries the most weight.
  • 30% weight on latency: A slow network is degraded but still functional. The 400 ms ceiling is chosen based on Solana's ~400 ms slot target — latency beyond that implies full slot misses.
  • Global score = unweighted average of all per-region health_score values that have fresh data (within STALE_SLOTS).

Staleness Model

Stale data from inactive observers must not pollute the global score. The protocol uses a slot-based staleness window:

STALE_SLOTS = 150

At Solana's ~400 ms per slot, 150 slots ≈ 60 seconds.

Rules:

  • A region's data is considered stale if current_slot - region.last_updated_slot > STALE_SLOTS.
  • Stale regions are excluded entirely from global score computation. Their old scores are not averaged in.
  • When an observer submits an attestation to a stale region, the region is first cleared (clear_region_aggregate) before the new data is accumulated.
  • On crank_aggregation, only observers with last_attestation_slot within STALE_SLOTS of the current slot are included.
  • submit_attestation also enforces current_slot > observer.last_attestation_slot to prevent replay of the same slot's data.

Slashing Mechanics

Slashing is admin-controlled and acts on an observer's SOL escrow:

  1. Escrow: When an observer registers, they lock min_stake_lamports of SOL into their ObserverAccount PDA. This SOL does not leave the protocol without authority action.
  2. Slash trigger: The admin calls slash_observer with a basis-points amount. Any reason for slashing (e.g., provably false attestations detected off-chain) is evaluated outside the program.
  3. Slash calculation: slash_amount = stake_lamports × slash_bps / 10_000. Uses saturating arithmetic throughout.
  4. Rent safety: The program will never drain the PDA below its rent-exempt minimum — it checks current_balance >= rent_min + slash_amount before proceeding.
  5. Auto-deactivation: If post-slash stake_lamports < min_stake_lamports, the observer is set to is_active = false and removed from the active count. They can no longer submit attestations.
  6. Treasury: Slashed funds are transferred to any arbitrary treasury account specified by the admin.
  7. Deregistration (voluntary or by admin) always returns the remaining stake to the observer's wallet — slashing and deregistration are independent operations.

Events

The program emits the following Anchor events, subscribable via program.addEventListener:

AttestationSubmitted

Emitted on every successful submit_attestation.

pub struct AttestationSubmitted {
    pub observer: Pubkey,        // Observer wallet
    pub region: Region,          // Geographic region
    pub score: u8,               // Computed health score for this observer
    pub reachability_pct: u8,    // TPU reachability %
    pub slot_latency_ms: u32,    // Slot propagation latency
    pub slot: u64,               // Solana slot
    pub agave_count: u16,
    pub firedancer_count: u16,
    pub jito_count: u16,
    pub solana_labs_count: u16,
    pub other_count: u16,
    pub reachable_stake_pct: u8, // Stake-weighted reachability %
}

ObserverRegistered

pub struct ObserverRegistered {
    pub observer: Pubkey,
    pub region: Region,
    pub stake_lamports: u64,
}

ObserverDeregistered

pub struct ObserverDeregistered {
    pub observer: Pubkey,
}

ObserverSlashed

pub struct ObserverSlashed {
    pub observer: Pubkey,
    pub slash_bps: u16,
    pub amount_slashed: u64,
}

ConfigUpdated

pub struct ConfigUpdated {
    pub min_stake_lamports: Option<u64>,
    pub max_observers: Option<u16>,
    pub paused: Option<bool>,
}

Error Codes

Code Name Message
6000 ValueCannotBeZero Value cannot be zero
6001 RegistryPaused Registry is currently paused
6002 MaxObserversReached Maximum number of observers has been reached
6003 InsufficientLamports Insufficient Lamports
6004 UnauthorizedObserver Unauthorized Observer
6005 ObserverNotActive Observer is not active
6006 ZeroValidatorsProbed Zero validators probed
6007 InsufficientValidatorsProbed Insufficient validators probed
6008 InvalidReachabilityCount Invalid reachability count
6009 InvalidLatencyValue Invalid latency submitted
6010 StaleAttestation Stale attestation
6011 NoActiveObservers No active observers
6012 ObserverAlreadyInActive Observer already inactive
6013 UnAuthorizedCaller Unauthorized caller
6014 InsufficientBalanceForRefund Insufficient balance in PDA for stake refund
6015 InvalidSlashBps Invalid slash basis points - must be <= 10000
6016 ObserverNotFound Observer not found
6017 InsufficientBalanceForSlash Insufficient balance in PDA for slash
6018 InvalidPendingAuthority Invalid or no pending authority for registry
6019 MaxObserversCannotBeLessThanActiveObservers Max observers cannot be less than active observers

PDAs — Seeds & Derivation

RegistryAccount      → seeds: [b"registry"]
NetworkHealthAccount → seeds: [b"network_health"]
ObserverAccount      → seeds: [b"observer", authority_pubkey_bytes]

All PDAs are derived with findProgramAddressSync using the canonical bump, which is stored in the bump field of each account for use in CPI and constraint validation.


Constants

Defined in src/constants.rs:

Constant Value Description
STALE_SLOTS 150 Slots after which an observer/region is considered stale (~60 seconds)
MIN_PROBE_COUNT 10 Minimum validators that must be probed per attestation round
MAX_RTT_US 10_000_000 Maximum accepted RTT value (10 seconds in µs)
MAX_SLOT_LATENCY_MS 10_000 Maximum accepted slot latency (10 seconds in ms)

Key Features

Incremental on-chain aggregation. submit_attestation updates the global oracle state in O(1) using running totals — no iteration over all observers required. The global NetworkHealthAccount is always immediately up to date.

Permissionless crank. Anyone can call crank_aggregation to trigger a full recompute from all observer accounts. This is the correctness backstop — useful when observers go offline in bulk or to periodically reset accumulated floating-point drift (all arithmetic is integer).

Stake-backed sybil resistance. Observers must escrow SOL to participate. The required amount is configurable by the admin. This makes spinning up fake observer identities economically costly.

Slashing with rent safety. The slash instruction always preserves the PDA's rent-exempt minimum, preventing observers from being permanently bricked by a slash that would destroy the account.

Two-step authority transfer. Admin key rotation uses a propose→accept pattern, preventing accidental transfers to unreachable keys.

Emergency pause. A single paused flag on RegistryAccount stops all attestation submission and crank operations without needing a program upgrade.

Geographic distribution awareness. Data is tracked per region (7 regions), so consumers can detect regional outages rather than only seeing a degraded global average.

Client diversity tracking. The mix of Agave, Firedancer, Jito, Solana Labs, and other clients is tracked per region and globally, enabling monitoring of client monoculture risk.

All-time records. min_health_ever and max_health_ever provide historical baselines — useful for displaying relative health context ("currently at 92, all-time low was 34").

Saturating arithmetic throughout. All counter increments and decrements use saturating_add / saturating_sub — no panic risk from arithmetic overflow.

No floating point. All percentage and average computations use integer division. The protocol is fully deterministic on-chain.


Prerequisites & Local Setup

Required tools

Tool Version Install
Rust stable + solana target rustup install stable
Solana CLI ≥ 2.x docs.solana.com
Anchor CLI 0.32.x cargo install --git https://github.com/coral-xyz/anchor anchor-cli
Node.js ≥ 18 nodejs.org
Yarn ≥ 1.22 npm install -g yarn

Rust toolchain

The project pins a specific toolchain via rust-toolchain.toml. Cargo will install it automatically on first use.

Install JS dependencies

yarn install

Build & Test

Build the program

anchor build

The compiled .so is placed at target/deploy/s0racle_program.so.

Run tests

Tests use litesvm (in-process Solana VM) via Rust unit tests, and a Mocha/TypeScript integration test suite.

# Rust unit tests (fast, no validator required)
cargo test -p s0racle-program

# TypeScript integration tests
anchor test

Deploy to devnet

anchor deploy --provider.cluster devnet

Ensure your wallet at ~/.config/solana/id.json has devnet SOL (solana airdrop 2 --url devnet).

Verify deployed program

solana program show 2paXpX8Ze3tvYezviSwQJSSihG3LbrDiD7SNsaFwgTow --url devnet

Integrating as a Consumer

Read the oracle (TypeScript)

import { Connection, PublicKey } from "@solana/web3.js";
import { Program, AnchorProvider } from "@coral-xyz/anchor";
import { IDL } from "./idl/s0racle_program"; // generated by `anchor build`

const PROGRAM_ID = new PublicKey("2paXpX8Ze3tvYezviSwQJSSihG3LbrDiD7SNsaFwgTow");
const connection = new Connection("https://api.devnet.solana.com");
const provider = AnchorProvider.env();
const program = new Program(IDL, PROGRAM_ID, provider);

// Derive NetworkHealthAccount PDA
const [networkHealthPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("network_health")],
  PROGRAM_ID
);

// Fetch and read
const networkHealth = await program.account.networkHealthAccount.fetch(networkHealthPda);
console.log("Health Score:       ", networkHealth.healthScore);       // 0–100
console.log("TPU Reachability:   ", networkHealth.tpuReachabilityPct, "%");
console.log("Avg Slot Latency:   ", networkHealth.avgSlotLatencyMs, "ms");
console.log("Active Observers:   ", networkHealth.activeObserverCount);
console.log("Active Regions:     ", networkHealth.activeRegionCount);
console.log("Last Updated Slot:  ", networkHealth.lastUpdatedSlot.toString());
console.log("Total Attestations: ", networkHealth.totalAttestations.toString());

// Check staleness — freshness window is 150 slots (~60s)
const currentSlot = await connection.getSlot();
const STALE_SLOTS = 150;
const isStale = currentSlot - Number(networkHealth.lastUpdatedSlot) > STALE_SLOTS;
console.log("Is stale?", isStale);

// Per-region breakdown
for (const rs of networkHealth.regionScores) {
  if (rs.observerCount > 0) {
    console.log(`Region ${JSON.stringify(rs.region)}: score=${rs.healthScore} reach=${rs.reachabilityPct}% latency=${rs.slotLatencyMs}ms observers=${rs.observerCount}`);
  }
}

Subscribe to attestation events

program.addEventListener("AttestationSubmitted", (event) => {
  console.log("New attestation:", event.observer.toBase58(), "score:", event.score);
});

On-chain CPI read (Rust)

// In your program — read NetworkHealthAccount
let network_health: Account<NetworkHealthAccount> = Account::try_from(&network_health_info)?;
require!(network_health.health_score >= MIN_REQUIRED_HEALTH, YourError::NetworkTooUnhealthy);

Project Structure

s0racle-program-main/
├── Anchor.toml                          # Anchor config — cluster, wallet, scripts
├── Cargo.toml                           # Workspace manifest
├── Cargo.lock
├── rust-toolchain.toml                  # Pinned Rust toolchain
├── package.json                         # JS dependencies
├── tsconfig.json
├── yarn.lock
│
├── programs/
│   └── s0racle-program/
│       ├── Cargo.toml                   # Program crate manifest
│       └── src/
│           ├── lib.rs                   # Program entrypoint — instruction dispatch
│           ├── constants.rs             # STALE_SLOTS, MIN_PROBE_COUNT, MAX_RTT_US, etc.
│           ├── error.rs                 # CustomErrors enum (20 variants)
│           ├── events.rs                # Anchor events — AttestationSubmitted, etc.
│           ├── state/
│           │   └── mod.rs              # All account structs: Registry, Observer, NetworkHealth, etc.
│           ├── utils.rs                 # compute_health_score, recompute_global_score, etc.
│           ├── instructions/
│           │   ├── mod.rs
│           │   ├── initialize.rs
│           │   ├── register_observer.rs
│           │   ├── submit_attestation.rs
│           │   ├── crank_aggregation.rs
│           │   ├── deregister_observer.rs
│           │   ├── slash_observer.rs
│           │   ├── update_config.rs
│           │   └── transfer_authority.rs
│           └── tests/
│               └── mod.rs              # Rust unit tests (litesvm)
│
├── tests/
│   └── s0racle-program.ts              # TypeScript integration tests
│
└── migrations/
    └── deploy.ts                        # Anchor deploy script

Dependency Versions

Rust / Anchor

Crate Version
anchor-lang 0.32.1
litesvm (dev) 0.9.1
solana-sdk (dev) 3.0.0
solana-keypair (dev) 3.1
solana-clock (dev) 3.0

JavaScript

Package Version
@coral-xyz/anchor as per package.json
ts-mocha as per package.json
typescript as per package.json

License

See LICENSE in the root of the repository.


Built with Anchor on Solana.

About

s0racle gives you network health on-chain.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors