Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ dependencies = [
"chia-sha2 0.46.0",
"chia-ssl",
"chia-traits 0.46.0",
"chia-vdf-verify",
"clvm-traits",
"clvm-utils",
"clvmr",
Expand Down Expand Up @@ -659,6 +660,19 @@ dependencies = [
"thiserror 2.0.18",
]

[[package]]
name = "chia-vdf-verify"
version = "0.46.0"
dependencies = [
"criterion",
"hex",
"malachite-base",
"malachite-nz",
"serde",
"serde_json",
"sha2 0.10.9",
]

[[package]]
name = "chia_py_streamable_macro"
version = "0.46.0"
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ chia-serde = { workspace = true, optional = true }
chia-traits = { workspace = true, optional = true }
chia-puzzle-types = { workspace = true, optional = true }
chia-sha2 = { workspace = true, optional = true }
chia-vdf-verify = { workspace = true, optional = true }
clvm-traits = { workspace = true, optional = true }
clvm-utils = { workspace = true, optional = true }
clvmr = { workspace = true }
Expand All @@ -90,6 +91,7 @@ default = [
"traits",
"puzzle-types",
"sha2",
"vdf-verify",
"clvm-traits",
"clvm-utils"
]
Expand All @@ -105,6 +107,7 @@ serde = ["dep:chia-serde", "chia-protocol/serde", "chia-bls/serde"]
traits = ["dep:chia-traits"]
puzzle-types = ["dep:chia-puzzle-types"]
sha2 = ["dep:chia-sha2"]
vdf-verify = ["dep:chia-vdf-verify"]
clvm-traits = ["dep:clvm-traits"]
clvm-utils = ["dep:clvm-utils"]

Expand All @@ -131,6 +134,7 @@ chia-traits = { path = "./crates/chia-traits", version = "0.46.0" }
chia-puzzle-types = { path = "./crates/chia-puzzle-types", version = "0.46.0" }
chia-sha2 = { path = "./crates/chia-sha2", version = "0.46.0" }
chia-serde = { path = "./crates/chia-serde", version = "0.46.0" }
chia-vdf-verify = { path = "./crates/chia-vdf-verify", version = "0.46.0" }
clvm-traits = { path = "./crates/clvm-traits", version = "0.46.0" }
clvm-utils = { path = "./crates/clvm-utils", version = "0.46.0" }
clvm-derive = { path = "./crates/clvm-derive", version = "0.46.0" }
Expand Down Expand Up @@ -199,3 +203,5 @@ tempfile = "3.19.1"
bitvec = "1.0.1"
indexmap = "2.10.0"
serde_arrays = "0.2.0"
malachite-nz = "0.9"
malachite-base = "0.9"
74 changes: 74 additions & 0 deletions crates/chia-vdf-verify/BENCHMARKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Benchmark Summary: Rust chia-vdf-verify vs C++ chiavdf

## Result

**C++ was 1.75x faster → now ~1.14x faster.** The Rust VDF verifier closes
most of the gap with C++ chiavdf (GMP-backed) through pure Rust optimizations
using crates.io malachite-nz, with no C dependencies.

## Baseline (before optimization)

| | C++ (chiavdf + GMP) | Rust (chia-vdf-verify + malachite-nz) | Ratio |
| ------------------------ | ------------------- | ------------------------------------- | -------------------- |
| Single-threaded ms/proof | 8.67 ms | 15.19 ms | **C++ 1.75x faster** |

## After optimization

### Real-world: 100 mainnet reward-chain IP proofs, single-threaded

| | C++ (ms/proof) | Rust (ms/proof) | Ratio |
| -------------------- | -------------- | --------------- | -------------------- |
| Single-threaded | 42.85 | 48.97 | **C++ 1.14x faster** |
| Multi-threaded (32T) | 441 proofs/s | 438 proofs/s | ~parity |

100 proofs, 14 witness types (0–19), iterations 35K–78M (median ~10.6M).
Built with generic x86-64 target (no `target-cpu=native`), matching what
ships in wheels. Rust's better GIL scaling nearly closes the gap under
concurrent load.

### Core operation: nudupl + reduce (1024-bit discriminant)

| Version | Time per op | Speedup |
| ------------------------ | ----------- | --------------- |
| Before (main branch) | 51 µs | — |
| After (malachite branch) | 19 µs | **2.7x faster** |

## Optimizations applied

| # | Optimization | Impact |
| --- | --------------------------------------------------------- | ---------------------- |
| 1 | Port from num-bigint to malachite-nz | foundation |
| 2 | Fix PyO3 bindings, release GIL | correctness + parallel |
| 3 | Discriminant bytes API (avoid repeated decimal parse) | small |
| 4 | Extract limb words without allocation in Lehmer loop | ~5% |
| 5 | Eliminate clones, use in-place negation (`NegAssign`) | ~15% |
| 6 | O(n) byte-to-integer via direct limb construction | ~30% on decompression |
| 7 | Fused multiply-accumulate (`AddMulAssign`/`SubMulAssign`) | ~10% |
| 8 | Owned-argument GCD variants, avoid double-clones | small |
| 9 | Compiler: LTO=fat, codegen-units=1 | ~5% |
| 10 | Optimize `fdiv_r`, BQFC decompression, refactor nudupl | small |
| 11 | GCD argument swap: return native Bézout cofactor | small |

## What's left

The remaining ~14% single-threaded gap comes from GMP vs malachite-nz fundamentals:

- **GMP reuses pre-allocated buffers** via thread-local scratch (`mpz_t`);
malachite allocates fresh `Vec`s per operation.
- **GMP has hand-tuned x86-64 assembly** for core limb operations
(`mpn_addmul_1`, `mpn_mul_basecase`); malachite uses LLVM codegen.
- **GMP's `extended_gcd` skips unused cofactors**; malachite always derives
both Bézout coefficients (the second via a full multiply + divide).

A malachite fork adding `extended_gcd_first_cofactor` (skip second-cofactor
derivation) and `Integer × i64` (avoid wrapper allocation) would likely
close the remaining gap, bringing Rust to parity with C++.

## Environment

- 1024-bit class group discriminants, 35K–78M iterations per proof
- Rust stable, release profile with LTO (generic x86-64 target)
- malachite-nz 0.9.1 (crates.io, no fork)
- GMP: system package (used by chiavdf)
- `.cargo/config.toml` with `target-cpu=native` removed — benchmarks
reflect what ships in pip wheels
39 changes: 39 additions & 0 deletions crates/chia-vdf-verify/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "chia-vdf-verify"
version.workspace = true
edition = "2024"
license = "Apache-2.0"
description = "Pure-Rust Chia VDF proof verifier — no GMP, no C dependencies"
authors = ["Richard Kiss <him@richardkiss.com>"]
homepage = "https://github.com/Chia-Network/chia_rs"
repository = "https://github.com/Chia-Network/chia_rs"
readme = "README.md"
keywords = ["chia", "vdf", "verifiable-delay-function", "wesolowski"]
categories = ["cryptography"]

[lints]
workspace = true

[lib]
name = "chia_vdf_verify"
crate-type = ["rlib"]
bench = false

[dependencies]
malachite-nz = { workspace = true }
malachite-base = { workspace = true }
sha2 = { workspace = true }

[dev-dependencies]
hex = { workspace = true }
criterion = { workspace = true, features = ["html_reports"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

[[bench]]
name = "verify"
harness = false

[[bench]]
name = "mainnet_proofs"
harness = false
102 changes: 102 additions & 0 deletions crates/chia-vdf-verify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# chia-vdf-verify

Pure-Rust Chia VDF (Verifiable Delay Function) proof verifier. No GMP, no C dependencies, no unsafe code.

Imported from [richardkiss/chia-vdf-verify](https://github.com/richardkiss/chia-vdf-verify). This is a port of the verification path from [chiavdf](https://github.com/Chia-Network/chiavdf) (C++/GMP). Only proof **verification** is implemented — proof creation (proving) is not included. This crate is Rust-only (no C FFI or Python bindings).

## Why?

The existing `chiavdf` library depends on GMP (GNU Multiple Precision Arithmetic Library) via C/C++ linking, which is painful to build cross-platform — especially on Windows. This crate replaces GMP with [num-bigint](https://crates.io/crates/num-bigint) for a fully portable pure-Rust implementation.

## Performance

Rust is consistently ~12% slower than C++/GMP across all proof depths, measured on real mainnet proofs:

| Depth | chiavdf C++ | chia-vdf-verify (Rust) | Ratio |
| ----- | ----------- | ---------------------- | ----- |
| 0 | ~4.3 ms | ~4.8 ms | 1.11x |
| 1 | ~9.5 ms | ~10.7 ms | 1.13x |
| 2 | ~15.2 ms | ~17.1 ms | 1.12x |
| 3 | ~20.4 ms | ~22.9 ms | 1.12x |
| 4 | ~25.8 ms | ~28.9 ms | 1.12x |
| 5 | ~31.2 ms | ~34.4 ms | 1.10x |

Benchmarked against 100 real mainnet proofs. Chia mainnet uses 1024-bit discriminants, depth 0–2 typical. The ~12% overhead is acceptable for consensus validation (one proof per block).

## How VDF verification works

A [Verifiable Delay Function](https://en.wikipedia.org/wiki/Verifiable_delay_function) requires T sequential squarings to compute but is fast to verify. Chia uses the [Wesolowski scheme](https://eprint.iacr.org/2018/623) operating in [class groups](https://en.wikipedia.org/wiki/Ideal_class_group) of imaginary quadratic fields.

**Key concepts:**

- **Discriminant (D):** A large negative prime (1024 bits on mainnet) that defines the class group. Generated deterministically from a challenge hash via `CreateDiscriminant`.

- **Forms:** Elements of the class group, represented as binary quadratic forms (a, b, c) where b² − 4ac = D. These form a group under [composition](https://en.wikipedia.org/wiki/Binary_quadratic_form#Composition) (NUCOMP). The identity element is (1, 1, (1−D)/4).

- **The VDF computation:** Starting from the identity form x, compute y = x^(2^T) — i.e., square the form T times. This is inherently sequential.

- **The proof:** The prover also produces a proof form π. Verification checks:

\[ \pi^B \cdot x^r = y \]

where B = HashPrime(x ‖ y) is a 264-bit prime derived from the input/output, and r = 2^T mod B. This requires only a few group exponentiations — much faster than the T squarings.

- **Depth (n-Wesolowski):** A proof can be split into n segments, each with its own sub-proof. Depth 0 = single proof; higher depths break the proof into pieces with intermediate checkpoints. More segments means a larger proof blob but allows parallelized proving. Verification checks each segment in sequence.

## Usage

```rust
use chia_vdf_verify::discriminant::create_discriminant;
use chia_vdf_verify::verifier::check_proof_of_time_n_wesolowski;

let d = create_discriminant(seed, 1024);
let valid = check_proof_of_time_n_wesolowski(&d, &input_form, &proof_blob, iterations, depth);
```

## Testing

Run the standard test suite (fast, ~2 seconds):

```bash
cargo test
```

### Stress tests

The crate includes 110 test vectors extracted from chiavdf's `vdf.txt` — real VDF proofs with 1024-bit discriminants at depths 0 through 7. Two vectors run by default as a smoke test. To run all 110:

```bash
cargo test --release -- --ignored test_vdf_txt_all
```

This takes ~15 seconds in release mode.

### Benchmarks

Criterion benches:

```bash
cargo bench --bench verify # fixture vectors (512-bit + vdf.txt depths)
cargo bench --bench mainnet_proofs # 100 real mainnet reward-chain IP proofs
```

## Architecture

Ported from chiavdf's C++ verification path (~2,700 LOC):

| Module | Source | Purpose |
| -------------- | ----------------------- | ------------------------------------------------------ |
| `verifier` | `verifier.h` | `VerifyWesolowskiProof`, `CheckProofOfTimeNWesolowski` |
| `proof_common` | `proof_common.h` | `FastPow`, `FastPowFormNucomp`, `GetB`, serialization |
| `nucomp` | `nucomp.h` | Class group form composition (`nucomp`, `nudupl`) |
| `reducer` | `Reducer.h` | Pulmark form reduction |
| `xgcd_partial` | `xgcd_partial.c` | Partial extended GCD (Lehmer-accelerated) |
| `bqfc` | `bqfc.c` | Compressed form serialization (BQFC format) |
| `primetest` | `primetest.h` | BPSW primality test, `HashPrime` |
| `discriminant` | `create_discriminant.h` | Discriminant generation from seed |
| `form` | `ClassGroup.h` | Quadratic form (a, b, c) with discriminant |
| `integer` | `integer_common.h` | BigInt wrapper, Lehmer extended GCD |

## License

[Apache License 2.0](LICENSE)
95 changes: 95 additions & 0 deletions crates/chia-vdf-verify/benches/extract_proofs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Extract VDF proofs that validate with the identity element from a Chia blockchain DB.

Extracts reward-chain infusion-point VDFs, which use the identity element as
input and are self-contained (no block-context needed to verify).

Usage:
python benches/extract_proofs.py [--db PATH] [--count N] [--output PATH]

Requires: chia_rs, zstd, chiavdf (pip install chia-rs zstd chiavdf)
"""
from __future__ import annotations

import argparse
import json
import os
import sqlite3
import sys
from pathlib import Path

import chiavdf
import zstd
import chia_rs

DISC_BITS = 1024
IDENTITY = bytes([0x08]) + bytes(99)


def extract(db_path: str, n: int) -> list[dict]:
conn = sqlite3.connect(db_path)
rows = conn.execute(
"SELECT block FROM full_blocks ORDER BY height DESC LIMIT ?",
(n * 100,),
).fetchall()

proofs: list[dict] = []
for (raw,) in rows:
block = chia_rs.FullBlock.from_bytes(zstd.decompress(raw))
rcb = block.reward_chain_block

rc_ip_vdf = rcb.reward_chain_ip_vdf
rc_ip_proof = block.reward_chain_ip_proof
blob = rc_ip_vdf.output.data + bytes(rc_ip_proof.witness)
challenge = bytes(rc_ip_vdf.challenge)
disc_str = str(int(chiavdf.create_discriminant(challenge, DISC_BITS), 16))

if chiavdf.verify_n_wesolowski(
disc_str, IDENTITY, blob,
rc_ip_vdf.number_of_iterations, DISC_BITS, rc_ip_proof.witness_type,
):
proofs.append({
"challenge": challenge.hex(),
"input_el": IDENTITY.hex(),
"proof_blob": blob.hex(),
"iters": rc_ip_vdf.number_of_iterations,
"witness_type": rc_ip_proof.witness_type,
"height": block.height,
"vdf_type": "rc_ip",
})

if len(proofs) >= n:
break

conn.close()
return proofs[:n]


def main() -> None:
default_db = os.path.expanduser("~/.chia/mainnet/db/blockchain_v2_mainnet.sqlite")
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--db", default=default_db, help="blockchain SQLite DB path")
parser.add_argument("--count", type=int, default=20, help="number of proofs to extract")
parser.add_argument("--output", default="benches/proofs.json", help="output JSON path")
args = parser.parse_args()

if not Path(args.db).exists():
sys.exit(f"DB not found: {args.db}")

proofs = extract(args.db, args.count)
if not proofs:
sys.exit("No validating VDF proofs found")

with open(args.output, "w") as f:
json.dump(proofs, f, indent=2)

heights = sorted(p["height"] for p in proofs)
wtypes = sorted(set(p["witness_type"] for p in proofs))
print(f"Extracted {len(proofs)} validating proofs (heights {heights[0]:,}–{heights[-1]:,}, "
f"witness types {wtypes})")
print(f"Wrote {args.output} ({os.path.getsize(args.output):,} bytes)")


if __name__ == "__main__":
main()
Loading