Verifiable Random Function (VRF) oracle for the Arch Network. Any on-chain program can request unpredictable, provably-fair randomness by calling RequestRandomness; the oracle server responds with a cryptographic proof that lets anyone verify the random output off-chain or on-chain.
Cryptography is based on RFC 9381 — Verifiable Random Functions (VRFs), specifically the ECVRF-EDWARDS25519-SHA512-TAI suite defined in Section 5.5.
arch-rand-gen/
├── oracle-program/ On-chain Arch program (RBPF / SBF)
├── vrf-core/ Pure-Rust RFC 9381 VRF library
├── shared/ Constants shared between on-chain and off-chain code
├── server/ Off-chain oracle daemon
├── e2e-tests/ End-to-end integration test
├── Makefile Build + run shortcuts
└── dev.sh One-command tmux dev environment
An Arch RBPF program that manages randomness request accounts and verifies VRF proofs on-chain.
Instructions
| Instruction | Accounts | Description |
|---|---|---|
RequestRandomness |
fee_payer (signer, writable), request_pda (writable, new), oracle_treasury (writable), system_program | Creates a VrfRequest PDA, transfers the oracle fee to the treasury, and stores the caller-supplied seed. Optionally accepts a callback_pid + callback_disc so the oracle can CPI back into the caller's program when the request is fulfilled. |
FulfillRequest |
oracle (signer), request_pda (writable), callback_program | Called exclusively by the oracle server. Verifies the VRF proof against ORACLE_ED_PUBKEY, writes beta (the 64-byte random output) into the VrfRequest account, marks it Finalized, and optionally CPIs into the callback program. |
VrfRequest account layout (Borsh, ~186 bytes)
| Field | Type | Description |
|---|---|---|
discriminator |
[u8; 8] |
Unique account type tag |
requester |
[u8; 32] |
Fee payer pubkey |
nonce |
u64 |
Caller-supplied nonce; used in PDA derivation |
seed |
[u8; 32] |
Alpha string passed to the VRF |
status |
RequestStatus |
Pending → Finalized or Failed |
result |
[u8; 64] |
Beta — the verified random output |
pi |
[u8; 80] |
VRF proof stored for provably-fair auditability |
callback_pid |
[u8; 32] |
Optional callback program ID ([0u8;32] = none) |
callback_disc |
[u8; 8] |
First 8 bytes prepended to the CPI data payload |
bump |
u8 |
PDA canonical bump |
PDA seeds: [b"vrf_req", fee_payer_pubkey, nonce.to_le_bytes(), bump]
A no_std-compatible Rust library implementing the full RFC 9381 ECVRF-EDWARDS25519-SHA512-TAI suite. It is used both by the on-chain program (proof verification) and the oracle server (proof generation).
// Generate a proof (oracle server)
let pi: [u8; 80] = ecvrf_prove(&secret_key_32, alpha)?;
// Derive the random output from the proof (off-chain)
let beta: [u8; 64] = ecvrf_proof_to_hash(&pi)?;
// Verify a proof (on-chain program)
let beta: [u8; 64] = ecvrf_verify(&public_key_32, alpha, &pi)?;All three steps follow the algorithms in RFC 9381 Section 5. The test vectors in vrf-core/tests/vrf_vectors.rs are taken directly from Appendix B.1.
Crate used by both on-chain and off-chain code. Gated behind features = ["on-chain"] for programs that run without std.
| Module | Contents |
|---|---|
keys |
ORACLE_ED_PUBKEY (Ed25519, for VRF), ORACLE_ARCH_PUBKEY (secp256k1, for tx signing) |
fees |
ORACLE_FEE — lamports charged per request |
seeds |
VRF_REQ_SEED — PDA seed prefix |
network |
Testnet / mainnet RPC + explorer URLs |
An async Rust service (Tokio) that listens for RequestRandomness instructions on-chain and automatically fulfills them.
Architecture
WebSocket listener ──(mpsc channel)──> Processor thread
│ │
│ subscribes to all processed txs │ ecvrf_prove(oracle_ed_sk, seed) → pi
│ filters for oracle program ixs │ build + sign FulfillRequest tx
│ extracts seed + request_pda │ submit via blocking RPC
└───────────────────────────────────────┘
- The listener maintains a persistent WebSocket connection to the Arch leader node, re-connecting automatically on disconnect.
- The processor runs in a
spawn_blockingthread to avoid blocking the async runtime during the (CPU-bound) VRF proof computation. - Key files read from
~/.arch/:oracle.key(Arch signer),oracle_program.key(deployed program pubkey),treasury.key(fee recipient),.oracle_ed.key(Ed25519 VRF secret key).
test_deploy_and_request_randomness — deploys the oracle program to a running Arch localnet, submits a RequestRandomness instruction, and polls the VrfRequest PDA until the server fulfills it, then asserts the proof and random output are non-zero and the status is Finalized.
| Tool | Purpose |
|---|---|
| Rust stable | Compile off-chain crates |
Solana toolchain (cargo-build-sbf) |
Compile the on-chain RBPF program |
| Arch localnet | Running Arch node + Bitcoin regtest (arch-node) |
| tmux | Dev environment window management |
The oracle needs two keypairs: an Arch secp256k1 keypair (for signing transactions) and an Ed25519 keypair (for the VRF). Keys are stored under ~/.arch/.
# Arch keypairs — generated automatically on first server run if absent.
# The files are: ~/.arch/oracle.key ~/.arch/oracle_program.key ~/.arch/treasury.key
# Ed25519 VRF keypair — run once, then paste the printed constant into shared/src/keys.rs.
make keygenThe keygen target runs an ignored test that writes ~/.arch/.oracle_ed.key and prints the matching ORACLE_ED_PUBKEY constant. Paste that constant into shared/src/keys.rs before building the on-chain program.
make build
# or individually:
cargo build -p server
cd oracle-program && cargo build-sbfmake server
# or:
cargo run -p serverThe server connects to ws://127.0.0.1:10080 and begins listening for requests.
With Arch localnet running and the server started:
make e2eThis deploys the oracle program to localnet, sends a RequestRandomness, and waits for the server to fulfill it.
./dev.shBuilds everything, then opens a tmux session with two windows:
| Window | Command |
|---|---|
oracle |
cargo run -p server |
e2e |
Waits for localnet, then runs test_deploy_and_request_randomness |
Options:
--skip-build Skip build steps (use existing binaries)
--skip-e2e Open the e2e window as a plain shell (server only)
Switch windows with Ctrl+B N / Ctrl+B P. Detach with Ctrl+B D.
Caller tx
RequestRandomness { seed, callback_pid, callback_disc, bump }
→ oracle-program: create VrfRequest PDA (Pending), transfer fee
Oracle server (listener)
→ detects RequestRandomness instruction on-chain
Oracle server (processor)
→ pi = ecvrf_prove(oracle_ed_sk, seed) [RFC 9381 §5.1]
→ submit FulfillRequest { pi }
oracle-program (on-chain)
→ beta = ecvrf_verify(ORACLE_ED_PUBKEY, seed, pi) [RFC 9381 §5.3]
→ write beta + pi into VrfRequest PDA, mark Finalized
→ if callback_pid != [0;32]: CPI(callback_pid, callback_disc ++ beta)
Caller (callback or poll)
→ read VrfRequest.result → 64 bytes of verifiable randomness
The random output (beta) is fully deterministic given the oracle's secret key and the caller-supplied seed, yet unpredictable to the caller before the oracle publishes pi. Anyone can re-verify the output independently using ecvrf_verify.
The FulfillRequest instruction runs ecvrf_verify inside the RBPF VM to confirm the proof before writing beta to the VrfRequest account. This means the verification already happens on-chain as part of the oracle's fulfillment transaction — callers receive a result they can trust without doing any additional cryptographic work themselves.
It is technically possible for a caller program to re-verify the proof on-chain by importing vrf-core and calling ecvrf_verify directly. However, the full RFC 9381 ECVRF verification involves several Edwards25519 point decompressions, scalar multiplications, and a hash-to-curve operation, which together consume well over 1.4 million compute units — exceeding the current per-transaction CU budget. On-chain re-verification by consumer programs is therefore not practical at this time. Off-chain verification (see below) is the recommended approach for provably-fair auditing.
# Given: pi (hex), seed (hex), oracle_ed_pubkey (hex from shared/src/keys.rs)
# Derive beta off-chain:
python3 - <<'EOF'
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
# ... or use any RFC 9381-compatible library
EOFBecause the proof pi is stored in the VrfRequest account, anyone can independently verify that beta was computed correctly from the published oracle public key and the original seed — no trust in the oracle operator required.
Add a RequestRandomness CPI to your program:
let ix = Instruction {
program_id: oracle_program_id,
accounts: vec![
AccountMeta::new(fee_payer, true), // signer, writable
AccountMeta::new(request_pda, false), // writable, new
AccountMeta::new(oracle_treasury, false), // writable
AccountMeta::new_readonly(system_program::ID, false),
],
data: borsh::to_vec(&OracleInstruction::RequestRandomness {
nonce,
seed,
callback_pid: *your_program_id.as_ref(), // oracle will CPI back here
callback_disc: YOUR_CALLBACK_DISCRIMINATOR,
bump,
})?,
};
invoke(&ix, &[fee_payer_info, request_pda_info, treasury_info, system_info])?;Your program's callback entry point receives callback_disc ++ beta as instruction data, where beta is the 64-byte verified random output.