feat: init docs

This commit is contained in:
Prad Nukala
2026-09-02 15:29:51 -04:00
parent d2390a8aad
commit 69425e2b7a
45 changed files with 11790 additions and 18 deletions
+244
View File
@@ -0,0 +1,244 @@
---
title: Verifiable Random Function
description: A bespoke VRF over Edwards25519 using SHAKE256 and the Elligator map — unpredictable outputs that anyone holding the public key can verify.
sidebar:
order: 5
icon: dice-5
---
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.
```go
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, \qquad \mathrm{VRF}_x(n) = h\!\left(n,\, H(n)^x\right)
$$
The proof is a ChaumPedersen style sigma protocol made non-interactive, proving that the same
secret `x` relates `g → g^x` and `H(n) → 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.go` — `Public()` signals failure through its boolean instead.
## API
<TypeTable
type={{
"GenerateKey(rnd io.Reader)": {
type: "(PrivateKey, error)",
description: "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.Public()": {
type: "(PublicKey, bool)",
description: "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.",
},
"PrivateKey.Compute(m []byte)": {
type: "[]byte",
description: "The 32-byte VRF output alone. One scalar multiplication plus a hash. No error return — a malformed key produces garbage rather than a failure.",
},
"PrivateKey.Prove(m []byte)": {
type: "(vrf, proof []byte)",
description: "The same 32-byte output plus a 96-byte proof. Roughly three scalar multiplications. Deterministic — no reader, no nonce.",
},
"PublicKey.Verify(m, vrfBytes, proof []byte)": {
type: "bool",
description: "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.",
},
}}
/>
`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`.
```go vrf.go
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
:::danger[This is a bespoke construction with no standards claim]
The package doc names no RFC and no paper. It is **not** RFC 9381 (`draft-irtf-cfrg-vrf`) — that
standard specifies SHA-512 with `try-and-increment` or `hash_to_curve` for `ECVRF-EDWARDS25519-SHA512-*`
ciphersuites, and a differently structured proof and encoding. This package uses SHAKE256 throughout
and the Elligator map, and packs the proof as `s ‖ t ‖ ii`.
The design matches the VRF shipped with the CONIKS key-transparency work, but nothing in this
repository asserts conformance to any published specification, and there are no cross-implementation
test vectors — `vrf_test.go` contains three self-consistency tests and four benchmarks, nothing more.
**Assume zero interoperability** with any other VRF implementation. If your protocol requires a
counterparty running different software to verify these proofs, this package is the wrong choice.
:::
:::danger[The 64-byte key is not an Ed25519 key, despite looking like one]
`PrivateKey` is `[]byte` with the same 64-byte seed-then-public-key layout as
`crypto/ed25519.PrivateKey`, and `Public()` is implemented by converting to
`golang.org/x/crypto/ed25519.PrivateKey` and calling through. But `GenerateKey` derives the scalar by
expanding the seed with **SHAKE256**, where Ed25519 uses SHA-512. The public key written into bytes
32..63 therefore corresponds to a *different* scalar than standard Ed25519 would derive from the same
seed.
Measured against this repository, for one generated key:
```
ed25519.Sign with the vrf key, verified under its own embedded pubkey: false
ed25519.NewKeyFromSeed(sk[:32]) derives the same public key: false
```
The types will not stop you — both are `[]byte` with identical lengths. Never share a seed, a key,
or a signature between `vrf` and `crypto/ed25519`.
:::
:::warning[No error channel anywhere on the hot path]
`Compute` returns only `[]byte`; `Prove` returns only two slices; `Verify` returns only `bool`. A
truncated key, a wrong-length public key, or a corrupted proof all surface as `false` or as silently
wrong bytes. `Compute` in particular does no validation at all — calling it on a short or zero
`PrivateKey` will panic or return meaningless output rather than report anything. Validate lengths
against `PrivateKeySize` and `PublicKeySize` at your trust boundary.
:::
:::warning[Vendored curve code]
The implementation depends on `internal/ed25519/edwards25519` and `internal/ed25519/extra25519`,
vendored copies rather than the maintained `filippo.io/edwards25519`. `extra25519.HashToEdwards` is
the Elligator implementation, and the package doc notes the map "covers half of E" — the cofactor
clearing by three doublings is what brings the result into the prime-order subgroup. None of this
code is constant-time by construction, and none of it receives upstream security fixes. See
[security notes](/reference/security).
:::
:::note[`Verify` does not check that the public key is in the prime-order subgroup]
It calls `FromBytesBaseGroup` on the encoded point, which rejects non-canonical encodings, but the
protocol's security against a maliciously chosen public key rests on the cofactor clearing inside
`hashToCurve` rather than on validating the key. If public keys arrive from untrusted parties in your
protocol, validate them yourself before storing.
:::
## Related
<CardGroup cols={2}>
<Card title="Schnorr proofs" href="/zero-knowledge/schnorr" icon="binary">
The general sigma protocol this VRF's proof is a specialisation of.
</Card>
<Card title="Threshold Ed25519" href="/threshold/threshold-ed25519" icon="users">
Standards-conformant Ed25519 from distributed shares — the interoperable neighbour of this
package's non-standard key handling.
</Card>
<Card title="Curve abstraction" href="/foundations/curves" icon="git-branch">
The library's Ed25519 curve type, which this package deliberately bypasses.
</Card>
<Card title="Security notes" href="/reference/security" icon="shield">
Vendored code, non-standard constructions, and unvalidated inputs across the library.
</Card>
</CardGroup>