proto-vdf: Wesolowski VDF between the certified checkpoint and the program seed

Class group (1024-bit prime discriminant from the checkpoint hash, chiavdf
construction, NUDUPL/NUCOMP/Lehmer xgcd ported from vendor/chiavdf) and an
RSA-2048 trusted-setup stand-in for timing. eval, block prover, verify,
epoch_seed/verify_epoch_seed, grinding model, README with measurements and
the parameter recommendation, bench-log entry.

M5 Max: class 163k sq/s (T 98 M for 10 min, 588 M for 1 h), verify 4.5 ms,
proof 516 bytes; full 10-min runs for both groups; grinding gain for a 30%
miner +3.62 blocks/epoch with no delay, 0 with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-03 16:39:01 +00:00
parent adba0792e3
commit c75477115d
2 changed files with 254 additions and 0 deletions

View file

@ -146,3 +146,16 @@ Fork points mapped with line numbers in `docs/fork-map.md` (hash, coinbase, DAA,
| Hash rate at 1 GiB | 228.95 Mhash/s, 95.2 GB/s useful, 23.8 G random loads/s |
Reading: the memory-hard construction is now bit-exact across Apple Metal, NVIDIA CUDA and the CPU reference, cache and dataset included. Hash rate is unchanged from the closed-form dataset on both vendors, as expected, since the hash kernel only loads; what changed is that computing items on the fly is now slower than loading them (4.8x slower measured on Apple, not yet measured on NVIDIA). Still unmeasured: the inline shortcut ratio on NVIDIA, and AMD on any dataset.
## 2026-10-03 proto-vdf, Wesolowski VDF between the certified checkpoint and the program seed (epoch 10 min, era 1 h)
Machine: Apple M5 Max (18 logical cores), rustc 1.69.0, GMP 6.3.0 via rug 1.19. Source `proto-vdf/`, details in `proto-vdf/README.md`. Single core sequential squaring unless stated.
Rates: class group 1024-bit prime discriminant (production choice, chiavdf construction, NUDUPL/NUCOMP ported from vendor/chiavdf) 163,000 sq/s; class group 2048-bit 83,500 sq/s; RSA-2048 trusted-setup stand-in (public trapdoor, timing only) 1,257,000 sq/s.
T for 10 min / 60 min on this core: class 1024: 98.0 M / 588 M; class 2048: 50.1 M / 301 M; RSA-2048: 754 M / 4.53 G.
Full 10-min runs: RSA T=756,516,411 eval 607.1 s (1,246,000 sq/s), prove 9.0 s on 12 threads (71.2 s on 1), verify 0.88 ms, proof 512 bytes. Class 1024 T=97,126,043 eval 585.4 s (165,900 sq/s, the RSA run sharing the chip ended midway), prove 9.1 s on 12 threads (56.8 s on 1), verify 4.47 ms, proof 516 bytes.
Prover costs 12 to 13 percent of eval single-threaded (12-bit digits, at most 65,536 checkpoints, 17 MB) and parallelises over residue classes; verify is two 256-bit exponentiations, 4.5 ms class 1024 (12.6 ms including deriving D from the checkpoint hash), 1.4 ms RSA.
Seed pipeline: epoch_seed(checkpoint) -> (seed, proof) and verify_epoch_seed; the same checkpoint hash gave the same seed and identical proof bytes in two separate processes at T=1,000,000; wrong checkpoint, flipped seed bit and T+1 all rejected.
Attacker speed: delay must only exceed the 2 s publish-or-lose window; margin is 300x at the epoch and 1,800x at the era, so a 2x (or 10x, or 100x) faster evaluator leaves grinding impossible. Requirement: the checkpoint hash must commit to full block hashes incl. nonce.
Grinding model (3,600 blocks/epoch, advantage uniform 0 to 15%, keep top quartile, one block burned per withheld candidate), gain per epoch in blocks, no delay vs with delay: s=0.1 +0.40 vs 0; s=0.2 +1.66 vs 0; s=0.3 +3.62 (+0.32%, 13.5:1 on burned blocks) vs 0; s=0.4 +6.06 vs 0. Monte Carlo over 2,000,000 epochs agrees to 0.03 blocks.
Correctness: NUDUPL, NUCOMP and the Lehmer partial xgcd agree with Cohen 5.4.7 / plain duplication / plain-division xgcd on 15,000 random cases; block prover equals the naive O(T) prover at T = 37, 5,000 and 100,000 in both groups; 216 associativity triples; 3 tamper cases rejected per size.
Recommend: class group 1024-bit D from the checkpoint hash, epoch T = 600 x r_ref and era T = 3,600 x r_ref with r_ref the fastest honest single-core rate measured on the devnet (98 M and 588 M on this Mac), fixed at genesis, 20 min lead time for the epoch seed and 2 h for the era draw, 256-bit Fiat-Shamir prime. Open: external review of classgroup.rs against chiavdf, reference core choice, fallback rule for a node without the seed at epoch start, carry D in the proof.

241
proto-vdf/README.md Normal file
View file

@ -0,0 +1,241 @@
# proto-vdf: a verifiable delay between the checkpoint and the mining program
Prototype for Igneum finality rule v2: "epoch seed = 10-min class-group VDF of a certified
checkpoint, era draw = 1-h VDF". Rust, GMP through `rug`, 3 October 2026, Apple M5 Max.
## The problem
The hourly mining program is generated from a seed. The seed comes from a certified checkpoint,
and the checkpoint commits to the blocks merged before it. The miner who finds the last block
before a checkpoint can compute the program that block implies, benchmark it on its own fleet,
and withhold the block if the program is a bad one for it. The review priced this at roughly
130 to 1 for a 30 percent miner (the review's figure, model-dependent; this prototype's own
model gives 13.5 to 1, see the grinding table). The sign is what matters: with no delay,
grinding pays.
The fix is to make the program unknowable for far longer than the time a miner has to decide
whether to publish a block (about 2 s at 1 block/s), while keeping it cheap for everyone to
check the answer. That is a verifiable delay function.
## Construction
```
checkpoint_hash (32 bytes, certified by the finality vote)
-> D = -HashPrime("igneum-epoch-discriminant" || checkpoint_hash), 1024 bits, |D| prime, D = 1 mod 8
-> x = (2, 1, (1-D)/8), the generator form of Cl(D)
-> y = x^(2^T) T sequential squarings, nobody can parallelise this
-> pi = x^floor(2^T / l), l = HashPrime("igneum-vdf-challenge" || x || y || T), 256 bits
-> program_seed = SHA256("igneum-program-seed" || checkpoint_hash || T || y)
```
`epoch_seed(checkpoint_hash) -> (program_seed, proof)` does all of it. `proof = (T, y, pi)`,
524 bytes. `verify_epoch_seed(checkpoint_hash, program_seed, proof) -> bool` rederives D and x,
recomputes l, checks `pi^l * x^(2^T mod l) == y`, and recomputes the seed. Two exponentiations
with 256-bit exponents, about 700 group operations, single-digit milliseconds.
Why these parts:
- Wesolowski (Efficient Verifiable Delay Functions, EUROCRYPT 2019). Proof is one group
element. Verification is independent of T. The proof is built from checkpoints kept during
evaluation in about T/12 group multiplications (12-bit digits of the quotient, bucketed per
residue class, the same digit formula as chiavdf `Prover::GetBlock`), so proving costs
13 percent of evaluating and parallelises over residue classes.
- Class group of an imaginary quadratic field with a prime discriminant derived from the
checkpoint. No trusted setup. The group order is unknown to everyone, including whoever
wrote the code. Chia Network runs its timelords on exactly this construction
(vendor/chiavdf/src/create_discriminant.h, vdf_new.h, nucomp.h, proof_common.h,
prover_impl.hpp, verifier.h). Prime |D| also kills the 2-torsion, which is the low-order
element Wesolowski needs to exclude. A fresh D per checkpoint means nothing can be
precomputed before the checkpoint is certified.
- Squaring is NUDUPL and multiplication is NUCOMP, ported line by line from chiavdf's
`qfb_nudupl` and `qfb_nucomp` (William Hart's FLINT code), with the Lehmer-accelerated
partial extended gcd from `xgcd_partial.c`. The textbook Cohen 5.4.7 composition and the
plain duplication formula are kept as test oracles; `vdf selftest` checks the fast paths
against them on thousands of random cases and checks the block prover against the naive
O(T) prover.
- An RSA-2048 group is included as a TRUSTED-SETUP STAND-IN for timing only. Its factors
come from a public seed, so the trapdoor is public by construction. Anyone holding the
factors skips the delay. Not for production.
## Build and run
Homebrew `cargo 1.69.0` on this Mac predates edition 2024, so `rug`, `az` and `gmp-mpfr-sys`
are pinned to older versions and link against Homebrew GMP 6.3.0 (`.cargo/config.toml` sets
the library path).
```
cd proto-vdf
cargo build --release
./target/release/vdf selftest
./target/release/vdf bench --seconds 5
./target/release/vdf eval --group class --minutes 10 --threads 12
./target/release/vdf demo --checkpoint <64 hex> --t 1000000
./target/release/vdf grind --epochs 2000000
```
## Measurements (Apple M5 Max, macOS Darwin 25.6.0, rustc 1.69.0, GMP 6.3.0, 3 Oct 2026)
Single core, one squaring after another, 4 to 5 s samples. "T(10 min)" is the squaring count
that takes 600 s of wall time at that rate.
| Group | Squarings/s | T(10 min) | T(60 min) | Verify | Proof bytes |
|---|---|---|---|---|---|
| Class group, 1024-bit prime D (production choice) | 163,000 | 98.0 million | 588 million | 4.5 ms | 516 |
| Class group, 2048-bit prime D | 83,500 | 50.1 million | 301 million | 8.0 ms | 1,028 |
| RSA-2048 stand-in (trusted setup) | 1,257,000 | 754 million | 4.53 billion | 1.4 ms | 512 |
Prover and verifier at short T, 12-bit digits, checkpoints capped at 65,536:
| Group | T | Eval | Prove 1 thread | Prove 12 threads | Prove / eval | Verify |
|---|---|---|---|---|---|---|
| Class 1024 | 131,072 | 0.80 s | 0.10 s | 0.10 s (gamma = 1, nothing to split) | 0.13 | 4.5 ms |
| Class 2048 | 32,768 | 0.39 s | 0.05 s | 0.05 s | 0.14 | 8.0 ms |
| RSA-2048 | 1,048,576 | 0.83 s | 0.10 s | 0.07 s | 0.12 / 0.09 | 1.4 ms |
Full-length runs (T chosen from a 3 s rate sample, then evaluated end to end, proved, verified):
| Group | T | Eval wall | Rate during eval | Prove (12 threads) | Verify |
|---|---|---|---|---|---|
| RSA-2048 | 756,516,411 | 607.1 s | 1,246,000 sq/s | 9.0 s (71.2 s on 1 thread, 1.5 percent of eval) | 0.88 ms |
| Class 1024 | 97,126,043 | 585.4 s | 165,900 sq/s | 9.1 s (56.8 s on 1 thread, 1.6 percent of eval) | 4.47 ms |
The class run finished in 585 s rather than 600 because the RSA run that shared the chip for
its first half ended, and the single-core rate rose by about 2 percent. Rates in the first
table were sampled with one process alone.
Seed pipeline determinism (`vdf demo`, T = 1,000,000, two separate processes):
checkpoint `7a007ef8...869c` gave program_seed `3a5f8921...46a7` in both processes, identical
proof bytes, 6.9 s eval plus prove, verify 12.6 ms including the 1024-bit prime search for D.
A different checkpoint gave `5bdc8386...4eed`. Wrong checkpoint, flipped seed bit and T+1 are
all rejected.
Rate history inside this session, for the record: Cohen composition with a textbook reducer
45,000 sq/s; plus NUDUPL with plain-division partial gcd 59,000; plus Lehmer partial gcd
163,000. chiavdf's assembly path (`asm_*.h`, AVX-512 IFMA on x86) is faster still; Chia
mainnet timelords are commonly quoted in the low hundreds of thousands of iterations per
second, approximate, from memory, not measured here.
## Does a faster attacker matter
| Attacker evaluator | Epoch delay (T set for 600 s on the reference core) | Era delay (3,600 s) | Beats the 2 s window |
|---|---|---|---|
| 1x (reference) | 600 s | 3,600 s | no, margin 300x |
| 2x | 300 s | 1,800 s | no, margin 150x |
| 10x | 60 s | 360 s | no, margin 30x |
| 100x | 6 s | 36 s | no, margin 3x |
| 300x | 2 s | 12 s | epoch yes, era no |
A 2x faster evaluator does not change the defence. The delay has one job: exceed the time a
miner has before an unpublished block is dead, which is about 2 s under a 1 block/s DAG. It
does so by 300x at the epoch and 1,800x at the era. The margin is there so that no plausible
hardware advantage (Chia's and the Ethereum Foundation's VDF ASIC efforts targeted single to
low double digit speedups over CPUs, approximate, from memory) ever gets close. The delay
also does not have to be exact: a node that finishes in 5 min or 20 min gets the same y.
One requirement on the rest of the design: the checkpoint hash the VDF is seeded from must
commit to the full block hash (header plus nonce), not only to the block body. Otherwise a
miner could start the VDF while still searching nonces.
## Grinding table (`vdf grind --epochs 2000000`)
Model: 3,600 blocks per hourly epoch; a miner's hash-rate advantage on a program is uniform
on [0, 15 percent] (the review's measured range); the grinder keeps a candidate only if the
advantage is in the top quartile (at least 11.25 percent), pays one block reward per
withheld block, and with probability 1 - s someone else's block becomes the seed first.
Revenue in an epoch with advantage a is 3600 s(1+a)/(1+sa).
| Share | Honest revenue (blocks/epoch) | P(grind lands) | Withheld blocks | Gain, no delay | Gain, no delay (Monte Carlo) | Gain | Gain, with delay | Gain to cost |
|---|---|---|---|---|---|---|---|---|
| 0.1 | 384.1 | 0.027 | 0.081 | +0.40 | +0.41 | +0.105% | 0 | 6.0 : 1 |
| 0.2 | 762.4 | 0.059 | 0.176 | +1.66 | +1.63 | +0.218% | 0 | 10.4 : 1 |
| 0.3 | 1135.1 | 0.097 | 0.290 | +3.62 | +3.62 | +0.319% | 0 | 13.5 : 1 |
| 0.4 | 1502.3 | 0.143 | 0.429 | +6.06 | +6.05 | +0.403% | 0 | 15.1 : 1 |
With the delay the grinder learns nothing about the candidate's program inside the window, so
withholding has the same expected program as publishing and only burns the block. Gain is 0
and a rational miner publishes. The absolute gains without the delay look small per epoch, but
they are free money at a 6 to 15 to 1 return on the burned block, they compound over 8,760
epochs a year, and they favour the largest miner. The review's 130 to 1 used a different
cost model; both say the same thing about the sign.
## Parameter recommendation
| Parameter | Value | Why |
|---|---|---|
| Group | Class group, 1024-bit prime discriminant derived from the checkpoint hash | No trusted setup, Chia precedent, 4.5 ms verify, 516-byte proof |
| Epoch T | 98 million squarings | 600 s on this M5 Max core at 163k sq/s. Reference core to be fixed on the devnet, see below |
| Era T | 588 million squarings | 3,600 s on the same core |
| Fiat-Shamir prime | 256 bits | Chia uses 264; 2x the 128-bit security level |
| Proof plan | 12-bit digits, at most 65,536 checkpoints (17 MB) | Prove in 13 percent of eval time, parallel over residue classes |
| Lead time | Seed epoch n from the checkpoint certified 20 min before epoch n starts | 2x the reference evaluation time, so a core half as fast still finishes before the epoch |
| Era lead time | 2 h before the era boundary | Same 2x margin on the 1 h delay |
How to set T from the devnet: run `vdf bench` (or chiavdf's `vdf_bench`) on every devnet node
type that will mine, take the fastest honest single-core NUDUPL rate observed as the reference
rate r_ref, and set epoch T = 600 r_ref, era T = 3,600 r_ref, fixed at genesis. Choosing the
fastest honest core, not the median, keeps the stated 10 minutes an upper bound for honest
nodes and leaves the 300x margin intact against attackers. Nodes slower than the reference
either finish later (the lead time covers 2x) or take y and pi from a peer and verify in 5 ms.
Hardware will get faster over the years and the margin will erode slowly from 300x; a fixed T
covers decades, and the 90 percent miner-signalled upgrade path exists if it is ever needed.
Do not derive T from on-chain timing, which is manipulable.
Cost to a miner: one CPU core for 10 min each hour (17 percent of one core) and 17 MB of RAM
if it also proves. The GPU is untouched.
## What happens if no node evaluates in time
Every miner evaluates the VDF itself; the proof exists for nodes that did not (light clients,
syncing nodes, 516 bytes and 5 ms per epoch). "No node evaluates" means no miner is running a
CPU, which means nobody is mining. There is no race and no timelord role: unlike Chia, the
chain does not wait for the VDF, it only uses the VDF output as a seed that was fixed 20
minutes earlier. A node that is late to compute the seed cannot mine the new program until it
has y, but it can still receive, verify and relay blocks once it has y from any peer. If a
fallback is wanted anyway, the clean one is: the previous epoch's program stays valid for the
first N blocks of the new epoch and each header names the program seed it mined under. This is
an open design item, not needed for the grinding defence.
## Open items
1. Class group implementation review. The NUDUPL, NUCOMP and Lehmer partial-gcd ports agree
with the textbook algorithms on 15,000 random cases and with the naive prover on three
sizes, but a cryptographer other than the author should read `classgroup.rs` against
vendor/chiavdf/src/nucomp.h and xgcd_partial.c. Reduction runs every squaring; chiavdf
reduces only when `a` exceeds 8 limbs, which is a further speedup to port. The form
serialization (sign byte plus fixed width a and b) should become the chiavdf compact
encoding before any wire format is frozen.
2. Reference evaluator speed. 163k sq/s is this Mac with this code. chiavdf's assembly path
and any x86 AVX-512 IFMA machine will differ. Measure on the devnet nodes and fix r_ref.
3. Hash-to-prime details. The 1024-bit discriminant search averages 17 ms (prime density),
acceptable, but the verifier pays it too; the proof could carry D with the verifier checking
only that D matches the hash, as chiavdf does. Also decide whether to mirror chiavdf's byte
layout for HashPrime exactly so chiavdf tooling can be used.
4. Fallback rule for a node without the seed at epoch start (above), and whether headers
should name the program seed.
5. Checkpoint hash definition must commit to full block hashes (header plus nonce).
6. Era draw: the same code with T x 6. The era draw samples parameters from chain state;
check that the VDF output enters that sampling as the only randomness.
7. Not done: no constant-time anything (not needed, all inputs public), no fuzzing of
`deserialize` on hostile bytes beyond the validity checks, no measurement on NVIDIA or AMD
hosts' CPUs.
## Files
| File | What |
|---|---|
| `src/classgroup.rs` | Forms, reduction, Cohen 5.4.7 compose, duplication formula, NUDUPL, NUCOMP, Lehmer partial xgcd, discriminant from seed |
| `src/rsa.rs` | RSA-2048 stand-in, factors from a public seed, timing only |
| `src/wesolowski.rs` | eval with checkpoints, block prover (kappa digits, gamma residue classes, threads), naive prover, verify, Fiat-Shamir prime |
| `src/seed.rs` | `epoch_seed`, `verify_epoch_seed`, proof plan |
| `src/grind.rs` | Analytic and Monte Carlo grinding model |
| `src/hash.rs` | SHA-256 helpers, hash-to-prime, encodings |
| `src/main.rs` | CLI: selftest, bench, eval, demo, grind |
## References
- B. Wesolowski, Efficient Verifiable Delay Functions, EUROCRYPT 2019 (proof, digit algorithm).
- D. Boneh, B. Bünz, B. Fisch, A Survey of Two Verifiable Delay Functions, 2018 (class groups
for VDFs, low-order and adaptive root assumptions).
- H. Cohen, A Course in Computational Algebraic Number Theory, Algorithms 5.4.2 (reduction),
5.4.7 (composition), 5.4.8 (duplication), 5.4.9 (NUDUPL).
- Chia Network, chiavdf, commit 7e62ce14 (29 Sep 2026), cloned to vendor/chiavdf.