# Solana ECVRF
[](https://github.com/blueshift-gg/solana-ecvrf/actions/workflows/ci.yml)
[](https://github.com/blueshift-gg/solana-ecvrf/blob/master/LICENSE)
Verifiable randomness for Solana programs, using the
ECVRF-EDWARDS25519-SHA512-TAI ciphersuite from
[RFC 9381](https://www.rfc-editor.org/rfc/rfc9381). 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
```text
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.
| 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](https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0512-sha512-syscall.md),
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.
```sh
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:
| `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:
```sh
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.
```ts
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
```toml
[dependencies]
solana-ecvrf = "0.0.1"
```
```rust
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:
```toml
solana-ecvrf = { version = "0.0.1", features = ["prove"] }
```
```rust
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.