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
NetworkHealthAccountthat any dApp, UI, or smart contract can read in one RPC call.
- Why This Exists
- How It Works — High-Level
- Program Identity & Deployed Addresses
- Architecture & On-Chain Accounts
- Instructions — Full Reference
- Metrics & Data Model
- Health Score Formula
- Staleness Model
- Slashing Mechanics
- Events
- Error Codes
- PDAs — Seeds & Derivation
- Constants
- Key Features
- Prerequisites & Local Setup
- Build & Test
- Integrating as a Consumer
- Project Structure
- Dependency Versions
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.
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
- Observer nodes run off-chain software that probes validators via QUIC every ~10 seconds.
- Each observer calls
submit_attestation— the instruction validates the measurement, updates the observer's own PDA, and immediately updates the globalNetworkHealthAccountincrementally. - Optionally, any wallet can call
crank_aggregationto do a full recomputation from all observer accounts (useful after many stale observers drop out). - Any on-chain program or RPC client can read
NetworkHealthAccount— a single fixed PDA — to get the current network-health snapshot.
| Network | Program ID |
|---|---|
| Devnet | 2paXpX8Ze3tvYezviSwQJSSihG3LbrDiD7SNsaFwgTow |
| Mainnet-beta | not yet deployed |
| Localnet | 2paXpX8Ze3tvYezviSwQJSSihG3LbrDiD7SNsaFwgTow |
| 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
);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)
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
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
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
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_count…other_count |
u16 |
Per-client display counts (averaged) |
total_agave_count…total_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
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 herenetwork_health— PDA[b"network_health"], created heresystem_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.
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 hereregistry— 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.
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:
- If the observer had a fresh previous attestation (within
STALE_SLOTS = 150slots), subtract their old contribution from the region's running totals before adding the new one. The observer count stays the same. - If the observer is new to this region or their previous attestation is stale, simply add their new values and increment
observer_count. - If the region itself is stale (no update for > 150 slots), clear the region's entire aggregate first before adding.
- 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.
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, mutableregistry_account— read-only (checked for paused state)clock— sysvarremaining_accounts— a list of allObserverAccountPDAs to include in the recompute
How it works:
- Iterates
remaining_accounts; skips any account not owned by this program or that fails deserialization. - Skips observers where
is_active == falseorlast_attestation_slotis older thanSTALE_SLOTS. - Snapshots each valid observer's data into an in-memory
ObserverSnapshot. - Clears all region aggregates in
NetworkHealthAccount. - Rebuilds running totals from the snapshots.
- Recomputes all averages and the global score.
- Requires at least one active region — fails with
NoActiveObserversif all observers are stale.
Side effects: Fully overwrites all fields of NetworkHealthAccount. Updates min_health_ever / max_health_ever. Updates global client distribution.
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 toobserver_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.
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 matchregistry.authority)observer_wallet— read-only (PDA seed derivation)observer_account— PDA, mutableregistry— mutabletreasury— 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.
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.
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).
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.
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 |
| 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 |
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.
| 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 |
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_scorevalues that have fresh data (withinSTALE_SLOTS).
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 withlast_attestation_slotwithinSTALE_SLOTSof the current slot are included. submit_attestationalso enforcescurrent_slot > observer.last_attestation_slotto prevent replay of the same slot's data.
Slashing is admin-controlled and acts on an observer's SOL escrow:
- Escrow: When an observer registers, they lock
min_stake_lamportsof SOL into theirObserverAccountPDA. This SOL does not leave the protocol without authority action. - Slash trigger: The admin calls
slash_observerwith a basis-points amount. Any reason for slashing (e.g., provably false attestations detected off-chain) is evaluated outside the program. - Slash calculation:
slash_amount = stake_lamports × slash_bps / 10_000. Uses saturating arithmetic throughout. - Rent safety: The program will never drain the PDA below its rent-exempt minimum — it checks
current_balance >= rent_min + slash_amountbefore proceeding. - Auto-deactivation: If post-slash
stake_lamports < min_stake_lamports, the observer is set tois_active = falseand removed from the active count. They can no longer submit attestations. - Treasury: Slashed funds are transferred to any arbitrary
treasuryaccount specified by the admin. - Deregistration (voluntary or by admin) always returns the remaining stake to the observer's wallet — slashing and deregistration are independent operations.
The program emits the following Anchor events, subscribable via program.addEventListener:
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 %
}pub struct ObserverRegistered {
pub observer: Pubkey,
pub region: Region,
pub stake_lamports: u64,
}pub struct ObserverDeregistered {
pub observer: Pubkey,
}pub struct ObserverSlashed {
pub observer: Pubkey,
pub slash_bps: u16,
pub amount_slashed: u64,
}pub struct ConfigUpdated {
pub min_stake_lamports: Option<u64>,
pub max_observers: Option<u16>,
pub paused: Option<bool>,
}| 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 |
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.
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) |
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.
| 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 |
The project pins a specific toolchain via rust-toolchain.toml. Cargo will install it automatically on first use.
yarn installanchor buildThe compiled .so is placed at target/deploy/s0racle_program.so.
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 testanchor deploy --provider.cluster devnetEnsure your wallet at ~/.config/solana/id.json has devnet SOL (solana airdrop 2 --url devnet).
solana program show 2paXpX8Ze3tvYezviSwQJSSihG3LbrDiD7SNsaFwgTow --url devnetimport { 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}`);
}
}program.addEventListener("AttestationSubmitted", (event) => {
console.log("New attestation:", event.observer.toBase58(), "score:", event.score);
});// 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);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
| 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 |
| Package | Version |
|---|---|
@coral-xyz/anchor |
as per package.json |
ts-mocha |
as per package.json |
typescript |
as per package.json |
See LICENSE in the root of the repository.
Built with Anchor on Solana.