Quickstart
# Fibonacci through SWIRL: prove it, then check the proof.
#
# openvm-zorch is OpenVM's prover rebuilt on zorch blocks. prove() composes
# SWIRL's five stages over ONE Fiat-Shamir transcript -- a stacked commitment,
# LogUp-GKR over any interaction buses, a batched ZeroCheck with univariate
# skip, a stacked opening reduction, and a WHIR opening -- and verify() is its
# stage-for-stage dual. Both run below, on the AIR this file builds.
#
# A prover's input here is a TRACE, not an ELF. Compiling a guest program and
# executing it into columns is the openvm SDK's job; openvm-zorch starts where
# the trace does, which is where all five stages and every GPU win live.
#
# So the guest is the AIR itself -- openvm's Fibonacci. Two columns (a, b),
# row i = (F_i, F_{i+1}); the public values are the seed (a0, b0) and the
# claimed F_n. Change the seed A0, B0 or the height LOG_HEIGHT below and re-run
# -- it proves whatever sequence you pick. The default (0, 1) over 64 rows is
# byte-identical to openvm-stark-backend v2.0.0's fixture, whose constraints
# and trace this repo byte-matches.
import frx.numpy as fnp
from zk_dtypes import babybear_mont as F
from openvm_zorch.logup_zerocheck.constraints import ConstraintsDag
from openvm_zorch.poly_common import VerificationError
from openvm_zorch.poseidon2.babybear16 import babybear16_hasher
from openvm_zorch.prove import prove
from openvm_zorch.transcript import new_transcript
from openvm_zorch.types import AirInstance, AirVk, SystemParams
from openvm_zorch.verify import verify
from openvm_zorch.whir.prover import WhirConfig
A0, B0 = 0, 1 # the seed row (F_0, F_1)
LOG_HEIGHT = 6 # 2^6 = 64 rows, so the claim is F_64
def fibonacci_trace(a0: int, b0: int, log_height: int):
"""The witness: row i = (F_i, F_{i+1}) from the seed (a0, b0), plus the
final F_n as a field element. The columns are BabyBear, so each add reduces
mod the field on its own -- no explicit modulus."""
rows = [(fnp.full((), a0, F), fnp.full((), b0, F))]
for _ in range((1 << log_height) - 1):
a_i, b_i = rows[-1]
rows.append((b_i, a_i + b_i))
return fnp.array(rows, dtype=F), rows[-1][1]
trace, f_n = fibonacci_trace(A0, B0, LOG_HEIGHT)
public_values = (A0, B0, int(f_n)) # a0, b0, the claimed F_n
# The AIR, as the symbolic node DAG keygen hands the prover: nodes in
# topological order, and the indices of the nodes asserted to be zero.
nodes: list[dict] = []
def node(**kw) -> int:
nodes.append(kw)
return len(nodes) - 1
def main(index: int, offset: int) -> int:
"""Column `index` of this row (offset 0) or the next one (offset 1)."""
return node(kind="variable", entry="main", part_index=0, index=index, offset=offset)
def public(index: int) -> int:
return node(kind="variable", entry="public", part_index=None, index=index, offset=None)
def add(left: int, right: int) -> int:
return node(kind="add", left=left, right=right)
def sub(left: int, right: int) -> int:
return node(kind="sub", left=left, right=right)
def mul(left: int, right: int) -> int:
return node(kind="mul", left=left, right=right)
first, trans, last = (
node(kind="is_first_row"),
node(kind="is_transition"),
node(kind="is_last_row"),
)
a, b = main(0, 0), main(1, 0)
a_next, b_next = main(0, 1), main(1, 1)
a0, b0, claimed = public(0), public(1), public(2)
# Each row asserts `selector * expr == 0`, which is what pins the trace to the
# public values at both ends and to the recurrence in between.
constraint_idx = (
mul(first, sub(a, a0)), # the trace starts at the given input,
mul(first, sub(b, b0)), # both columns
mul(trans, sub(b, a_next)), # a' = b
mul(trans, sub(add(a, b), b_next)), # b' = a + b
mul(last, sub(b, claimed)), # and it ends at the claimed F_n
)
# Fibonacci is a pure constraint AIR: no LogUp interactions, no bus. With
# nothing to fold, the LogUp-GKR stage folds an empty input layer -- exactly
# what the reference does for any AIR that declares no interactions -- so the
# proof's weight rides the ZeroCheck over the five constraints above and the
# WHIR opening that pins them to the committed trace.
dag = ConstraintsDag(
nodes=tuple(nodes), constraint_idx=constraint_idx, interactions=()
)
# Production-shaped params, as the reference fixture pins them.
params = SystemParams(
l_skip=4,
n_stack=8,
log_blowup=1,
logup_pow_bits=2,
max_constraint_degree=3,
whir=WhirConfig(
k=4,
num_queries=[10, 3, 2],
mu_pow_bits=3,
folding_pow_bits=2,
query_phase_pow_bits=1,
),
)
sponge, comp = babybear16_hasher()
vk_pre_hash = (0,) * 8
_, proof = prove(
new_transcript(),
sponge,
comp,
params,
vk_pre_hash,
[
AirInstance(
trace=trace,
dag=dag,
public_values=public_values,
constraint_degree=2,
needs_next=True,
is_required=True,
)
],
)
def accepts(claim: tuple[int, int, int]) -> bool:
"""Verify against a claim, from the AIR's shape alone -- no trace. The
public values are absorbed into the transcript before anything is sampled,
so a changed claim re-derives different challenges and the proof stops
answering the questions it was built for."""
vk = AirVk(
dag=dag,
log_height=LOG_HEIGHT,
width=2,
public_values=claim,
constraint_degree=2,
needs_next=True,
is_required=True,
)
try:
verify(
new_transcript(),
sponge,
comp,
params,
vk_pre_hash,
[vk],
proof.common_main_commit,
proof,
)
return True
except VerificationError:
return False
# A wrong claim: F_n + 1 in the field, so it stays a valid element even when
# F_n is P-1 (a plain int F_n + 1 would be P, which has no field element and
# fails to construct).
lied_about = public_values[:2] + (int(f_n + fnp.ones((), F)),)
n = 1 << LOG_HEIGHT
print(f"proved the ({A0}, {B0}) Fibonacci sequence: F_{n} = {public_values[2]} over {n} rows")
print("verifier accepts the proof: ", accepts(public_values))
print("verifier accepts a wrong F_n: ", accepts(lied_about))
openvm-zorch
A lean OpenVM prover built on zorch's scheme-agnostic SNARK blocks.
FRX → zorch (scheme-/zkVM-agnostic blocks) → openvm-zorch (SWIRL glue)
FRX is Fractalyze's fork of JAX, and it compiles through Fractalyze's fork of XLA. Both differ from upstream in the way that matters here: finite fields are native dtypes, not emulated. Everywhere below, FRX and XLA mean those forks.
OpenVM proves with SWIRL — a sumcheck-based proof system composing
LogUp-GKR (interactions), a batched ZeroCheck with univariate skip
(constraints), a stacked opening reduction, and a WHIR polynomial commitment —
as implemented by
openvm-stark-backend at tag
v2.0.0. This repo re-implements that prover on zorch, keeping
only the SWIRL-specific surface here and pushing every generic block upstream.
Installation
Python 3.11 on Linux x86_64, or macOS on Apple Silicon. (frxlib ships a
cp311 wheel for those two platforms only — not 3.12/3.13, not Intel Macs.)
CPU
pip install openvm-zorch
GPU (CUDA 12)
pip install openvm-zorch 'frx[cuda12]' \
--extra-index-url https://fractalyze.github.io/pypi/simple/
The extra index carries the CUDA plugin wheels, which are too large for PyPI's per-file limit. It is not needed for the CPU tier.
Verify
python -c "import frx, openvm_zorch; print(frx.devices()); print(openvm_zorch.__version__)"
[CpuDevice(id=0)] means the CPU tier; a CUDA install prints the GPU devices.
Development
bazel test //... # hermetic, CPU by default
Install the git hooks with both stages named. Plain pre-commit install wires
only the pre-commit stage, which leaves the commit-message linter inactive —
a malformed commit message then sails through to CI:
pre-commit install --install-hooks --hook-type pre-commit --hook-type commit-msg
Commit messages follow Conventional Commits:
a valid type, a lowercase summary with no trailing period, a header of at most
80 characters, and a body on everything but docs. The scope is the package the
change lives in — commit, logup_gkr, logup_zerocheck, poseidon2,
stacked_reduction, whir — or one of prove, verify, verify_prove,
transcript, fields, poly_common, bench_common, release for the modules
directly under openvm_zorch/. A change spanning several takes no scope. The
same linter runs in CI over every commit in a pull request and over the PR title.
Regenerate golden fixtures (requires Rust toolchain; pinned to the reference tag, so output is reproducible):
cd tools/fixture-gen
cargo run --release -- \
--out ../../openvm_zorch/commit/testdata/stacked_commit \
--transcript-out ../../openvm_zorch/testdata/transcript \
--gkr-out ../../openvm_zorch/logup_gkr/testdata/logup_gkr \
--zerocheck-out ../../openvm_zorch/logup_zerocheck/testdata/zerocheck \
--stacking-out ../../openvm_zorch/stacked_reduction/testdata/stacking \
--whir-out ../../openvm_zorch/whir/testdata/whir \
--prove-out ../../openvm_zorch/testdata/prove
Reference pin
openvm-stark-backend tag v2.0.0
(16d60de724c21dcadfde7d8315a1db507e5832d7) — the same pin the openvm
v2.0.0 release consumes. Config: BabyBear base field,
BabyBear⁴ challenges, Poseidon2 width-16 (default_babybear_poseidon2_16,
plonky3 =0.4.3).
Documentation
See docs/ for the full index — pipeline & terminology, and
development & benchmarking.
License
Licensed under the Apache License, Version 2.0 (see LICENSE).