Solana ECVRF
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
alphais 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.
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:
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
[]
= "0.0.1"
use ;
let public_key = PublicKey;
let proof = Proof;
let alpha = request_alpha;
let output: = proof.verify?;
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:
= { = "0.0.1", = ["prove"] }
use SecretKey;
let secret_key = SecretKey;
let proof = secret_key.prove;
let output = proof.verify?;
Proving is never compiled for target_os = "solana": placing the secret key
on-chain would disclose it.
Using the output safely
- Construct
alphafrom data the player committed to before the oracle acted, such asrequest_id || slot_hash_at_request. - Reconstruct
alphainside 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
yencodings. - Proofs are
Gamma || c || s, with lengths32 + 16 + 32 = 80bytes. - Outputs are 64 bytes.
- Non-canonical
y >= pencodings followcurve25519-dalek, matching the Solana runtime. s >= Lis rejected before any syscall.- The
provefeature enables host-side proving withcurve25519-dalek. - The
static-syscallsfeature 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.