Production-grade Verifiable Random Functions (VRFs) on the Bandersnatch curve for the JAM protocol. This package is part of Peanut Butter AND JAM (PBNJ) and extends @pbnjam/bandersnatch with:
- IETF VRF — RFC 9381–style prover/verifier (Elligator2 hash-to-curve)
- Pedersen VRF — Prover/verifier with blinding
- Ring VRF — Pedersen VRF plus KZG ring membership proofs; WASM and W3F (native Rust) backends
- Crypto utilities — Hash-to-curve, nonce generation, challenge hashing (reusable building blocks)
| Resource | Link |
|---|---|
| Monorepo docs | Getting started |
| Specification | Gray Paper |
| Changelog (this package) | CHANGELOG.md |
| API (this package) | This README; run bun run docs for TypeDoc output |
- Prerequisites
- Installation
- Getting started
- Usage
- Schemes
- Crypto utilities
- Development
- Notes
- Contributing
- License
- Security
- Support and references
- Changelog
- Bun (v1.3.x or later) — primary runtime and package manager
- Rust (stable) — only if using the W3F Ring VRF backend (native
rust-ring-proofmodule)
From the PBNJ monorepo root:
git clone https://github.com/Esscrypt/peanutbutterandjam.git
cd peanutbutterandjam
git submodule update --init --recursive
bun installTo use this package from another workspace in the monorepo, depend on @pbnjam/bandersnatch-vrf in your package.json; the workspace will resolve it.
Ring VRF requires a Structured Reference String (SRS) in uncompressed arkworks format. The package includes a test SRS for rings up to size 2¹¹:
| Item | Value |
|---|---|
| Path (from package root) | test-data/srs/zcash-srs-2-11-uncompressed.bin |
| In monorepo | packages/bandersnatch-vrf/test-data/srs/zcash-srs-2-11-uncompressed.bin |
Pass the resolved path to the RingVRFProver* and RingVRFVerifier* constructors. From other packages (e.g. infra/node), resolve relative to the repo root or your app (e.g. path.join).
| Backend | Classes | When to use |
|---|---|---|
| WASM | RingVRFProverWasm, RingVRFVerifierWasm |
Any environment; no native build. |
| W3F | RingVRFProverW3F, RingVRFVerifierW3F |
Higher throughput; requires bun run build:native (Rust). |
| Pure TypeScript | RingVRFProver, RingVRFVerifier |
No Rust/WASM; tests and tooling; slowest. |
For a prover/verifier time comparison and analysis of all three options, see RING_VRF_PROVER_VERIFIER_ANALYSIS.md.
All three backends (Pure TypeScript, WASM, and W3F) use the same SRS and produce interchangeable proofs (same gamma/beta; verifiers from any backend accept proofs from any other).
import path from 'node:path'
import { RingVRFProverW3F, RingVRFVerifierW3F } from '@pbnjam/bandersnatch-vrf'
import { hexToBytes } from 'viem'
const srsFilePath = path.join(
__dirname,
'test-data/srs/zcash-srs-2-11-uncompressed.bin',
)
async function main() {
const prover = new RingVRFProverW3F(srsFilePath)
await prover.init()
const verifier = new RingVRFVerifierW3F(srsFilePath)
await verifier.init()
const secretKey = hexToBytes('0x...') // 32 bytes
const ringInput = {
input: hexToBytes('0x...'), // VRF input
auxData: hexToBytes('0x...'), // optional
ringKeys: [/* Uint8Array of compressed pubkeys, 32 bytes each */],
proverIndex: 0, // index of prover's key in ringKeys
}
const result = prover.prove(secretKey, ringInput)
// result.gamma, result.proof (pedersenProof, ringCommitment, ringProof)
const ok = verifier.verify(
ringInput.ringKeys,
ringInput,
{ gamma: result.gamma, proof: result.proof },
ringInput.auxData,
)
console.log('Verified:', ok)
}import path from 'node:path'
import { RingVRFProverWasm, RingVRFVerifierWasm } from '@pbnjam/bandersnatch-vrf'
import { hexToBytes } from 'viem'
const srsFilePath = path.join(
__dirname,
'test-data/srs/zcash-srs-2-11-uncompressed.bin',
)
async function main() {
const prover = new RingVRFProverWasm(srsFilePath)
await prover.init()
const verifier = new RingVRFVerifierWasm(srsFilePath)
await verifier.init()
const secretKey = hexToBytes('0x...')
const ringInput = {
input: hexToBytes('0x...'),
auxData: hexToBytes('0x...'),
ringKeys: [/* Uint8Array, 32 bytes each */],
proverIndex: 0,
}
const result = prover.prove(secretKey, ringInput)
const ok = verifier.verify(
ringInput.ringKeys,
ringInput,
{ gamma: result.gamma, proof: result.proof },
ringInput.auxData,
)
console.log('Verified:', ok)
}import path from 'node:path'
import { RingVRFProver, RingVRFVerifier } from '@pbnjam/bandersnatch-vrf'
import { hexToBytes } from 'viem'
const srsFilePath = path.join(
__dirname,
'test-data/srs/zcash-srs-2-11-uncompressed.bin',
)
const ringSize = 8
const prover = new RingVRFProver(srsFilePath, ringSize)
const verifier = new RingVRFVerifier(srsFilePath, ringSize)
const secretKey = hexToBytes('0x...') // 32 bytes
const ringInput = {
input: hexToBytes('0x...'), // VRF input
auxData: hexToBytes('0x...'), // optional
ringKeys: [/* Uint8Array of compressed pubkeys, 32 bytes each */],
proverIndex: 0, // index of prover's key in ringKeys
}
const result = prover.prove(secretKey, ringInput)
// result.gamma, result.proof (pedersenProof, ringCommitment, ringProof)
const ok = verifier.verify(
ringInput.ringKeys,
ringInput,
{ gamma: result.gamma, proof: result.proof },
ringInput.auxData,
)
console.log('Verified:', ok)The Pure TypeScript backend requires no native build and no WASM runtime. Construction is synchronous (no init() call needed), and it accepts an explicit ringSize parameter. It is significantly slower than the WASM and W3F backends (see benchmarks) but useful for testing, tooling, and environments where only a JavaScript runtime is available.
Serialization: Use RingVRFProver.serialize(result) for a Uint8Array and RingVRFProver.deserialize(bytes) to reconstruct a result. Verifiers accept either the raw { gamma, proof } or the deserialized value.
You can use the CLI (JSON files) or the programmatic API (see Getting started and Import the public API).
The CLI proves and verifies VRF proofs using JSON input/output files.
# Prove IETF VRF
bandersnatch-vrf prove ietf --input input.json --output proof.json
# Verify IETF VRF
bandersnatch-vrf verify ietf --input verify.json
# Prove Pedersen VRF
bandersnatch-vrf prove pedersen --input input.json --output proof.json
# Verify Pedersen VRF
bandersnatch-vrf verify pedersen --input verify.json
# Prove Ring VRF
bandersnatch-vrf prove ring --input input.json --output proof.json
# Verify Ring VRF
bandersnatch-vrf verify ring --input verify.jsonIETF VRF Prove Input (input.json):
{
"secretKey": "0x...",
"input": "0x...",
"auxData": "0x..." // optional
}IETF VRF Prove Output (proof.json):
{
"gamma": "0x...",
"proof": "0x..."
}IETF VRF Verify Input (verify.json):
{
"publicKey": "0x...",
"input": "0x...",
"proof": "0x...",
"auxData": "0x..." // optional
}Pedersen VRF Prove Input (input.json):
{
"secretKey": "0x...",
"input": "0x...",
"auxData": "0x..." // optional
}Pedersen VRF Prove Output (proof.json):
{
"gamma": "0x...",
"proof": "0x..."
}Pedersen VRF Verify Input (verify.json):
{
"input": "0x...",
"gamma": "0x...",
"proof": "0x...",
"auxData": "0x..." // optional
}Ring VRF Prove Input (input.json):
{
"secretKey": "0x...",
"input": "0x...",
"auxData": "0x...", // optional
"ringKeys": ["0x...", "0x...", ...],
"proverIndex": 0,
"srsFilePath": "./test-data/srs/zcash-srs-2-11-uncompressed.bin",
"useWasm": false // optional, default: false
}Ring VRF Prove Output (proof.json):
{
"gamma": "0x...",
"hash": "",
"proof": {
"pedersenProof": "0x...",
"ringCommitment": "0x...",
"ringProof": "0x...",
"proverIndex": 0
},
"serialized": "0x..."
}Ring VRF Verify Input (verify.json):
{
"ringKeys": ["0x...", "0x...", ...],
"input": "0x...",
"serializedResult": "0x...",
"auxData": "0x...", // optional
"srsFilePath": "./test-data/srs/zcash-srs-2-11-uncompressed.bin",
"useWasm": false // optional, default: false
}The CLI can be run in several ways:
- Using Bun directly:
bun run packages/bandersnatch-vrf/src/cli.ts prove ietf --input input.json- After building the binary:
bun run build:bin
./bin/bandersnatch-vrf prove ietf --input input.json- Programmatically:
import { proveIETF, verifyIETF } from '@pbnjam/bandersnatch-vrf'
await proveIETF('input.json', 'proof.json')
await verifyIETF('verify.json')import {
// Provers
IETFVRFProver,
PedersenVRFProver,
RingVRFProver,
RingVRFProverWasm,
RingVRFProverW3F,
// Verifiers
IETFVRFVerifier,
PedersenVRFVerifier,
RingVRFVerifier,
RingVRFVerifierWasm,
RingVRFVerifierW3F,
// Crypto helpers
elligator2HashToCurve,
generateNonceRfc8032,
generateChallengeRfc9381,
pointToHashRfc9381,
// CLI functions
proveIETF,
verifyIETF,
provePedersen,
verifyPedersen,
proveRing,
verifyRing,
runCLI,
} from '@pbnjam/bandersnatch-vrf'| Scheme | Prover | Verifier | Notes |
|---|---|---|---|
| IETF VRF | IETFVRFProver |
IETFVRFVerifier |
RFC 9381; Elligator2 hash-to-curve |
| Pedersen VRF | PedersenVRFProver |
PedersenVRFVerifier |
Blinded; no SRS |
| Ring VRF (Pure TS) | RingVRFProver |
RingVRFVerifier |
SRS required; no native/WASM build; slowest |
| Ring VRF (WASM) | RingVRFProverWasm |
RingVRFVerifierWasm |
SRS required; no native build |
| Ring VRF (W3F) | RingVRFProverW3F |
RingVRFVerifierW3F |
SRS required; native Rust; fastest |
- Prover:
IETFVRFProver.prove(secretKey, input, auxData?)→{ gamma, proof } - Verifier:
IETFVRFVerifier.verify(publicKey, input, proof, auxData?)→boolean
Key details reflected in the implementation:
- The prover uses Elligator2 hash-to-curve via
IETFVRFProver.hashToCurve(...). - Proofs are verified against a recomputed challenge using
generateChallengeRfc9381(...).
- Prover:
PedersenVRFProver.prove(secretKey, { input, auxData? }) - Verifier:
PedersenVRFVerifier.verify(input, gamma, proof, auxData?)
The Pedersen proof structure follows the code-defined layout:
PedersenVRFProof:(Y_bar, R, O_k, s, s_b)as byte arrays.
Ring VRF combines:
- A Pedersen VRF proof (blinded)
- A ring membership proof over a ring of public keys (KZG commitments)
SRS: use the same SRS file for all Ring VRF provers/verifiers, e.g. test-data/srs/zcash-srs-2-11-uncompressed.bin (see Getting started).
Main entry points:
- Prover:
new RingVRFProver(srsFilePath)thenprove(secretKey, input)(TypeScript/KZG). Or use W3F / WASM backends:RingVRFProverW3F,RingVRFProverWasm(see Simple usage above). - Verifier:
new RingVRFVerifier(srsFilePath)thenverify(ringKeys, input, serializedResult, auxData?). OrRingVRFVerifierW3F,RingVRFVerifierWasm.
Serialization helpers:
RingVRFProver.serialize(result)→Uint8ArrayRingVRFProver.deserialize(bytes)→RingVRFResult
WASM- and W3F-backed variants:
- WASM:
RingVRFProverWasm,RingVRFVerifierWasm(ark-vrf WASM; no native build). - W3F:
RingVRFProverW3F,RingVRFVerifierW3F(native Rust; faster; requiresbun run build:native).
The crypto/ exports are intended to be reusable building blocks:
- Elligator2 / hash-to-curve (
crypto/elligator2.ts)elligator2HashToCurve(message: Uint8Array): CurvePointcurvePointToNoble(point: CurvePoint): EdwardsPointcompressPoint(point: CurvePoint): string- plus helpers like
clearCofactor,isOnCurve,addPoints,scalarMultiply, etc.
- Nonce generation (
crypto/nonce-rfc8032.ts)generateNonceRfc8032(secretKey: Uint8Array, inputPoint: Uint8Array): Uint8Array
- Challenge / output hashing (
crypto/rfc9381.ts)generateChallengeRfc9381(...)pointToHashRfc9381(...)
From the package directory (packages/bandersnatch-vrf):
bun run build # build package
bun run test # run test suite
bun run build:native # build Rust ring-proof module (optional; for W3F backend)
bun run docs # generate API documentation (TypeDoc → docs/)For a full comparison of prover/verifier times and trade-offs across all three backends (WASM, W3F, Pure TypeScript), see RING_VRF_PROVER_VERIFIER_ANALYSIS.md.
The ring end-to-end test runs Pure TypeScript (and optionally WASM/W3F when available) and reports execution time for each.
Command (from packages/bandersnatch-vrf):
bun test src/__tests__/ring-end-to-end.test.tsHardware (machine used for the numbers below):
- CPU: Apple M4
- Arch: arm64
- Cores: 10
Typical results (2 test vectors, ring size 8):
| Backend | Prove | Verify |
|---|---|---|
| Pure TypeScript | ~25–26 s | ~5 s |
| WASM | ~740–1 400 ms | ~170–220 ms |
| W3F (native) | ~190–220 ms | ~66–72 ms |
Per-vector (Pure TypeScript) — from bun test src/__tests__/ring-end-to-end.test.ts:
| Vector | Pure TS prove | Pure TS verify |
|---|---|---|
| 1 (bandersnatch_sha-512_ell2_ring) | 25 867 ms | 5 018 ms |
| 2 (bandersnatch_sha-512_ell2_ring) | 26 463 ms | 4 895 ms |
| Vector | WASM prove | WASM verify | W3F prove | W3F verify |
|---|---|---|---|---|
| 1 | ~1400 ms | ~220 ms | ~220 ms | ~72 ms |
| 2 | ~740 ms | ~170 ms | ~190 ms | ~66 ms |
W3F (native) is roughly 4–6× faster than WASM on prove and ~3× faster on verify. Pure TypeScript is ~100× slower than W3F and is intended for environments that cannot use native or WASM. See RING_VRF_PROVER_VERIFIER_ANALYSIS.md for the full comparison.
- Data formats: Compressed curve points are 32-byte
Uint8Arrays; scalars are little-endian, consistent with the JAM codec. - Ring VRF: Prover and verifier constructors require a path to a compatible SRS file (see SRS file).
- Specification: Ring VRF test vectors align with the bandersnatch-vrf-spec; IETF VRF follows RFC 9381.
Calling await prover.init() / await verifier.init() on the W3F (native Rust) backend is a one-time, upfront cost that can consume several gigabytes of native process memory. For a full JAM validator ring of 1 023 keys, expect ~3–4 GB RSS after initialisation.
Why so large?
The RingPiopParams::setup step precomputes polynomial commitment data (Lagrange bases, KZG evaluations) for every validator public key in the ring. This data is allocated on the Rust heap inside the napi-rs native module and held for the lifetime of the process so that individual prove/verify calls are fast.
What you will observe in process.memoryUsage():
| Field | Typical value | What it represents |
|---|---|---|
heapUsed |
~500 MB | JavaScript objects (services, state, etc.) |
external |
~100–150 MB | Node.js Buffers / TypedArrays |
rss |
~3.5–4 GB | Total process RSS, includes Rust native heap |
Gap (rss − heap − external) |
~3–3.5 GB | Rust ring-proof precomputed data (not tracked by JS GC) |
The gap does not appear in heapUsed or external because napi-rs allocations bypass the JavaScript garbage collector entirely. This is expected behaviour, not a memory leak — the value is stable after init and does not grow with block count.
Mitigation options:
- Use the WASM backend (
RingVRFProverWasm) if resident memory is a hard constraint; WASM linear memory is larger than V8's trackedexternalbut the footprint profile differs. - Initialise the prover and verifier once at process start and reuse them across all operations; re-initialising for every block or trace multiplies the cost.
This package is part of Peanut Butter AND JAM (PBNJ) and is developed as a submodule with its own issue and PR templates. See CONTRIBUTING.md for setup and process. When opening issues or PRs in this repo: Bug report, Feature request, PR template.
Licensed under the Apache License 2.0. See LICENSE in this directory.
To report a security vulnerability, do not open a public issue. See SECURITY.md in the repository root for responsible disclosure.
- JAM protocol and PBNJ: Gray Paper, PBNJ docs
- Issues and discussions: GitHub Issues