Verifiable Delay Functions (VDF) library for Node.js and browsers. Pure TypeScript implementation with precomputed discriminants for optimal performance.
A Verifiable Delay Function (VDF) is a function that requires substantial time to evaluate (even with a polynomial number of parallel processors) but can be very quickly verified as correct. VDFs can be used to construct randomness beacons with multiple applications in distributed network environments.
By introducing a time delay during evaluation, VDFs prevent malicious actors from influencing output. The output cannot be differentiated from a random number until the final result is computed.
This implementation provides two VDF constructions:
estimateDifficulty) for approximate target delaysnpm install crypto-vdf
const { PietrzakVDFParams, DISCRIMINANT_256 } = require('crypto-vdf');
// Pietrzak VDF with 256-bit precomputed discriminant
const vdf = new PietrzakVDFParams(256).new();
const challenge = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
// Solve and verify (uses precomputed discriminant automatically)
const proof = await vdf.solve(challenge, 100, DISCRIMINANT_256);
vdf.verify(challenge, 100, proof, DISCRIMINANT_256);
console.log('✓ Proof verified!');
import { WesolowskiVDFParams, DISCRIMINANT_512 } from 'crypto-vdf';
const vdf = new WesolowskiVDFParams(512).new();
const challenge = new Uint8Array([0xaa]);
const proof = await vdf.solve(challenge, 100, DISCRIMINANT_512);
vdf.verify(challenge, 100, proof, DISCRIMINANT_512);
console.log('✓ Proof verified!');
<!DOCTYPE html>
<html>
<head>
<title>VDF Browser Example</title>
</head>
<body>
<h1>VDF Demo</h1>
<button onclick="runVDF()">Run VDF</button>
<pre id="output"></pre>
<script src="node_modules/crypto-vdf/dist/browser/vdf.min.js"></script>
<script>
async function runVDF() {
const output = document.getElementById('output');
output.textContent = 'Running VDF...\n';
// Use precomputed discriminant for fast performance
const vdfInstance = new vdf.WesolowskiVDFParams(256).new();
const challenge = new Uint8Array([0xaa, 0xbb, 0xcc]);
output.textContent += 'Generating proof...\n';
const proof = await vdfInstance.solve(challenge, 100, vdf.DISCRIMINANT_256);
output.textContent += 'Verifying proof...\n';
vdfInstance.verify(challenge, 100, proof, vdf.DISCRIMINANT_256);
output.textContent += '✓ Proof verified!\n';
output.textContent += `Proof size: ${proof.length} bytes`;
}
</script>
</body>
</html>
This library includes precomputed discriminants (256, 512, 1024, 2048 bits) generated using Rust GMP for optimal performance. Always use precomputed discriminants as generating them in pure JavaScript is extremely slow.
// Node.js / ES Modules
import {
WesolowskiVDFParams,
DISCRIMINANT_256,
DISCRIMINANT_512,
getPrecomputedDiscriminant
} from 'crypto-vdf';
const vdf = new WesolowskiVDFParams(256).new();
const challenge = new Uint8Array([0xaa]);
// Method 1: Use specific discriminant constant
const proof = await vdf.solve(challenge, 100, DISCRIMINANT_256);
vdf.verify(challenge, 100, proof, DISCRIMINANT_256);
// Method 2: Get discriminant by bit size
const disc = getPrecomputedDiscriminant(512);
const vdf512 = new WesolowskiVDFParams(512).new();
await vdf512.solve(challenge, 100, disc);
// Browser (same API, discriminants available as vdf.DISCRIMINANT_256, etc.)
Why use precomputed discriminants?
Warning: Generating custom discriminants in pure JavaScript is very slow (can take minutes). Only use if you have specific requirements.
import { createDiscriminant } from 'crypto-vdf';
// This will be VERY slow! Use precomputed discriminants instead.
const customDisc = createDiscriminant(Buffer.from('my-seed'), 256);
await vdf.solve(challenge, 100, customDisc);
import {
WesolowskiVDFParams,
PietrzakVDFParams,
DISCRIMINANT_256,
DISCRIMINANT_512,
InvalidProof
} from 'crypto-vdf';
// Example 1: Wesolowski VDF (recommended for most cases)
const wesolowski = new WesolowskiVDFParams(256).new();
const challenge = new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd]);
const proof1 = await wesolowski.solve(challenge, 100, DISCRIMINANT_256);
wesolowski.verify(challenge, 100, proof1, DISCRIMINANT_256);
console.log('✓ Wesolowski proof verified!');
// Example 2: Pietrzak VDF (requires even difficulty >= 66)
const pietrzak = new PietrzakVDFParams(512).new();
const proof2 = await pietrzak.solve(challenge, 100, DISCRIMINANT_512);
pietrzak.verify(challenge, 100, proof2, DISCRIMINANT_512);
console.log('✓ Pietrzak proof verified!');
// Example 3: Error handling
try {
const badProof = new Uint8Array(256);
wesolowski.verify(challenge, 100, badProof, DISCRIMINANT_256);
} catch (error) {
if (error instanceof InvalidProof) {
console.log('✗ Proof verification failed (expected)');
}
}
<!DOCTYPE html>
<html>
<head>
<title>VDF Demo</title>
<style>
body { font-family: monospace; padding: 20px; }
button { padding: 10px 20px; font-size: 16px; }
#output { background: #f4f4f4; padding: 15px; margin-top: 20px; }
</style>
</head>
<body>
<h1>Verifiable Delay Function Demo</h1>
<p>Click to run a VDF proof (takes a few seconds)</p>
<button onclick="runWesolowski()">Run Wesolowski VDF</button>
<button onclick="runPietrzak()">Run Pietrzak VDF</button>
<button onclick="compareVDFs()">Compare Both</button>
<pre id="output"></pre>
<script src="node_modules/crypto-vdf/dist/browser/vdf.min.js"></script>
<script>
const output = document.getElementById('output');
async function runWesolowski() {
output.textContent = 'Running Wesolowski VDF (256-bit)...\n';
const start = performance.now();
const vdfInstance = new vdf.WesolowskiVDFParams(256).new();
const challenge = new Uint8Array([0xaa, 0xbb, 0xcc]);
const proof = await vdfInstance.solve(challenge, 100, vdf.DISCRIMINANT_256);
const solveTime = (performance.now() - start).toFixed(0);
vdfInstance.verify(challenge, 100, proof, vdf.DISCRIMINANT_256);
const totalTime = (performance.now() - start).toFixed(0);
output.textContent += `✓ Proof verified!\n`;
output.textContent += ` Solve time: ${solveTime}ms\n`;
output.textContent += ` Verify time: ${totalTime - solveTime}ms\n`;
output.textContent += ` Proof size: ${proof.length} bytes\n`;
}
async function runPietrzak() {
output.textContent = 'Running Pietrzak VDF (256-bit)...\n';
const start = performance.now();
const vdfInstance = new vdf.PietrzakVDFParams(256).new();
const challenge = new Uint8Array([0xaa, 0xbb, 0xcc]);
const proof = await vdfInstance.solve(challenge, 100, vdf.DISCRIMINANT_256);
const solveTime = (performance.now() - start).toFixed(0);
vdfInstance.verify(challenge, 100, proof, vdf.DISCRIMINANT_256);
const totalTime = (performance.now() - start).toFixed(0);
output.textContent += `✓ Proof verified!\n`;
output.textContent += ` Solve time: ${solveTime}ms\n`;
output.textContent += ` Verify time: ${totalTime - solveTime}ms\n`;
output.textContent += ` Proof size: ${proof.length} bytes\n`;
}
async function compareVDFs() {
output.textContent = 'Comparing VDF implementations...\n\n';
const challenge = new Uint8Array([0xaa]);
// Wesolowski
output.textContent += '1. Wesolowski VDF:\n';
const w = new vdf.WesolowskiVDFParams(256).new();
const w_start = performance.now();
const w_proof = await w.solve(challenge, 100, vdf.DISCRIMINANT_256);
const w_time = performance.now() - w_start;
w.verify(challenge, 100, w_proof, vdf.DISCRIMINANT_256);
output.textContent += ` Time: ${w_time.toFixed(0)}ms, Size: ${w_proof.length}B\n\n`;
// Pietrzak
output.textContent += '2. Pietrzak VDF:\n';
const p = new vdf.PietrzakVDFParams(256).new();
const p_start = performance.now();
const p_proof = await p.solve(challenge, 100, vdf.DISCRIMINANT_256);
const p_time = performance.now() - p_start;
p.verify(challenge, 100, p_proof, vdf.DISCRIMINANT_256);
output.textContent += ` Time: ${p_time.toFixed(0)}ms, Size: ${p_proof.length}B\n\n`;
output.textContent += `Wesolowski is ${(p_time/w_time).toFixed(1)}x faster\n`;
output.textContent += `Wesolowski proof is ${((1-w_proof.length/p_proof.length)*100).toFixed(0)}% smaller`;
}
</script>
</body>
</html>
Both PietrzakVDF and WesolowskiVDF implement the same interface:
interface VDF {
// Solve VDF and generate proof (use precomputed discriminant in production)
solve(
challenge: Uint8Array,
difficulty: number,
discriminant?: bigint
): Promise<Uint8Array>;
// Verify a proof
verify(
challenge: Uint8Array,
difficulty: number,
proof: Uint8Array,
discriminant?: bigint
): void; // Throws InvalidProof if verification fails
// Check if difficulty is valid for this VDF type
checkDifficulty(difficulty: number): void;
}
⚠️ IMPORTANT LIMITATION: Pietrzak VDF has a maximum difficulty limit of 7000 in this JavaScript implementation. Difficulties above 7000 will fail verification. This is due to accumulation of rounding errors in extended proof chains. For production use or difficulties above 7000, use Wesolowski VDF instead.
import { PietrzakVDFParams, DISCRIMINANT_256 } from 'crypto-vdf';
const params = new PietrzakVDFParams(256); // bit length: 256, 512, 1024, or 2048
const vdf = params.new();
// ⚠️ Constraints:
// - Difficulty must be even
// - Difficulty must be >= 66
// - Difficulty must be <= 7000 (hard limit - use Wesolowski for higher difficulties)
const proof = await vdf.solve(challenge, 100, DISCRIMINANT_256);
vdf.verify(challenge, 100, proof, DISCRIMINANT_256);
import { WesolowskiVDFParams, DISCRIMINANT_512 } from 'crypto-vdf';
const params = new WesolowskiVDFParams(512); // bit length: 256, 512, 1024, or 2048
const vdf = params.new();
// ✅ Accepts any positive difficulty
// ✅ Smaller proofs than Pietrzak
// ✅ Faster verification
const proof = await vdf.solve(challenge, 200, DISCRIMINANT_512);
vdf.verify(challenge, 200, proof, DISCRIMINANT_512);
difficulty is an iteration count, not seconds. Use estimateDifficulty to pick an approximate value for a desired wall-clock delay. Figures are approximate (hardware and JS engine vary); measure on your machine when accuracy matters.
import {
estimateDifficulty,
estimateSolveSeconds,
getCalibratedIterationsPerSecond,
WesolowskiVDFParams,
DISCRIMINANT_256,
} from 'crypto-vdf';
// ~2 seconds of sequential work on a typical modern Node.js CPU (256-bit)
const difficulty = estimateDifficulty({ bits: 256, targetSeconds: 2 });
const vdf = new WesolowskiVDFParams(256).new();
const challenge = new Uint8Array([0xaa, 0xbb, 0xcc]);
const proof = await vdf.solve(challenge, difficulty, DISCRIMINANT_256);
vdf.verify(challenge, difficulty, proof, DISCRIMINANT_256);
// Pietrzak: result is even and clamped to [66, 7000]
const pietrzakDiff = estimateDifficulty({
bits: 256,
targetSeconds: 1,
scheme: 'pietrzak',
});
solve() on the target device and compute iterations/second:
ips = difficulty / (elapsedSeconds).const difficulty = estimateDifficulty({
bits: 512,
targetSeconds: 5,
iterationsPerSecond: 3200, // measured on this machine
});
// Inverse: approximate seconds for a known difficulty
const seconds = estimateSolveSeconds({
bits: 512,
difficulty,
iterationsPerSecond: 3200,
});
Built-in calibration (Node.js, precomputed discriminant, typical modern CPU):
| Bit length | Approx. iterations/second |
|---|---|
| 256 | 12 000 |
| 512 | 5 000 |
| 1024 | 1 800 |
| 2048 | 600 |
Read the table with getCalibratedIterationsPerSecond(bits). Browsers and low-power devices are often several times slower — always re-calibrate for production delay targets.
import { PietrzakVDFParams, DISCRIMINANT_2048 } from 'crypto-vdf';
const vdf = new PietrzakVDFParams(2048).new();
const challenge = new Uint8Array([0xaa]);
const proof = await vdf.solve(challenge, 100, DISCRIMINANT_2048);
// Proof is deterministic for same inputs
const proof2 = await vdf.solve(challenge, 100, DISCRIMINANT_2048);
console.assert(Buffer.compare(proof, proof2) === 0);
import {
PietrzakVDFParams,
InvalidProof,
InvalidIterations,
DISCRIMINANT_2048
} from 'crypto-vdf';
const vdf = new PietrzakVDFParams(2048).new();
const challenge = new Uint8Array([0xaa]);
try {
// This will throw InvalidIterations
vdf.checkDifficulty(67); // Odd number
} catch (error) {
if (error instanceof InvalidIterations) {
console.error('Invalid difficulty:', error.message);
}
}
try {
// This will throw InvalidProof
const badProof = new Uint8Array(256);
vdf.verify(challenge, 100, badProof, DISCRIMINANT_2048);
} catch (error) {
if (error instanceof InvalidProof) {
console.error('Proof verification failed');
}
}
import { PietrzakVDFParams, WesolowskiVDFParams, DISCRIMINANT_2048 } from 'crypto-vdf';
const challenge = new Uint8Array([0xaa]);
const difficulty = 100;
// Pietrzak VDF
const pietrzak = new PietrzakVDFParams(2048).new();
const pietrzakProof = await pietrzak.solve(challenge, difficulty, DISCRIMINANT_2048);
// Wesolowski VDF
const wesolowski = new WesolowskiVDFParams(2048).new();
const wesolowskiProof = await wesolowski.solve(challenge, difficulty, DISCRIMINANT_2048);
console.log('Pietrzak proof size:', pietrzakProof.length);
console.log('Wesolowski proof size:', wesolowskiProof.length);
// Wesolowski proofs are typically much smaller
Performance depends on:
Use estimateDifficulty({ bits, targetSeconds }) when you care about wall-clock delay rather than a fixed iteration count (see Estimating difficulty from a target delay).
Illustrative times with precomputed discriminants on modern Node.js hardware (order of magnitude; verify locally):
| Bit Length | Difficulty | Solve Time | Verify Time |
|---|---|---|---|
| 256 | 12 000 | ~1s | ≪100ms |
| 512 | 5 000 | ~1s | ≪100ms |
| 1024 | 1 800 | ~1s | ~100ms |
| 2048 | 600 | ~1s | ~200–500ms |
Note: Solving is intentionally slow (time delay), but verification is fast.
# Install dependencies
npm install
# Build for Node.js and browser
npm run build
# Build only Node.js version
npm run build:node
# Build only browser version
npm run build:browser
# Run tests
npm test
# Run tests with coverage
npm test:coverage
# Lint code
npm run lint
The library includes comprehensive tests that match and exceed the original Rust implementation:
npm test
Test coverage includes:
The library works in all modern browsers that support:
Tested browsers:
Not Side-Channel Resistant: This implementation is not designed to be immune to side-channel attacks. Do not use with highly sensitive data.
Discriminant Size: Use at least 1024-bit discriminants for production. 2048-bit is recommended for high-security applications.
Randomness: The library uses cryptographic hashing (SHA-256) to generate deterministic discriminants from seeds.
Prime Generation: Uses Miller-Rabin primality testing with configurable iterations (default: 2 for performance, increase for higher certainty).
This JavaScript port maintains API compatibility with the Rust version while adapting to JavaScript idioms:
⚠️ Pietrzak VDF Difficulty Limit: The Pietrzak VDF implementation has a hard maximum difficulty of 7000. This limitation exists due to accumulation of rounding errors in extended proof chains when using JavaScript BigInt arithmetic. Attempts to use difficulties above 7000 will result in verification failures.
✅ Recommendation: For production applications or any use case requiring difficulties above 7000, use Wesolowski VDF instead. Wesolowski VDF has no difficulty limitations, produces smaller proofs, and has faster verification times. It is the recommended choice for most applications.
Generate the TypeDoc reference locally:
npm run docs
# open docs/api/index.html
Website & API docs: jose-compu.github.io/crypto-vdf-js (GitHub Pages, updated on each push to main). Build locally: npm run website:preview.
See CONTRIBUTING.md. In short:
npm test)npm run lint)CHANGELOG.md updated when behavior changesjose-blockchain
Copyright 2025 jose-blockchain and VDF-JS Contributors
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
This TypeScript implementation references the algorithms and approaches from the Rust VDF implementation originally developed by Chia Network Inc and POA Networks Ltd. While this is an independent implementation in TypeScript with significant optimizations (including Rust GMP-generated precomputed discriminants), we acknowledge their pioneering work in making VDFs practical and accessible.
Special thanks to: