Skip to content
Sonr Crypto
Esc
navigateopen⌘Jpreview
On this page

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:

H(n)=f(h(n))8,VRFx(n)=h ⁣(n,H(n)x)H(n) = f(h(n))^8, \qquad \mathrm{VRF}_x(n) = h\!\left(n,\, H(n)^x\right)

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:

Provex(n)=(c,  t=rcx,  ii=H(n)x)\mathrm{Prove}_x(n) = \bigl(c,\; t = r - c\cdot x,\; \mathit{ii} = H(n)^x\bigr)

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.goPublic() signals failure through its boolean instead.

API

PropType
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.

Type(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.

Type(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.

Type[]byte
PrivateKey.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.

Type(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.

Typebool

Compute 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

Last updated on September 2, 2026

Was this page helpful?