solana-ecvrf 0.0.1

ECVRF-EDWARDS25519-SHA512-TAI (RFC 9381) verification for Solana programs using curve25519 and sha512 syscalls
Documentation

Solana ECVRF

CI License: MIT

Verifiable randomness for Solana programs, using the ECVRF-EDWARDS25519-SHA512-TAI ciphersuite from RFC 9381. Verification costs about 10k compute units.

What is an ECVRF?

An elliptic-curve verifiable random function turns a secret key and an input into two values:

  • a deterministic 64-byte pseudorandom output; and
  • an 80-byte proof that anyone with the public key can verify.

For a given public key and input, exactly one output can verify. The prover cannot try several valid proofs and choose the most favourable result, as it could with ordinary nonce-based signatures.

That makes an ECVRF useful when a Solana program needs randomness from an off-chain oracle without trusting the oracle to choose the result.

An ECVRF does not force the oracle to respond. The oracle can still withhold an unfavourable result, so applications need a timeout, fallback, or economic penalty when liveness matters.

How it works

                    secret key + alpha
                            │
                            ▼
                     ECVRF prove
                            │
                  proof + random output
                            │
        public key + alpha + proof
                            │
                            ▼
                     ECVRF verify
                            │
                 same output or rejection

alpha binds the proof to one request. A typical program constructs it from a request identifier and state committed before the oracle acts. The program must reconstruct alpha; accepting it directly from instruction data gives the caller control over the randomness domain.

The proof contains an encoded curve point Gamma, a 16-byte challenge c, and a 32-byte response s. Verification hashes alpha onto Edwards25519, checks two group equations, recomputes the challenge, and only then derives the random output from Gamma.

This implementation

The Rust crate is no_std and has no on-chain dependencies. Expensive curve arithmetic and SHA-512 run through Solana syscalls; the crate itself handles proof parsing, scalar encoding checks, and the small-order lookup.

Only ECVRF-EDWARDS25519-SHA512-TAI (suite_string = 0x03) is implemented. The ELL2 suite needs an Elligator 2 map in software, which costs more than the TAI retry it replaces on Solana.

RFC 9381 operation Solana implementation Cost
Reject small-order public keys Seven-entry encoded-y lookup ~20 CU
Reject s >= L Two-limb comparison ~20 CU
Hash alpha to H sol_sha512, point validation, then three doublings input-dependent
U = s·B - c·Y One two-term sol_curve_multiscalar_mul 3,031 CU
V = s·H - c·Gamma One two-term sol_curve_multiscalar_mul 3,031 CU
Recompute c sol_sha512 over 163 bytes ~250 CU
Derive the output Three doublings and sol_sha512 ~1,637 CU

The multiscalar multiplications decompress the public key and Gamma, so separate point-validation syscalls would duplicate work. Multiplication by the cofactor uses three additions because add(P, P) costs 473 CU versus 2,177 CU for scalar multiplication.

sol_sha512 is provided by SIMD-0512, feature gate s512oDwgx8hjMnaQjXfqqrZroVj4HvC6TkN3iSSWXCh. A program using this crate cannot deploy to a cluster where that feature is inactive.

Tests and verification

The implementation is checked at four levels:

  • all RFC 9381 Appendix B.3 vectors are reproduced exactly;
  • deterministic generated vectors pin the Rust and TypeScript implementations byte-for-byte;
  • tampering with every proof byte, the public key, or alpha is rejected; and
  • sBPF tests execute the compiled program under Mollusk and measure compute units.

The small-order lookup table and scalar arithmetic are also checked directly against curve25519-dalek.

The sBPF tests require cargo-build-sbf 4.x and platform-tools v1.53 or newer.

cargo test --lib
cargo +nightly clippy --all-targets -- -D warnings
bun run test
cargo test --test sbpf -- --nocapture --test-threads=1

Current local sBPF measurements with platform-tools v1.53 are:

Operation Compute units
Proof::verify, first candidate succeeds 9,930
Proof::verify, one retry 10,277
PublicKey::validate 193

These are reproducible local measurements, not proof that a particular cluster has activated the required syscall.

TypeScript: create proofs off-chain

Install the oracle SDK:

bun add @blueshift-gg/solana-ecvrf

A Solana keypair is already an ECVRF keypair. Its first 32 bytes are the Ed25519 seed and its address is the ECVRF public key.

import { readFileSync } from 'node:fs';
import { SecretKey } from '@blueshift-gg/solana-ecvrf';

const keypair = JSON.parse(readFileSync('id.json', 'utf8'));
const secretKey = SecretKey.fromKeypair(keypair);
const alpha = new TextEncoder().encode('request-42');

const proof = secretKey.prove(alpha);
const output = proof.verify(secretKey.publicKey, alpha);

console.log(secretKey.publicKey.toString());
console.log(Buffer.from(proof.bytes).toString('hex'));
console.log(Buffer.from(output).toString('hex'));

SecretKey.fromSeed also accepts a 32-byte Ed25519 seed. PublicKey.from accepts either 32 bytes or a base58 Solana address. Proof.from accepts the 80 encoded proof bytes.

Proof.verify returns the 64-byte output or throws when the key or proof is invalid. Keys and proofs are immutable value objects; their bytes getters return copies.

Rust: verify proofs on-chain

[dependencies]
solana-ecvrf = "0.0.1"
use solana_ecvrf::{Proof, PublicKey};

let public_key = PublicKey(oracle_public_key_bytes);
let proof = Proof(proof_bytes);
let alpha = request_alpha;

let output: [u8; 64] = proof.verify(&public_key, alpha)?;

verify performs public-key validation, proof verification, and output derivation as one operation. PublicKey::validate is available separately for programs that validate and store oracle keys during registration.

Host applications can also enable the reference prover:

solana-ecvrf = { version = "0.0.1", features = ["prove"] }
use solana_ecvrf::SecretKey;

let secret_key = SecretKey(seed);
let proof = secret_key.prove(alpha);
let output = proof.verify(&secret_key.public_key(), alpha)?;

Proving is never compiled for target_os = "solana": placing the secret key on-chain would disclose it.

Using the output safely

  • Construct alpha from data the player committed to before the oracle acted, such as request_id || slot_hash_at_request.
  • Reconstruct alpha inside the program instead of accepting arbitrary bytes from the proof submission.
  • Store and validate the oracle public key during registration.
  • Use one proof per request, then derive multiple draws with a domain-separated hash such as SHA-256(output || draw_index).
  • Define what happens when the oracle withholds a proof.

Encoding and features

  • Public keys and curve points use RFC 8032 compressed Edwards y encodings.
  • Proofs are Gamma || c || s, with lengths 32 + 16 + 32 = 80 bytes.
  • Outputs are 64 bytes.
  • Non-canonical y >= p encodings follow curve25519-dalek, matching the Solana runtime.
  • s >= L is rejected before any syscall.
  • The prove feature enables host-side proving with curve25519-dalek.
  • The static-syscalls feature forces the sBPF v3 static-syscall ABI when a supporting toolchain does not advertise it automatically.

License

MIT. This software is provided as-is; review it for your own threat model before using it to secure value.