Verifiable Random Function
A bespoke VRF over Edwards25519 using SHAKE256 and the Elligator map — unpredictable outputs that anyone holding the public key can verify.
A verifiable random function is a keyed hash with a proof. Given a secret key and an input message, it produces an output that looks uniformly random to anyone without the key, yet is uniquely determined by the key and message, and comes with a proof that lets anyone holding the public key confirm the output is the right one. It is the primitive you want whenever a system needs randomness that participants cannot grind and cannot dispute.
import "github.com/sonr-io/crypto/vrf"
When to use one
- Leader election. Each validator computes
VRF_sk(round_seed). Whoever’s output falls below a threshold is the leader, and can prove it. Nobody can pre-compute another validator’s output, and nobody can retry with a different key without publishing that key. - Verifiable lotteries. Draw a winner from a beacon value; the operator proves the draw was honest without revealing the key.
- Private lookup keys. In a key-transparency directory (this construction’s origin), the map
index for a username is
VRF_sk(username), so the directory can prove a name’s absence without its tree structure leaking the set of registered names to an enumerating client.
Do not reach for a VRF where a plain signature would do — this package offers no way to sign arbitrary data, and verification only ever answers “is this the correct output for this message”. And do not treat the output as a commitment: it is a deterministic function of the message, so once a proof is published anyone holding the public key can confirm a guess at the message by re-verifying against it. A VRF hides the output from people without the key; it does not hide the input from people who can guess it.
The construction
The package doc comment states the scheme exactly. E is Curve25519 in Edwards coordinates, h is
SHA-3 (specifically SHAKE256 throughout the implementation), f is the Elligator map, and 8 is the
cofactor:
The proof is a Chaum–Pedersen style sigma protocol made non-interactive, proving that the same
secret x relates g → g^x and H(n) → H(n)^x:
with r = h(x, n) supplying the proof’s randomness — so proving, like computing, is fully
deterministic. Verification recomputes the challenge from g^t · P^c and H(n)^t · ii^c and checks
it equals the challenge carried in the proof, and separately checks that the claimed output equals
h(n, ii).
Concretely, in vrf.go: hashToCurve runs sha3.ShakeSum256 over the message, maps the digest with
extra25519.HashToEdwards, then applies three successive GeDouble calls — multiplication by the
cofactor 8 — to land in the prime-order subgroup. The challenge is
SHAKE256(g ‖ H(n) ‖ pk ‖ H(n)^x ‖ g^r ‖ H(n)^r ‖ n) reduced mod the group order. In the code the
challenge scalar is named s, which is why the proof layout below reads s ‖ t ‖ ii rather than
c ‖ t ‖ ii.
Sizes and constants
| Constant | Value | Meaning |
|---|---|---|
PublicKeySize |
32 |
Compressed Edwards point |
PrivateKeySize |
64 |
32-byte seed followed by the 32-byte public key |
Size |
32 |
The VRF output |
ProofSize |
96 |
s ‖ t ‖ H(n)^x, three 32-byte values |
ErrGetPubKey is the package’s only exported error value; it is declared but never returned by any
exported function in vrf.go — Public() signals failure through its boolean instead.
API
GenerateKey(rnd io.Reader)?(PrivateKey, error)
Reads 32 bytes of seed from rnd (crypto/rand when nil), expands it, and writes the derived public key into bytes 32..63. Returns a 64-byte PrivateKey.
(PrivateKey, error)PrivateKey.Public()?(PublicKey, bool)
Returns the trailing 32 bytes of the private key. The bool reports whether the internal type assertion succeeded; in practice it is always true for a well-formed key.
(PublicKey, bool)PrivateKey.Compute(m []byte)?[]byte
The 32-byte VRF output alone. One scalar multiplication plus a hash. No error return — a malformed key produces garbage rather than a failure.
[]bytePrivateKey.Prove(m []byte)?(vrf, proof []byte)
The same 32-byte output plus a 96-byte proof. Roughly three scalar multiplications. Deterministic — no reader, no nonce.
(vrf, proof []byte)PublicKey.Verify(m, vrfBytes, proof []byte)?bool
Checks the output against the proof under this public key. Returns false on any length mismatch, any bad point encoding, and any check failure. No error channel.
boolCompute and Prove return the same output for the same key and message — Prove just also
gives you the evidence. Use Compute when the holder needs the value locally (deciding whether it
even won a leader election, indexing its own directory) and Prove only when the value must be
published. That distinction is the main performance lever in the package: skipping the proof avoids
two of the three scalar multiplications.
Example
Grounded in TestHonestComplete and TestConvertPrivateKeyToPublicKey.
package main
import (
"bytes"
"fmt"
"log"
"github.com/sonr-io/crypto/vrf"
)
func main() {
// nil reader means crypto/rand.
sk, err := vrf.GenerateKey(nil)
if err != nil {
log.Fatal(err)
}
pk, ok := sk.Public()
if !ok {
log.Fatal(vrf.ErrGetPubKey)
}
round := []byte("epoch-4711")
// Cheap path: the holder just wants the value.
out := sk.Compute(round)
// Publishing path: the value plus evidence.
outFromProof, proof := sk.Prove(round)
fmt.Println("Compute == Prove:", bytes.Equal(out, outFromProof)) // true
fmt.Println("output bytes:", len(out), "proof bytes:", len(proof)) // 32 96
// Anyone with pk can check it.
fmt.Println("verified:", pk.Verify(round, outFromProof, proof)) // true
// Any single flipped bit in the proof fails the check.
tampered := append([]byte(nil), proof...)
tampered[0] ^= 0x01
fmt.Println("tampered verified:", pk.Verify(round, outFromProof, tampered)) // false
}
TestFlipBitForgery in the package flips bits across the proof and asserts every variant fails.
Properties a caller can rely on
| Property | What it means here |
|---|---|
| Uniqueness | For a fixed key and message there is exactly one output that will verify. A prover cannot shop for a favourable value. This is what a plain signature cannot give you. |
| Pseudorandomness | Without the secret key, the output is indistinguishable from a uniform 32-byte string, so future outputs cannot be predicted from past ones. |
| Public verifiability | Anyone with the 32-byte public key can check an output against its proof — no interaction with the prover, no shared secret. |
| Determinism | Both Compute and Prove derive all internal randomness from the key and message, so there is no RNG at evaluation time and nothing to fail open. |
Caveats
Related
Schnorr proofs
The general sigma protocol this VRF’s proof is a specialisation of.
Threshold Ed25519
Standards-conformant Ed25519 from distributed shares — the interoperable neighbour of this package’s non-standard key handling.
Curve abstraction
The library’s Ed25519 curve type, which this package deliberately bypasses.
Security notes
Vendored code, non-standard constructions, and unvalidated inputs across the library.