From 90b86b3a873afc350a91a23a6fe8252deccd5845 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 17 May 2026 10:11:32 +0200 Subject: [PATCH 01/69] whir in python --- Cargo.lock | 3 + crates/backend/fiat-shamir/src/lib.rs | 2 +- crates/backend/fiat-shamir/src/transcript.rs | 6 +- crates/lean_prover/Cargo.toml | 1 + crates/lean_prover/test_whir.py | 100 ++ crates/lean_prover/tests/dump_whir_configs.rs | 84 + crates/lean_prover/verifier.py | 1386 ++++++++++++++++ crates/lean_prover/whir_configs.json | 1478 +++++++++++++++++ crates/whir/Cargo.toml | 2 + crates/whir/tests/dump_test_vectors.rs | 603 +++++++ 10 files changed, 3661 insertions(+), 4 deletions(-) create mode 100644 crates/lean_prover/test_whir.py create mode 100644 crates/lean_prover/tests/dump_whir_configs.rs create mode 100644 crates/lean_prover/verifier.py create mode 100644 crates/lean_prover/whir_configs.json create mode 100644 crates/whir/tests/dump_test_vectors.rs diff --git a/Cargo.lock b/Cargo.lock index d938586b8..f7b279a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -532,6 +532,7 @@ dependencies = [ "rand", "rec_aggregation", "serde", + "serde_json", "sub_protocols", "tracing", "utils", @@ -710,6 +711,8 @@ dependencies = [ "mt-utils", "rand", "rayon", + "serde", + "serde_json", "system-info", "tracing", "tracing-forest", diff --git a/crates/backend/fiat-shamir/src/lib.rs b/crates/backend/fiat-shamir/src/lib.rs index d3f79c9ed..da272cb02 100644 --- a/crates/backend/fiat-shamir/src/lib.rs +++ b/crates/backend/fiat-shamir/src/lib.rs @@ -16,7 +16,7 @@ mod transcript; pub use transcript::{DIGEST_LEN_FE, MerkleOpening, MerklePath, MerklePaths, Proof, RawProof}; mod merkle_pruning; -pub(crate) use merkle_pruning::*; +pub use merkle_pruning::PrunedMerklePaths; mod verifier; pub use verifier::*; diff --git a/crates/backend/fiat-shamir/src/transcript.rs b/crates/backend/fiat-shamir/src/transcript.rs index 612c2d109..31cecf3f5 100644 --- a/crates/backend/fiat-shamir/src/transcript.rs +++ b/crates/backend/fiat-shamir/src/transcript.rs @@ -27,12 +27,12 @@ pub struct MerklePath { } #[derive(Debug, Clone)] -pub struct MerklePaths(pub(crate) Vec>); +pub struct MerklePaths(pub Vec>); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Proof { - pub(crate) transcript: Vec, - pub(crate) merkle_paths: Vec>, + pub transcript: Vec, + pub merkle_paths: Vec>, } impl Proof { diff --git a/crates/lean_prover/Cargo.toml b/crates/lean_prover/Cargo.toml index bab7da203..2163ed200 100644 --- a/crates/lean_prover/Cargo.toml +++ b/crates/lean_prover/Cargo.toml @@ -27,3 +27,4 @@ serde.workspace = true [dev-dependencies] xmss.workspace = true rec_aggregation.workspace = true +serde_json.workspace = true diff --git a/crates/lean_prover/test_whir.py b/crates/lean_prover/test_whir.py new file mode 100644 index 000000000..05758d782 --- /dev/null +++ b/crates/lean_prover/test_whir.py @@ -0,0 +1,100 @@ +"""Run the Python WHIR verifier against the Rust-generated test vectors. + +Generate the vectors first (in this repo's `target/whir_test_vectors/`): + cargo test --release -p mt-whir --test dump_test_vectors -- --nocapture +Then run: + .venv/bin/python crates/lean_prover/test_whir.py +""" + +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from verifier import ( # noqa: E402 + EF, + Fp, + MerkleOpening, + ParsedCommitment, + Proof, + ProofError, + SparseStatement, + SparseValue, + VerifierState, + prunedpaths_from_json, + restore_merkle_paths, + whir_config, + whir_verify, +) + + +def _ef(coords: list[int]) -> EF: + return EF([Fp(v) for v in coords]) + + +def _load(path: Path) -> tuple: + raw = json.loads(path.read_text()) + statement = [ + SparseStatement( + total_num_variables=s["total_num_variables"], + point=[_ef(p) for p in s["point"]], + values=[SparseValue(v["selector"], _ef(v["value"])) for v in s["values"]], + is_next=s["is_next"], + ) + for s in raw["statement"] + ] + transcript = [Fp(v) for v in raw["proof"]["transcript"]] + openings: list[MerkleOpening] = [] + for bucket in raw["proof"]["merkle_paths"]: + restored = restore_merkle_paths(prunedpaths_from_json(bucket)) + for r in restored: + openings.append(MerkleOpening(leaf_data=r.leaf_data, path=r.sibling_hashes)) + proof = Proof(transcript=transcript, merkle_openings=openings) + expected = [_ef(p) for p in raw["expected_folding_randomness"]] + return raw["num_variables"], raw["log_inv_rate"], statement, proof, expected + + +def run(path: Path) -> bool: + num_vars, log_inv_rate, statement, proof, expected = _load(path) + cfg = whir_config(log_inv_rate, num_vars) + state = VerifierState(proof) + parsed = ParsedCommitment( + num_variables=num_vars, + root=state.next_base_scalars_vec(8), + ood_points=state.sample_vec(cfg.commitment_ood_samples) if cfg.commitment_ood_samples > 0 else [], + ood_answers=[], + ) + if cfg.commitment_ood_samples > 0: + parsed.ood_answers = state.next_extension_scalars_vec(cfg.commitment_ood_samples) + try: + got = whir_verify(state, cfg, parsed, statement) + except ProofError as e: + print(f" {path.name}: ProofError: {e}") + return False + if len(got) != len(expected): + print(f" {path.name}: evaluation-point length mismatch ({len(got)} vs {len(expected)})") + return False + for i, (a, b) in enumerate(zip(got, expected)): + if a != b: + print(f" {path.name}: evaluation_point[{i}] mismatch (got {a}, expected {b})") + return False + print(f" {path.name}: OK") + return True + + +def main() -> int: + out_dir = Path(__file__).resolve().parents[2] / "target" / "whir_test_vectors" + skip = {"challenger_sanity.json", "merkle_sanity.json", "state_trace.json", "permute_oracle.json"} + vectors = sorted(p for p in out_dir.glob("*.json") if p.name not in skip) + if not vectors: + print(f"No test vectors in {out_dir}.") + return 1 + ok = True + for v in vectors: + ok &= run(v) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/crates/lean_prover/tests/dump_whir_configs.rs b/crates/lean_prover/tests/dump_whir_configs.rs new file mode 100644 index 000000000..e78001783 --- /dev/null +++ b/crates/lean_prover/tests/dump_whir_configs.rs @@ -0,0 +1,84 @@ +//! Dump the float-derived parts of every reachable `WhirConfig` to +//! `crates/lean_prover/whir_configs.json` so the Python verifier doesn't have +//! to redo the soundness-formula computation in pure Python. +//! +//! Only the values that come from f64 math are dumped (query counts, OOD +//! samples, grinding bits). Everything else (per-round `num_variables`, +//! `domain_size`, `folding_factor`, `log_inv_rate`, `folded_domain_gen`, +//! `n_rounds`, `final_sumcheck_rounds`, `final_log_inv_rate`) is integer +//! arithmetic and is derived on the Python side. +//! +//! Run: +//! cargo test -p lean_prover --test dump_whir_configs -- --nocapture + +use std::fs; +use std::path::PathBuf; + +use backend::{TwoAdicField, WhirConfig}; +use lean_prover::default_whir_config; +use lean_vm::{EF, F, MAX_WHIR_LOG_INV_RATE, MIN_WHIR_LOG_INV_RATE}; +use serde::Serialize; + +#[derive(Serialize)] +struct Round { + num_queries: usize, + ood_samples: usize, + query_pow_bits: usize, + folding_pow_bits: usize, +} + +#[derive(Serialize)] +struct Config { + log_inv_rate: usize, + num_variables: usize, + commitment_ood_samples: usize, + starting_folding_pow_bits: usize, + final_queries: usize, + final_query_pow_bits: usize, + rounds: Vec, +} + +#[test] +fn dump_whir_configs() { + let mut configs = Vec::new(); + + for log_inv_rate in MIN_WHIR_LOG_INV_RATE..=MAX_WHIR_LOG_INV_RATE { + let builder = default_whir_config(log_inv_rate); + let first_ff = builder.folding_factor.at_round(0); + + let min_nv = first_ff; + let max_nv = F::TWO_ADICITY + first_ff - log_inv_rate; + + for num_variables in min_nv..=max_nv { + let cfg: WhirConfig = WhirConfig::new(&builder, num_variables); + + let rounds = cfg + .round_parameters + .iter() + .map(|r| Round { + num_queries: r.num_queries, + ood_samples: r.ood_samples, + query_pow_bits: r.query_pow_bits, + folding_pow_bits: r.folding_pow_bits, + }) + .collect(); + + configs.push(Config { + log_inv_rate, + num_variables, + commitment_ood_samples: cfg.commitment_ood_samples, + starting_folding_pow_bits: cfg.starting_folding_pow_bits, + final_queries: cfg.final_queries, + final_query_pow_bits: cfg.final_query_pow_bits, + rounds, + }); + } + } + + let json = serde_json::to_string_pretty(&configs).unwrap(); + let path = std::env::var("WHIR_DUMP_PATH").map(PathBuf::from).unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("whir_configs.json") + }); + fs::write(&path, json).unwrap_or_else(|e| panic!("failed to write {}: {e}", path.display())); + println!("wrote {} WhirConfig entries to {}", configs.len(), path.display()); +} diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py new file mode 100644 index 000000000..24fecb150 --- /dev/null +++ b/crates/lean_prover/verifier.py @@ -0,0 +1,1386 @@ +""" +Setup: + uv venv .venv --python 3.12 + VIRTUAL_ENV=.venv uv pip install "git+https://github.com/leanEthereum/leanSpec.git" + .venv/bin/python crates/lean_prover/verifier.py +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Iterable, Sequence + +from lean_spec.subspecs.koalabear import Fp, P +from lean_spec.subspecs.poseidon1 import PARAMS_16, Poseidon1 + +SECURITY_BITS = 124 +MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 +WHIR_INITIAL_FOLDING_FACTOR = 7 +WHIR_SUBSEQUENT_FOLDING_FACTOR = 5 +RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 + +# Poseidon16 duplex sponge parameters (challenger.rs). +RATE = 8 +WIDTH = 16 +CAPACITY = WIDTH - RATE +DIGEST_LEN_FE = 8 +DIGEST_ELEMS = 8 + +# Hardcoded leanVM SNARK domain separator (lean_prover/src/lib.rs) +SNARK_DOMAIN_SEP = [ + Fp(v) for v in ( + 130704175, 1303721200, 493664240, 1035493700, + 2063844858, 1410214009, 1938905908, 1696767928, + ) +] + +# Bounds (mirrors lean_vm/src/core/constants.rs). +MIN_WHIR_LOG_INV_RATE = 1 +MAX_WHIR_LOG_INV_RATE = 4 +MIN_LOG_MEMORY_SIZE = 16 +MAX_LOG_MEMORY_SIZE = 26 +MIN_LOG_N_ROWS_PER_TABLE = 8 +MIN_BYTECODE_LOG_SIZE = 8 +BASE_TWO_ADICITY = 24 # KoalaBear + +# WHIR config table dumped by `cargo test -p lean_prover --test dump_whir_configs`. +# Lives next to this file. +WHIR_CONFIGS_PATH = "whir_configs.json" + + +# --------------------------------------------------------------------------- +# Error type +# --------------------------------------------------------------------------- + + +class ProofError(Exception): + """Mirrors backend::ProofError.""" + + +# --------------------------------------------------------------------------- +# Quintic extension field: EF = Fp[X] / (X^5 + X^2 - 1) +# Reduction rule: X^5 = 1 - X^2. +# --------------------------------------------------------------------------- + + +class EF: + """Element of the degree-5 extension of KoalaBear. + + Stored as 5 base coefficients `c[0..5]` representing `c0 + c1 X + ... + c4 X^4`. + Irreducible polynomial: X^5 + X^2 - 1, i.e. X^5 ≡ 1 - X^2. + """ + + __slots__ = ("c",) + DIMENSION = 5 + + def __init__(self, coeffs: Sequence[Fp]): + assert len(coeffs) == 5 + self.c = tuple(coeffs) + + # --- constructors ----------------------------------------------------- + + @staticmethod + def zero() -> "EF": + return EF([Fp(0)] * 5) + + @staticmethod + def one() -> "EF": + return EF([Fp(1), Fp(0), Fp(0), Fp(0), Fp(0)]) + + @staticmethod + def from_base(x: Fp) -> "EF": + return EF([x, Fp(0), Fp(0), Fp(0), Fp(0)]) + + @staticmethod + def from_basis_coefficients(coeffs: Sequence[Fp]) -> "EF": + return EF(coeffs) + + # --- arithmetic ------------------------------------------------------- + + def __add__(self, other: "EF") -> "EF": + return EF([a + b for a, b in zip(self.c, other.c)]) + + def __sub__(self, other: "EF") -> "EF": + return EF([a - b for a, b in zip(self.c, other.c)]) + + def __neg__(self) -> "EF": + return EF([-a for a in self.c]) + + def __mul__(self, other: "EF | Fp | int") -> "EF": + if isinstance(other, Fp): + return EF([a * other for a in self.c]) + if isinstance(other, int): + f = Fp(other % P) + return EF([a * f for a in self.c]) + # Schoolbook poly mul mod (X^5 + X^2 - 1). + a, b = self.c, other.c + prod = [Fp(0)] * 9 # degree up to 8 + for i in range(5): + for j in range(5): + prod[i + j] = prod[i + j] + a[i] * b[j] + # Reduce: X^5 = 1 - X^2, X^k for k >= 5 reduced repeatedly. + for k in range(8, 4, -1): # 8,7,6,5 + coef = prod[k] + if int(coef.value) == 0: + continue + # X^k = X^(k-5) * X^5 = X^(k-5) * (1 - X^2) + prod[k] = Fp(0) + prod[k - 5] = prod[k - 5] + coef + prod[k - 3] = prod[k - 3] - coef + return EF(prod[:5]) + + __rmul__ = __mul__ + + def __eq__(self, other: object) -> bool: + if not isinstance(other, EF): + return NotImplemented + return self.c == other.c + + def __hash__(self) -> int: + return hash(self.c) + + def __repr__(self) -> str: + return f"EF({[int(x.value) for x in self.c]})" + + def is_zero(self) -> bool: + return all(int(x.value) == 0 for x in self.c) + + def inv(self) -> "EF": + """Inverse via Fermat: a^(q-2) where q = P^5. Slow but simple.""" + if self.is_zero(): + raise ZeroDivisionError("EF inverse of zero") + exp = P ** 5 - 2 + return self.pow(exp) + + def pow(self, n: int) -> "EF": + if n < 0: + return self.inv().pow(-n) + result = EF.one() + base = self + while n > 0: + if n & 1: + result = result * base + base = base * base + n >>= 1 + return result + + +# --------------------------------------------------------------------------- +# Poseidon16-based Challenger (duplex sponge) +# --------------------------------------------------------------------------- + + +_POSEIDON16 = Poseidon1(PARAMS_16) + + +def poseidon16_permute(state: list[Fp]) -> list[Fp]: + """Apply the Poseidon16 permutation to a length-WIDTH state. + + NOTE: this is the bare permutation. For the Davies-Meyer-style + *compression* used by Merkle trees use `poseidon16_compress_in_place`. + The Fiat-Shamir challenger uses the bare permutation (no feed-forward). + """ + assert len(state) == WIDTH + return _POSEIDON16.permute(state) + + +def poseidon16_compress_in_place(state: list[Fp]) -> list[Fp]: + """`compress_in_place`: out = permute(state) + state (feed-forward).""" + assert len(state) == WIDTH + permuted = _POSEIDON16.permute(state) + return [a + b for a, b in zip(permuted, state)] + + +def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: + """2:1 Merkle compression: `compress_in_place(left || right)[:DIGEST_ELEMS]`.""" + assert len(left) == DIGEST_ELEMS and len(right) == DIGEST_ELEMS + return poseidon16_compress_in_place(list(left) + list(right))[:DIGEST_ELEMS] + + +def hash_slice(data: Sequence[Fp]) -> list[Fp]: + """`symetric::hash_slice` with WIDTH=16, RATE=OUT=8 (right-to-left absorbing). + + Uses the same `compress_in_place` (feed-forward) primitive as Merkle, NOT + the bare permutation used by the challenger. + """ + assert len(data) % RATE == 0 + n_chunks = len(data) // RATE + assert n_chunks >= 2 + state = list(data[len(data) - WIDTH :]) + state = poseidon16_compress_in_place(state) + for chunk_idx in range(n_chunks - 3, -1, -1): + offset = chunk_idx * RATE + state = state[:CAPACITY] + list(data[offset : offset + RATE]) + state = poseidon16_compress_in_place(state) + return state[:DIGEST_ELEMS] + + +class Challenger: + """Poseidon16 duplex sponge. + + Mirrors `fiat_shamir::challenger`: state is a length-WIDTH array, the rate + portion lives in `state[CAPACITY..]`. `observe` overwrites the rate, then + permutes. `sample` reads the rate and asserts `rate_fresh`. + """ + + def __init__(self) -> None: + self.state: list[Fp] = [Fp(0)] * WIDTH + self.rate_fresh: bool = False + + def observe(self, value: Sequence[Fp]) -> None: + assert len(value) == RATE + self.state = self.state[:CAPACITY] + list(value) + self.state = poseidon16_permute(self.state) + self.rate_fresh = True + + def observe_many(self, scalars: Sequence[Fp]) -> None: + for i in range(0, len(scalars), RATE): + chunk = list(scalars[i : i + RATE]) + if len(chunk) < RATE: + chunk = chunk + [Fp(0)] * (RATE - len(chunk)) + self.observe(chunk) + + def duplex(self) -> None: + self.observe([Fp(0)] * RATE) + + def sample(self) -> list[Fp]: + assert self.rate_fresh, "stale rate — insert a duplex() before sampling" + out = list(self.state[CAPACITY:]) + self.rate_fresh = False + return out + + def sample_many(self, n: int) -> list[list[Fp]]: + if n == 0: + return [] + out = [self.sample()] + for _ in range(1, n): + self.duplex() + out.append(self.sample()) + return out + + def sample_ef_vec(self, n: int) -> list[EF]: + """Mirrors utils::sample_vec — pulls ceil(n*5/8) blocks, takes first n*5.""" + n_blocks = (n * EF.DIMENSION + RATE - 1) // RATE + flat: list[Fp] = [] + for block in self.sample_many(n_blocks): + flat.extend(block) + flat = flat[: n * EF.DIMENSION] + return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] + + def sample_ef(self) -> EF: + return self.sample_ef_vec(1)[0] + + def sample_in_range(self, bits: int, n_samples: int) -> list[int]: + """Mirrors challenger::sample_in_range — not perfectly uniform.""" + assert bits < 31 + n_blocks = (n_samples + RATE - 1) // RATE + flat: list[Fp] = [] + for block in self.sample_many(n_blocks): + flat.extend(block) + mask = (1 << bits) - 1 + return [int(x.value) & mask for x in flat[:n_samples]] + + +# --------------------------------------------------------------------------- +# Proof container + VerifierState (transcript reader) +# --------------------------------------------------------------------------- + + +@dataclass +class MerkleOpening: + """Restored Merkle opening: matches fiat_shamir::transcript::MerkleOpening.""" + + leaf_data: list[Fp] + path: list[list[Fp]] # each sibling is a length-DIGEST_ELEMS digest + + +@dataclass +class Proof: + """Verifier-side proof: matches backend::Proof. + + `merkle_openings` is the RESTORED list of openings (post-pruning), in the + order the verifier consumes them. + """ + + transcript: list[Fp] + merkle_openings: list[MerkleOpening] = field(default_factory=list) + + +class VerifierState: + """Mirrors fiat_shamir::verifier::VerifierState exactly.""" + + def __init__(self, proof: Proof) -> None: + self.challenger = Challenger() + self.transcript: list[Fp] = list(proof.transcript) + self.offset = 0 + self.merkle_openings: list[MerkleOpening] = list(proof.merkle_openings) + self.merkle_opening_index = 0 + self.raw_transcript: list[Fp] = [] + + # ---- internal helpers ---------------------------------------------- + + def _read(self, n: int) -> list[Fp]: + if self.offset + n > len(self.transcript): + raise ProofError("ExceededTranscript") + out = self.transcript[self.offset : self.offset + n] + self.offset += n + return out + + def _absorb_and_record(self, scalars: list[Fp]) -> None: + self.challenger.observe_many(scalars) + self.raw_transcript.extend(scalars) + padded = (len(scalars) + RATE - 1) // RATE * RATE + self.raw_transcript.extend([Fp(0)] * (padded - len(scalars))) + + # ---- FSVerifier trait surface -------------------------------------- + + def observe_scalars(self, scalars: Sequence[Fp]) -> None: + self.challenger.observe_many(list(scalars)) + + def duplex(self) -> None: + self.challenger.duplex() + + def next_base_scalars_vec(self, n: int) -> list[Fp]: + scalars = self._read(n) + self._absorb_and_record(scalars) + return scalars + + def next_extension_scalars_vec(self, n: int) -> list[EF]: + flat = self.next_base_scalars_vec(n * EF.DIMENSION) + return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] + + def next_extension_scalar(self) -> EF: + return self.next_extension_scalars_vec(1)[0] + + def sample(self) -> EF: + return self.challenger.sample_ef() + + def sample_vec(self, n: int) -> list[EF]: + return self.challenger.sample_ef_vec(n) + + def sample_in_range(self, bits: int, n_samples: int) -> list[int]: + return self.challenger.sample_in_range(bits, n_samples) + + def next_merkle_opening(self) -> MerkleOpening: + if self.merkle_opening_index >= len(self.merkle_openings): + raise ProofError("ExceededTranscript: no more Merkle openings") + op = self.merkle_openings[self.merkle_opening_index] + self.merkle_opening_index += 1 + return op + + def check_pow_grinding(self, bits: int) -> None: + if bits == 0: + return + witness = self._read(1) + self.challenger.observe_many(witness) + # Rust now reads state[CAPACITY] (= state[8], i.e. the first element of + # the rate portion) after the absorb-permute. + if int(self.challenger.state[CAPACITY].value) & ((1 << bits) - 1) != 0: + raise ProofError("InvalidGrindingWitness") + self.raw_transcript.append(witness[0]) + self.raw_transcript.extend([Fp(0)] * (RATE - 1)) + + def next_sumcheck_polynomial( + self, + n_coeffs: int, + claimed_sum: EF, + eq_alpha: EF | None, + ) -> list[EF]: + """Mirrors `verifier::next_sumcheck_polynomial`. + + With `eq_alpha=None`: prover sends h(X) of `n_coeffs` coeffs, omitting + `c0` (recovered from `claimed_sum = h(0) + h(1)`). We read + `(n_coeffs-1) * 5` base scalars, recover `c0`, and absorb the full + flattened polynomial. + + With `eq_alpha=Some(α)`: prover sends a "bare" polynomial of + `n_coeffs - 1` coefficients; the verifier recovers `h0` and expands to + the full degree polynomial via `expand_bare_to_full`. + """ + if eq_alpha is None: + rest = self.next_extension_scalars_vec_no_record(n_coeffs - 1) + c0 = _ef_halve(claimed_sum - _ef_sum(rest)) + full = [c0] + rest + self._absorb_and_record(_flatten_ef_list(full)) + return full + + # eq_alpha path + rest_scalars = self._read((n_coeffs - 2) * EF.DIMENSION) + rest_bare = [ + EF(rest_scalars[i : i + EF.DIMENSION]) + for i in range(0, len(rest_scalars), EF.DIMENSION) + ] + h0 = claimed_sum - eq_alpha * _ef_sum(rest_bare) + bare = [h0] + rest_bare + full_coeffs = _expand_bare_to_full(bare, eq_alpha) + self._absorb_and_record(_flatten_ef_list(full_coeffs)) + return full_coeffs + + def next_extension_scalars_vec_no_record(self, n: int) -> list[EF]: + """Read `n` extension scalars but DON'T record/absorb yet — caller does.""" + flat = self._read(n * EF.DIMENSION) + return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] + + +# --------------------------------------------------------------------------- +# Bytecode (minimal placeholder) + helpers +# --------------------------------------------------------------------------- + + +@dataclass +class Bytecode: + """Subset of lean_vm::Bytecode needed by the verifier.""" + + hash: list[Fp] # 8 base elements + log_size: int # log2 of bytecode length + instructions_multilinear: object | None = None # TODO: multilinear repr + + def log_size_(self) -> int: + return self.log_size + + +poseidon16_compress_pair = poseidon16_compress # alias for utils::poseidon16_compress_pair + + +# --- small helpers used by next_sumcheck_polynomial --- + + +def _halve_fp() -> Fp: + # Multiplicative inverse of 2 mod P. Computed once at import time. + return Fp(pow(2, P - 2, P)) + + +_HALVE_FP = _halve_fp() + + +def _ef_halve(x: EF) -> EF: + return EF([c * _HALVE_FP for c in x.c]) + + +def _ef_sum(xs: Sequence[EF]) -> EF: + acc = EF.zero() + for x in xs: + acc = acc + x + return acc + + +def _flatten_ef_list(xs: Sequence[EF]) -> list[Fp]: + out: list[Fp] = [] + for x in xs: + out.extend(x.c) + return out + + +def _expand_bare_to_full(bare: list[EF], alpha: EF) -> list[EF]: + """utils::expand_bare_to_full: g(X) = eq(α, X) * h(X).""" + one_minus_alpha = EF.one() - alpha + two_alpha_minus_one = (alpha + alpha) - EF.one() + d = len(bare) - 1 + full: list[EF] = [one_minus_alpha * bare[0]] + for k in range(1, d + 1): + full.append(one_minus_alpha * bare[k] + two_alpha_minus_one * bare[k - 1]) + full.append(two_alpha_minus_one * bare[d]) + return full + + +def log2_ceil_usize(x: int) -> int: + if x <= 1: + return 0 + return (x - 1).bit_length() + + +def log2_strict_usize(x: int) -> int: + assert x > 0 and (x & (x - 1)) == 0, f"{x} is not a power of two" + return x.bit_length() - 1 + + +def padd_with_zero_to_next_power_of_two(values: Sequence[Fp]) -> list[Fp]: + if not values: + return [Fp(0)] + n = 1 + while n < len(values): + n <<= 1 + return list(values) + [Fp(0)] * (n - len(values)) + + +# --------------------------------------------------------------------------- +# Merkle: hashing primitives, pruned-paths restoration, Merkle verify. +# Mirrors symetric::merkle + fiat_shamir::merkle_pruning. +# --------------------------------------------------------------------------- + + +@dataclass +class MerklePath: + """Mirror of fiat_shamir::MerklePath (the un-pruned form).""" + + leaf_data: list[Fp] + sibling_hashes: list[list[Fp]] # each entry has DIGEST_ELEMS Fp + leaf_index: int + + +@dataclass +class PrunedMerklePaths: + """Mirror of fiat_shamir::PrunedMerklePaths — input to restore().""" + + merkle_height: int + original_order: list[int] + leaf_data: list[list[Fp]] + paths: list[tuple[int, list[list[Fp]]]] # (leaf_index, siblings) with skips + n_trailing_zeros: int + + +def _lca_level(a: int, b: int) -> int: + """Number of bits needed to differ — i.e., ceiling-LCA level over the tree.""" + diff = a ^ b + return diff.bit_length() + + +def restore_merkle_paths(p: PrunedMerklePaths) -> list[MerklePath]: + """Port of `merkle_pruning::restore` (fiat_shamir). + + Reconstructs full sibling arrays from the pruned form using leaf hashing + and 2:1 compression (Poseidon16). Raises ProofError on malformed inputs. + """ + + h = p.merkle_height + if h >= 32: + raise ProofError("Merkle height too large") + if p.n_trailing_zeros > 1024: + raise ProofError("Merkle leaf trailing-zero count too large") + + leaf_data = [list(d) + [Fp(0)] * p.n_trailing_zeros for d in p.leaf_data] + n = len(p.paths) + + def levels(i: int) -> int: + if i == 0: + return h + return _lca_level(p.paths[i - 1][0], p.paths[i][0]) + + def skip(i: int) -> int | None: + if i + 1 < n: + return _lca_level(p.paths[i][0], p.paths[i + 1][0]) - 1 + return None + + # Backward pass: build subtree hashes. + subtree_hashes: list[list[list[Fp]]] = [[] for _ in range(n)] + for i in range(n - 1, -1, -1): + leaf_idx, stored = p.paths[i] + if leaf_idx >= (1 << h): + raise ProofError("Merkle leaf index out of range") + stored_iter = iter(stored) + cur = hash_slice(leaf_data[i]) + subtree_hashes[i].append(list(cur)) + for lvl in range(levels(i)): + if skip(i) == lvl: + try: + sibling = subtree_hashes[i + 1][lvl] + except IndexError as e: + raise ProofError("Merkle restore: missing sibling") from e + else: + try: + sibling = next(stored_iter) + except StopIteration as e: + raise ProofError("Merkle restore: stored siblings exhausted") from e + if ((leaf_idx >> lvl) & 1) == 0: + cur = poseidon16_compress(cur, sibling) + else: + cur = poseidon16_compress(sibling, cur) + subtree_hashes[i].append(list(cur)) + + # Forward pass: build the full sibling lists. + restored: list[MerklePath] = [] + for i in range(n): + leaf_idx, stored = p.paths[i] + stored_iter = iter(stored) + siblings: list[list[Fp]] = [] + for lvl in range(levels(i)): + if skip(i) == lvl: + sibling = subtree_hashes[i + 1][lvl] + else: + try: + sibling = next(stored_iter) + except StopIteration as e: + raise ProofError("Merkle restore: stored siblings exhausted (fwd)") from e + siblings.append(list(sibling)) + if restored: + # Reuse the previous restored path's siblings for the levels above. + siblings.extend(restored[-1].sibling_hashes[levels(i) :]) + restored.append(MerklePath(leaf_data=leaf_data[i], sibling_hashes=siblings, leaf_index=leaf_idx)) + + # Reorder by original_order. + return [restored[idx] for idx in p.original_order] + + +def merkle_verify_path( + commit: list[Fp], + log_height: int, + index: int, + opened_values: Sequence[Fp], + opening_proof: Sequence[list[Fp]], +) -> bool: + """Mirror of symetric::merkle::merkle_verify (length-DIGEST_ELEMS digests).""" + + if len(opening_proof) != log_height: + return False + cur = hash_slice(list(opened_values)) + idx = index + for sibling in opening_proof: + if idx & 1 == 0: + cur = poseidon16_compress(cur, sibling) + else: + cur = poseidon16_compress(sibling, cur) + idx >>= 1 + return list(commit) == list(cur) + + +def prunedpaths_from_json(obj: dict) -> PrunedMerklePaths: + """Helper for test vectors: parse the JSON shape dumped by Rust.""" + return PrunedMerklePaths( + merkle_height=obj["merkle_height"], + original_order=list(obj["original_order"]), + leaf_data=[[Fp(v) for v in chunk] for chunk in obj["leaf_data"]], + paths=[(p["leaf_index"], [[Fp(v) for v in s] for s in p["siblings"]]) for p in obj["paths"]], + n_trailing_zeros=obj["n_trailing_zeros"], + ) + + +# --------------------------------------------------------------------------- +# WHIR polynomial primitives (poly + whir crates) +# --------------------------------------------------------------------------- + + +def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: + """[x, x^2, x^4, ..., x^{2^{n-1}}] — matches MultilinearPoint::expand_from_univariate.""" + out: list[EF] = [] + cur = x + for _ in range(num_variables): + out.append(cur) + cur = cur * cur + return out + + +def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: + """Π (1 + 2 a_i b_i − a_i − b_i) (eq polynomial).""" + assert len(a) == len(b) + one = EF.one() + acc = one + for x, y in zip(a, b): + acc = acc * (one + (x * y) + (x * y) - x - y) + return acc + + +def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: + """Port of poly::next_mle (the "next" multilinear on the boolean cube).""" + assert len(x) == len(y) + n = len(x) + one = EF.one() + eq_prefix: list[EF] = [one] + for i in range(n): + eq_i = x[i] * y[i] + (one - x[i]) * (one - y[i]) + eq_prefix.append(eq_prefix[i] * eq_i) + low_suffix: list[EF] = [one] * (n + 1) + for i in range(n - 1, -1, -1): + low_suffix[i] = low_suffix[i + 1] * x[i] * (one - y[i]) + s = EF.zero() + for arr in range(n): + carry = (one - x[arr]) * y[arr] + s = s + eq_prefix[arr] * carry * low_suffix[arr + 1] + prod = one + for v in list(x) + list(y): + prod = prod * v + return s + prod + + +def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: + """Evaluate a multilinear in *evaluation* form (length 2^n) at point ∈ EF^n. + + Big-endian indexing: index `i` corresponds to the bits `(b_0, ..., b_{n-1})` + where `b_0` is the *most significant* bit, matching `poly::eval_multilinear`. + Fold variables from the last to the first. + """ + assert len(evals) == 1 << len(point) + cur = list(evals) + for r in reversed(point): + nxt: list[EF] = [] + for j in range(0, len(cur), 2): + nxt.append(cur[j] + (cur[j + 1] - cur[j]) * r) + cur = nxt + return cur[0] + + +def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: + """poly::eval_multilinear_coeffs: split coeffs in half, recurse. + + `coeffs` represents `Σ_b c_b · x_0^{b_0} · ... · x_{n-1}^{b_{n-1}}` + in the standard multilinear coefficient basis. + """ + assert len(coeffs) == 1 << len(point) + if not point: + return coeffs[0] + x = point[0] + tail = point[1:] + half = len(coeffs) // 2 + return eval_multilinear_coeffs(coeffs[:half], tail) + eval_multilinear_coeffs(coeffs[half:], tail) * x + + +@dataclass +class SparseValue: + selector: int + value: EF + + +@dataclass +class SparseStatement: + """Mirror of whir::SparseStatement.""" + + total_num_variables: int + point: list[EF] # the "inner" point, length = inner_num_variables + values: list[SparseValue] + is_next: bool = False + + @property + def inner_num_variables(self) -> int: + return len(self.point) + + @property + def selector_num_variables(self) -> int: + return self.total_num_variables - self.inner_num_variables + + @staticmethod + def new_(total: int, point: list[EF], values: list[SparseValue]) -> "SparseStatement": + assert total >= len(point) + return SparseStatement(total, point, values, is_next=False) + + @staticmethod + def dense(point: list[EF], value: EF) -> "SparseStatement": + return SparseStatement(len(point), point, [SparseValue(0, value)], is_next=False) + + +# --------------------------------------------------------------------------- +# WHIR config helpers: derive integer-only parameters from the trimmed JSON +# --------------------------------------------------------------------------- + + +def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: + """FoldingFactor::compute_number_of_rounds with default (7, 5, max_send=8).""" + nv_except_first = num_variables - WHIR_INITIAL_FOLDING_FACTOR + max_send = MAX_NUM_VARIABLES_TO_SEND_COEFFS + if nv_except_first < max_send: + return 0, nv_except_first + n_rounds = -(-(nv_except_first - max_send) // WHIR_SUBSEQUENT_FOLDING_FACTOR) + final_sumcheck_rounds = nv_except_first - n_rounds * WHIR_SUBSEQUENT_FOLDING_FACTOR + return n_rounds, final_sumcheck_rounds + + +def whir_folding_factor_at_round(r: int) -> int: + return WHIR_INITIAL_FOLDING_FACTOR if r == 0 else WHIR_SUBSEQUENT_FOLDING_FACTOR + + +def whir_rs_reduction_factor(r: int) -> int: + return RS_DOMAIN_INITIAL_REDUCTION_FACTOR if r == 0 else 1 + + +def whir_log_inv_rate_at(starting_log_inv_rate: int, round_index: int) -> int: + rate = starting_log_inv_rate + for r in range(round_index): + rate += whir_folding_factor_at_round(r) - whir_rs_reduction_factor(r) + return rate + + +def whir_num_variables_at_round(num_variables: int, round_index: int) -> int: + """num_variables remaining at the START of round `round_index` (the verifier + parses a new commitment at this num_variables for that round). + """ + rem = num_variables + for r in range(round_index + 1): + rem -= whir_folding_factor_at_round(r) + return rem + + +# KoalaBear two-adic generators: index `bits` is the primitive 2^bits-th root of unity. +# Mirrors KoalaBearParameters::TWO_ADIC_GENERATORS (canonical-form u32 values). +KB_TWO_ADIC_GENERATORS: list[int] = [ + 0x1, 0x7F000000, 0x7E010002, 0x6832FE4A, 0x08DBD69C, 0x0A28F031, 0x5C4A5B99, + 0x29B75A80, 0x17668B8A, 0x27AD539B, 0x334D48C7, 0x7744959C, 0x768FC6FA, + 0x303964B2, 0x3E687D4D, 0x45A60E61, 0x6E2F4D7A, 0x163BD499, 0x6C4A8A45, + 0x143EF899, 0x514DDCAD, 0x484EF19B, 0x205D63C3, 0x68E7DD49, 0x6AC49F88, +] + + +def two_adic_generator(bits: int) -> Fp: + """Mirror of KoalaBear::two_adic_generator(bits).""" + assert 0 <= bits <= BASE_TWO_ADICITY + return Fp(KB_TWO_ADIC_GENERATORS[bits]) + + +def whir_domain_size_at(num_variables: int, starting_log_inv_rate: int, round_index: int) -> int: + """domain_size that goes into `round_parameters[round_index]`. + + The Rust code seeds `domain_size = 1 << (num_variables + log_inv_rate)` and + halves by `rs_reduction_factor(round)` BEFORE moving to the next round, so + the value stored in round r is the *current* domain_size pre-reduction. + """ + domain_log = num_variables + starting_log_inv_rate + for r in range(round_index): + domain_log -= whir_rs_reduction_factor(r) + return 1 << domain_log + + +# --------------------------------------------------------------------------- +# WHIR config table — float-derived numbers only, dumped by the Rust test. +# +# Everything not in the JSON (n_rounds, per-round num_variables/log_inv_rate/ +# domain_size/folding_factor/folded_domain_gen, final_sumcheck_rounds, +# final_log_inv_rate, ...) is integer arithmetic and should be derived on the +# Python side when it's actually needed. +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class WhirRoundConfig: + num_queries: int + ood_samples: int + query_pow_bits: int + folding_pow_bits: int + + +@dataclass(frozen=True) +class WhirConfig: + log_inv_rate: int + num_variables: int + commitment_ood_samples: int + starting_folding_pow_bits: int + final_queries: int + final_query_pow_bits: int + rounds: tuple[WhirRoundConfig, ...] + + +def _load_whir_configs() -> dict[tuple[int, int], WhirConfig]: + import json + from pathlib import Path + + path = Path(__file__).with_name(WHIR_CONFIGS_PATH) + with open(path) as f: + raw = json.load(f) + + out: dict[tuple[int, int], WhirConfig] = {} + for c in raw: + cfg = WhirConfig( + log_inv_rate=c["log_inv_rate"], + num_variables=c["num_variables"], + commitment_ood_samples=c["commitment_ood_samples"], + starting_folding_pow_bits=c["starting_folding_pow_bits"], + final_queries=c["final_queries"], + final_query_pow_bits=c["final_query_pow_bits"], + rounds=tuple(WhirRoundConfig(**r) for r in c["rounds"]), + ) + out[(cfg.log_inv_rate, cfg.num_variables)] = cfg + return out + + +_WHIR_CONFIGS: dict[tuple[int, int], WhirConfig] | None = None + + +def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: + global _WHIR_CONFIGS + if _WHIR_CONFIGS is None: + _WHIR_CONFIGS = _load_whir_configs() + key = (log_inv_rate, num_variables) + if key not in _WHIR_CONFIGS: + raise KeyError( + f"No WHIR config for log_inv_rate={log_inv_rate}, num_variables={num_variables}. " + f"Regenerate with: cargo test -p lean_prover --test dump_whir_configs" + ) + return _WHIR_CONFIGS[key] + + +# --------------------------------------------------------------------------- +# WHIR verifier (port of crates/whir/src/verify.rs) +# --------------------------------------------------------------------------- + + +@dataclass +class ParsedCommitment: + """Mirror of whir::ParsedCommitment.""" + + num_variables: int + root: list[Fp] # length DIGEST_ELEMS + ood_points: list[EF] + ood_answers: list[EF] + + def oods_constraints(self) -> list[SparseStatement]: + """One dense SparseStatement per (point, eval) pair.""" + out: list[SparseStatement] = [] + for p, ev in zip(self.ood_points, self.ood_answers): + point = expand_from_univariate(p, self.num_variables) + out.append(SparseStatement.dense(point, ev)) + return out + + +def parsed_commitment_parse(state: VerifierState, num_variables: int, ood_samples: int) -> ParsedCommitment: + """Port of ParsedCommitment::parse.""" + root = state.next_base_scalars_vec(DIGEST_ELEMS) + ood_points: list[EF] = [] + ood_answers: list[EF] = [] + if ood_samples > 0: + ood_points = state.sample_vec(ood_samples) + ood_answers = state.next_extension_scalars_vec(ood_samples) + return ParsedCommitment( + num_variables=num_variables, + root=root, + ood_points=ood_points, + ood_answers=ood_answers, + ) + + +def verify_sumcheck_rounds( + state: VerifierState, + claimed_sum_ref: list[EF], # 1-element box, mutated in-place + rounds: int, + pow_bits: int, +) -> list[EF]: + """Port of whir::verify::verify_sumcheck_rounds. + + Returns the folding randomness for these rounds. Mutates `claimed_sum_ref[0]`. + """ + randomness: list[EF] = [] + for _ in range(rounds): + coeffs = state.next_sumcheck_polynomial(3, claimed_sum_ref[0], None) + state.check_pow_grinding(pow_bits) + r = state.sample() + # Evaluate cubic poly (length-3 coeffs in standard univariate basis). + # DensePolynomial::evaluate uses Horner-style on coeffs[0..n]. + claimed_sum_ref[0] = _eval_univariate(coeffs, r) + randomness.append(r) + return randomness + + +def _eval_univariate(coeffs: list[EF], x: EF) -> EF: + """Standard univariate evaluation: c[0] + c[1]*x + c[2]*x^2 + ...""" + acc = EF.zero() + for c in reversed(coeffs): + acc = acc * x + c + return acc + + +def combine_constraints( + state: VerifierState, + claimed_sum_ref: list[EF], + constraints: list[SparseStatement], +) -> list[EF]: + """Port of combine_constraints — mutates claimed_sum_ref[0] in-place.""" + gamma: EF = state.sample() + combination = [EF.one()] + for smt in constraints: + for v in smt.values: + pow_prev = combination[-1] + claimed_sum_ref[0] = claimed_sum_ref[0] + pow_prev * v.value + combination.append(pow_prev * gamma) + combination.pop() + return combination + + +def verify_stir_challenges( + state: VerifierState, + cfg: WhirConfig, + round_index: int, + num_variables: int, + log_inv_rate: int, + folding_factor: int, + next_folding_factor: int, + num_queries: int, + query_pow_bits: int, + commitment: ParsedCommitment, + folding_randomness: list[EF], +) -> list[SparseStatement]: + """Port of WhirConfig::verify_stir_challenges (incl. Merkle verification). + + `folding_factor` is the folding factor applied AT this round (i.e. how the + leaves are arranged). `next_folding_factor` is the AIR sumcheck folding for + the *next* hop; for the final pseudo-round it equals `folding_factor`. + + Returns STIR constraints (SparseStatements) for the next claim-combining. + """ + # Domain size at this round (pre-RS-reduction for round `round_index`). + domain_size = whir_domain_size_at(cfg.num_variables, cfg.log_inv_rate, round_index) + folded_domain_size = domain_size >> folding_factor + folded_domain_gen = two_adic_generator(domain_size.bit_length() - 1 - folding_factor) + + state.check_pow_grinding(query_pow_bits) + indices = state.sample_in_range(folded_domain_size.bit_length() - 1, num_queries) + + leafs_base_field = round_index == 0 + log_height = folded_domain_size.bit_length() - 1 + answers_ef: list[list[EF]] = [] + for idx in indices: + op = state.next_merkle_opening() + if not merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path): + raise ProofError("Merkle verification failed") + # leaf_data is base; if leafs encode EF, pack 5 base → 1 EF. + if leafs_base_field: + answers_ef.append([EF.from_base(f) for f in op.leaf_data]) + else: + ans: list[EF] = [] + for i in range(0, len(op.leaf_data), EF.DIMENSION): + ans.append(EF(op.leaf_data[i : i + EF.DIMENSION])) + answers_ef.append(ans) + + # Each answer is a length-(2^folding_factor) eval-form multilinear; fold at folding_randomness. + folds: list[EF] = [eval_multilinear_evals(a, folding_randomness) for a in answers_ef] + + stir_constraints: list[SparseStatement] = [] + for idx, fold in zip(indices, folds): + point = folded_domain_gen.value + # point = folded_domain_gen ^ idx, as a base-field element wrapped into EF. + gen_pow = pow(int(folded_domain_gen.value), idx, P) + ef_pt = EF.from_base(Fp(gen_pow)) + expanded = expand_from_univariate(ef_pt, num_variables) + stir_constraints.append(SparseStatement.dense(expanded, fold)) + return stir_constraints + + +def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> bool: + """Port of verify_constraint_coeffs. + + Checks that the constraint's point is `[α, α^2, α^4, ...]` and that + the univariate polynomial (Horner) evaluates to each claimed value. + """ + assert constraint.selector_num_variables == 0 + alpha = constraint.point[0] + for a, b in zip(constraint.point, constraint.point[1:]): + if a * a != b: + return False + # Horner from highest-degree coefficient (last in `coeffs`) downward. + univ_eval = EF.zero() + for c in reversed(coeffs): + univ_eval = univ_eval * alpha + c + return all(univ_eval == v.value for v in constraint.values) + + +def eval_constraints_poly( + constraints: list[tuple[list[EF], list[SparseStatement]]], + point: list[EF], +) -> EF: + """Port of WhirConfig::eval_constraints_poly. + + `constraints` is a list of (combination_randomness, sparse_statements) per + round. `point` is the global folding randomness; it is sliced down by the + folding factor of each preceding round before use. + """ + value = EF.zero() + pt = list(point) + for round_idx, (randomness, smts) in enumerate(constraints): + if round_idx > 0: + k = whir_folding_factor_at_round(round_idx - 1) + pt = pt[k:] + i = 0 + for smt in smts: + inner_pt = pt[len(pt) - smt.inner_num_variables :] + if smt.is_next: + common_weight = next_mle(smt.point, inner_pt) + else: + common_weight = eq_poly_outside(smt.point, inner_pt) + for v in smt.values: + # Per-selector lagrange weight on bits NOT covered by the inner point. + lagrange = EF.one() + for j in range(smt.selector_num_variables): + bit = (v.selector >> (smt.selector_num_variables - 1 - j)) & 1 + lagrange = lagrange * (pt[j] if bit else (EF.one() - pt[j])) + value = value + lagrange * common_weight * randomness[i] + i += 1 + assert i == len(randomness) + return value + + +def whir_verify( + state: VerifierState, + cfg: WhirConfig, + parsed_commitment: ParsedCommitment, + statement: list[SparseStatement], +) -> list[EF]: + """Port of WhirConfig::verify. Returns the folding randomness.""" + for s in statement: + assert s.total_num_variables == parsed_commitment.num_variables + + n_rounds, final_sumcheck_rounds = whir_n_rounds_and_final_sumcheck(cfg.num_variables) + + round_constraints: list[tuple[list[EF], list[SparseStatement]]] = [] + round_folding: list[list[EF]] = [] + claimed_sum_ref: list[EF] = [EF.zero()] + prev_commitment = parsed_commitment + + # OODS + initial statement combine. + state.duplex() + initial_constraints = prev_commitment.oods_constraints() + statement + combo = combine_constraints(state, claimed_sum_ref, initial_constraints) + round_constraints.append((combo, initial_constraints)) + + # Initial sumcheck. + folding_rand = verify_sumcheck_rounds( + state, + claimed_sum_ref, + whir_folding_factor_at_round(0), + cfg.starting_folding_pow_bits, + ) + round_folding.append(folding_rand) + + # Round loop. + for r in range(n_rounds): + rp = cfg.rounds[r] + # New num_variables = num_variables_at_round(after this round's first absorb) + # In Rust: round_state.num_variables = num_variables - folding_factor.total_number(r+1) + nvars_round = cfg.num_variables - sum( + whir_folding_factor_at_round(i) for i in range(r + 1) + ) + new_commitment = parsed_commitment_parse(state, nvars_round, rp.ood_samples) + + stir_constraints = verify_stir_challenges( + state, + cfg, + round_index=r, + num_variables=nvars_round, + log_inv_rate=whir_log_inv_rate_at(cfg.log_inv_rate, r), + folding_factor=whir_folding_factor_at_round(r), + next_folding_factor=whir_folding_factor_at_round(r + 1), + num_queries=rp.num_queries, + query_pow_bits=rp.query_pow_bits, + commitment=prev_commitment, + folding_randomness=round_folding[-1], + ) + constraints_r = new_commitment.oods_constraints() + stir_constraints + + state.duplex() + combo_r = combine_constraints(state, claimed_sum_ref, constraints_r) + round_constraints.append((combo_r, constraints_r)) + + folding_rand_r = verify_sumcheck_rounds( + state, + claimed_sum_ref, + whir_folding_factor_at_round(r + 1), + rp.folding_pow_bits, + ) + round_folding.append(folding_rand_r) + prev_commitment = new_commitment + + # Final round: read the final polynomial coefficients (length 2^n_vars_final). + n_vars_final = cfg.num_variables - sum( + whir_folding_factor_at_round(i) for i in range(n_rounds + 1) + ) + final_coeffs = state.next_extension_scalars_vec(1 << n_vars_final) + + # Final STIR challenges (against the last commitment) — uses final_round_config. + # In Rust: final.domain_size = round_params.last().domain_size >> rs_reduction_factor(n_rounds-1). + # `whir_domain_size_at(num_variables, log_inv_rate, n_rounds)` already applies all the + # reductions for rounds 0..n_rounds, so it equals final.domain_size directly. + final_domain_size = whir_domain_size_at(cfg.num_variables, cfg.log_inv_rate, n_rounds) + final_folding_factor = whir_folding_factor_at_round(n_rounds) + final_num_variables = ( + cfg.num_variables - sum(whir_folding_factor_at_round(i) for i in range(n_rounds + 1)) + ) + folded_domain_size_final = final_domain_size >> final_folding_factor + folded_gen_final = two_adic_generator( + final_domain_size.bit_length() - 1 - final_folding_factor + ) + + state.check_pow_grinding(cfg.final_query_pow_bits) + indices_final = state.sample_in_range( + folded_domain_size_final.bit_length() - 1, cfg.final_queries + ) + log_height_final = folded_domain_size_final.bit_length() - 1 + answers_ef: list[list[EF]] = [] + for idx in indices_final: + op = state.next_merkle_opening() + if not merkle_verify_path( + prev_commitment.root, log_height_final, idx, op.leaf_data, op.path + ): + raise ProofError("Final Merkle verification failed") + if n_rounds == 0: + answers_ef.append([EF.from_base(f) for f in op.leaf_data]) + else: + ans: list[EF] = [] + for i in range(0, len(op.leaf_data), EF.DIMENSION): + ans.append(EF(op.leaf_data[i : i + EF.DIMENSION])) + answers_ef.append(ans) + folds_final = [eval_multilinear_evals(a, round_folding[-1]) for a in answers_ef] + final_stir: list[SparseStatement] = [] + for idx, fold in zip(indices_final, folds_final): + gen_pow = pow(int(folded_gen_final.value), idx, P) + ef_pt = EF.from_base(Fp(gen_pow)) + expanded = expand_from_univariate(ef_pt, final_num_variables) + final_stir.append(SparseStatement.dense(expanded, fold)) + + # Verify STIR constraints directly on final polynomial coefficients. + for c in final_stir: + if not verify_constraint_coeffs(c, final_coeffs): + raise ProofError("Final STIR constraint mismatch") + + # Final sumcheck. + final_sumcheck_rand = verify_sumcheck_rounds( + state, claimed_sum_ref, final_sumcheck_rounds, 0 + ) + round_folding.append(final_sumcheck_rand) + + # Flatten all folding randomness; evaluate the constraint weights polynomial. + folding_randomness_flat = [r for chunk in round_folding for r in chunk] + eval_weights = eval_constraints_poly(round_constraints, folding_randomness_flat) + + # Final coeffs are evaluated at REVERSED final_sumcheck_rand. + reversed_point = list(reversed(final_sumcheck_rand)) + final_value = eval_multilinear_coeffs(final_coeffs, reversed_point) + if claimed_sum_ref[0] != eval_weights * final_value: + raise ProofError("WHIR final sumcheck check failed") + + return folding_randomness_flat + + +# --------------------------------------------------------------------------- +# Stubs still pending for the lean_prover verifier +# --------------------------------------------------------------------------- + + +def stacked_pcs_parse_commitment(*args, **kwargs): + raise NotImplementedError("stacked_pcs_parse_commitment: port from sub_protocols/stacked_pcs.rs") + + +def verify_generic_logup(*args, **kwargs): + raise NotImplementedError("verify_generic_logup: port from sub_protocols/logup.rs") + + +def sumcheck_verify(*args, **kwargs): + raise NotImplementedError("sumcheck_verify: port from sub_protocols/air_sumcheck.rs (or sumcheck crate)") + + +# --------------------------------------------------------------------------- +# Top-level verifier (skeleton) +# --------------------------------------------------------------------------- + + +@dataclass +class ProofVerificationDetails: + bytecode_evaluation: object # Evaluation — TODO + + +def verify_execution( + bytecode: Bytecode, + public_input: Sequence[Fp], + proof: Proof, + n_tables: int, +) -> ProofVerificationDetails: + """Port of `verify_execution` (lean_prover/src/verify_execution.rs). + + Implements the prologue (dim/bound checks, transcript priming); calls into + stubs for the heavy sub-protocols. + + NOTE: `n_tables` (= N_TABLES in lean_vm) is passed in until we port the + table enum here; same for the per-table size limits. + """ + + state = VerifierState(proof) + state.observe_scalars(list(public_input)) + state.observe_scalars(poseidon16_compress_pair(bytecode.hash, SNARK_DOMAIN_SEP)) + + dims = [int(x.value) for x in state.next_base_scalars_vec(3 + n_tables)] + log_inv_rate = dims[0] + log_memory = dims[1] + public_input_len = dims[2] + table_log_n_rows = dims[3 : 3 + n_tables] + + if public_input_len != len(public_input): + raise ProofError("InvalidProof: public_input length mismatch") + + if not (MIN_WHIR_LOG_INV_RATE <= log_inv_rate <= MAX_WHIR_LOG_INV_RATE): + raise ProofError("InvalidRate") + + for log_n_rows in table_log_n_rows: + if log_n_rows < MIN_LOG_N_ROWS_PER_TABLE: + raise ProofError("InvalidProof: table too small") + # TODO: per-table upper bound (max_log_n_rows_per_table). + + max_table_log = max(table_log_n_rows) if table_log_n_rows else 0 + if log_memory < max(max_table_log, bytecode.log_size): + raise ProofError("InvalidProof: memory smaller than tables/bytecode") + + if not (MIN_LOG_MEMORY_SIZE <= log_memory <= MAX_LOG_MEMORY_SIZE): + raise ProofError("InvalidProof: log_memory out of range") + + if bytecode.log_size < MIN_BYTECODE_LOG_SIZE: + raise ProofError("InvalidProof: bytecode too small") + + public_memory = padd_with_zero_to_next_power_of_two(public_input) # noqa: F841 (used once WHIR is wired) + + # ------------- below: not implemented yet ----------------- + # parsed_commitment = stacked_pcs_parse_commitment(state, log_memory, bytecode.log_size, table_log_n_rows) + # logup_c = state.sample() + # logup_alphas = state.sample_vec(log2_ceil_usize(max_bus_width)) + # logup_statements = verify_generic_logup(state, logup_c, logup_alphas, log_memory, ...) + # bus_beta = state.sample(); air_alpha = state.sample(); eta = state.sample() + # ... sumcheck_verify(...), per-table AIR eval, whir_verify(...) ... + raise NotImplementedError( + "verify_execution: sub-protocols (WHIR/logup/sumcheck/AIR) are not yet ported." + ) + + +# --------------------------------------------------------------------------- +# Self-test: foundations only +# --------------------------------------------------------------------------- + + +def _smoke() -> None: + print(f"KoalaBear P = {P}") + + # EF sanity: (X) * (X^4 + X) should reduce since X^5 = 1 - X^2. + X = EF([Fp(0), Fp(1), Fp(0), Fp(0), Fp(0)]) + X4 = X.pow(4) + X5 = X * X4 + expected = EF.one() - EF([Fp(0), Fp(0), Fp(1), Fp(0), Fp(0)]) # 1 - X^2 + assert X5 == expected, (X5, expected) + one = EF.one() + a = EF([Fp(3), Fp(1), Fp(4), Fp(1), Fp(5)]) + assert a * a.inv() == one + print("EF arithmetic OK") + + # Challenger / sponge sanity: deterministic outputs. + ch1 = Challenger() + ch1.observe_many([Fp(1), Fp(2), Fp(3)]) + s1 = ch1.sample_ef() + ch2 = Challenger() + ch2.observe_many([Fp(1), Fp(2), Fp(3)]) + s2 = ch2.sample_ef() + assert s1 == s2, "Challenger not deterministic" + print(f"Challenger sample (deterministic) = {s1}") + + # WHIR config table: sample lookup. + cfg = whir_config(log_inv_rate=1, num_variables=20) + print( + f"WHIR cfg(log_inv_rate=1, num_vars=20): rounds={len(cfg.rounds)}, " + f"final_queries={cfg.final_queries}, final_query_pow_bits={cfg.final_query_pow_bits}" + ) + if cfg.rounds: + r0 = cfg.rounds[0] + print( + f" round[0]: num_queries={r0.num_queries}, ood_samples={r0.ood_samples}, " + f"query_pow_bits={r0.query_pow_bits}, folding_pow_bits={r0.folding_pow_bits}" + ) + + # VerifierState read/sample round-trip. + proof = Proof(transcript=[Fp(i) for i in range(20)]) + st = VerifierState(proof) + st.observe_scalars([Fp(7)]) + base = st.next_base_scalars_vec(3) + print(f"VerifierState read 3 base scalars: {base}") + chal = st.sample() + print(f"VerifierState sample = {chal}") + + # verify_execution prologue runs but bails at the first sub-protocol stub. + bc = Bytecode(hash=[Fp(0)] * 8, log_size=10) + bad_proof = Proof(transcript=[Fp(0)] * 64) + try: + verify_execution(bc, [Fp(0)] * 4, bad_proof, n_tables=0) + except NotImplementedError as e: + print(f"verify_execution prologue reached sub-protocol stub: {e}") + except ProofError as e: + print(f"verify_execution failed bound check (expected with dummy proof): {e}") + + +if __name__ == "__main__": + _smoke() diff --git a/crates/lean_prover/whir_configs.json b/crates/lean_prover/whir_configs.json new file mode 100644 index 000000000..306461ede --- /dev/null +++ b/crates/lean_prover/whir_configs.json @@ -0,0 +1,1478 @@ +[ + { + "log_inv_rate": 1, + "num_variables": 7, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 10, + "final_queries": 220, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 8, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 11, + "final_queries": 220, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 9, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 12, + "final_queries": 220, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 10, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 13, + "final_queries": 220, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 11, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 14, + "final_queries": 220, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 12, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 15, + "final_queries": 220, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 13, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 220, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 14, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 15, + "final_queries": 221, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 15, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 221, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 1, + "num_variables": 16, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 222, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 11 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 17, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 223, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 18, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 224, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 13 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 19, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 225, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 14 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 20, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 227, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 15 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 21, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 32, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 229, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 73, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 9 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 22, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 32, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 230, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + }, + { + "num_queries": 74, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 10 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 23, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 32, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 234, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 13 + }, + { + "num_queries": 74, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 11 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 24, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 32, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 235, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 14 + }, + { + "num_queries": 74, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 25, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 32, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 241, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 74, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 13 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 26, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 21, + "final_query_pow_bits": 14, + "rounds": [ + { + "num_queries": 243, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 74, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + }, + { + "num_queries": 32, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 27, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 21, + "final_query_pow_bits": 14, + "rounds": [ + { + "num_queries": 248, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 75, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 32, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 28, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 21, + "final_query_pow_bits": 14, + "rounds": [ + { + "num_queries": 256, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 75, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 32, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 29, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 21, + "final_query_pow_bits": 14, + "rounds": [ + { + "num_queries": 262, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 76, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 12 + }, + { + "num_queries": 33, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 17 + } + ] + }, + { + "log_inv_rate": 1, + "num_variables": 30, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 21, + "final_query_pow_bits": 14, + "rounds": [ + { + "num_queries": 270, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 76, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 13 + }, + { + "num_queries": 33, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 18 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 7, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 13, + "final_queries": 109, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 8, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 14, + "final_queries": 109, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 9, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 15, + "final_queries": 109, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 10, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 109, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 11, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 12, + "final_queries": 110, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 12, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 13, + "final_queries": 110, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 13, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 14, + "final_queries": 110, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 14, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 15, + "final_queries": 110, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 15, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 110, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 2, + "num_variables": 16, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 14, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 111, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 10 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 17, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 15, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 111, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 11 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 18, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 111, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 19, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 15, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 112, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 13 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 20, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 112, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 14 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 21, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 28, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 113, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 55, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 10 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 22, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 28, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 114, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 55, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 11 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 23, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 28, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 114, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 13 + }, + { + "num_queries": 56, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 24, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 28, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 115, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 14 + }, + { + "num_queries": 56, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 13 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 25, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 28, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 118, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 56, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 26, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 19, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 118, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 56, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 28, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 17 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 27, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 19, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 119, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 13 + }, + { + "num_queries": 57, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 28, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 18 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 28, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 19, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 120, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + }, + { + "num_queries": 57, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + }, + { + "num_queries": 29, + "ood_samples": 2, + "query_pow_bits": 15, + "folding_pow_bits": 19 + } + ] + }, + { + "log_inv_rate": 2, + "num_variables": 29, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 19, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 123, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 57, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 29, + "ood_samples": 2, + "query_pow_bits": 15, + "folding_pow_bits": 20 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 7, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 9, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 8, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 10, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 9, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 11, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 10, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 12, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 11, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 13, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 12, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 14, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 13, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 15, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 14, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 73, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 15, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 12, + "final_queries": 74, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 3, + "num_variables": 16, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 13, + "final_queries": 44, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 74, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 11 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 17, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 14, + "final_queries": 44, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 74, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 18, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 44, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 74, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 13 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 19, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 44, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 74, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 14 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 20, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 44, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 75, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 15 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 21, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 25, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 75, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 44, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 11 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 22, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 25, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 76, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 11 + }, + { + "num_queries": 45, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 23, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 25, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 76, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + }, + { + "num_queries": 45, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 13 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 24, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 25, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 77, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 13 + }, + { + "num_queries": 45, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 25, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 25, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 78, + "ood_samples": 2, + "query_pow_bits": 15, + "folding_pow_bits": 14 + }, + { + "num_queries": 45, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 26, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 18, + "final_query_pow_bits": 12, + "rounds": [ + { + "num_queries": 79, + "ood_samples": 2, + "query_pow_bits": 15, + "folding_pow_bits": 15 + }, + { + "num_queries": 45, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 25, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 19 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 27, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 18, + "final_query_pow_bits": 12, + "rounds": [ + { + "num_queries": 80, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 45, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 26, + "ood_samples": 2, + "query_pow_bits": 13, + "folding_pow_bits": 20 + } + ] + }, + { + "log_inv_rate": 3, + "num_variables": 28, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 18, + "final_query_pow_bits": 12, + "rounds": [ + { + "num_queries": 82, + "ood_samples": 2, + "query_pow_bits": 15, + "folding_pow_bits": 15 + }, + { + "num_queries": 46, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 26, + "ood_samples": 2, + "query_pow_bits": 13, + "folding_pow_bits": 21 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 7, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 8, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 8, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 9, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 9, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 10, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 10, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 11, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 11, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 12, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 12, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 13, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 13, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 14, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 14, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 15, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 15, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 16, + "final_queries": 55, + "final_query_pow_bits": 16, + "rounds": [] + }, + { + "log_inv_rate": 4, + "num_variables": 16, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 13, + "final_queries": 37, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 56, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 9 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 17, + "commitment_ood_samples": 1, + "starting_folding_pow_bits": 14, + "final_queries": 37, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 56, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 10 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 18, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 37, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 56, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 11 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 19, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 37, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 56, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 12 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 20, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 13, + "final_queries": 37, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 57, + "ood_samples": 1, + "query_pow_bits": 16, + "folding_pow_bits": 13 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 21, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 14, + "final_queries": 23, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 57, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + }, + { + "num_queries": 37, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 12 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 22, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 23, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 57, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + }, + { + "num_queries": 37, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 13 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 23, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 23, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 57, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 37, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 24, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 23, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 58, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 13 + }, + { + "num_queries": 38, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 15 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 25, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 23, + "final_query_pow_bits": 15, + "rounds": [ + { + "num_queries": 58, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 14 + }, + { + "num_queries": 38, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 26, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 16, + "final_queries": 16, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 60, + "ood_samples": 2, + "query_pow_bits": 15, + "folding_pow_bits": 15 + }, + { + "num_queries": 38, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 17 + }, + { + "num_queries": 23, + "ood_samples": 2, + "query_pow_bits": 15, + "folding_pow_bits": 22 + } + ] + }, + { + "log_inv_rate": 4, + "num_variables": 27, + "commitment_ood_samples": 2, + "starting_folding_pow_bits": 15, + "final_queries": 16, + "final_query_pow_bits": 16, + "rounds": [ + { + "num_queries": 61, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 16 + }, + { + "num_queries": 38, + "ood_samples": 2, + "query_pow_bits": 16, + "folding_pow_bits": 18 + }, + { + "num_queries": 23, + "ood_samples": 2, + "query_pow_bits": 15, + "folding_pow_bits": 23 + } + ] + } +] \ No newline at end of file diff --git a/crates/whir/Cargo.toml b/crates/whir/Cargo.toml index 1c2a2b0a7..aba785d2b 100644 --- a/crates/whir/Cargo.toml +++ b/crates/whir/Cargo.toml @@ -21,3 +21,5 @@ tracing.workspace = true [dev-dependencies] tracing-forest.workspace = true tracing-subscriber.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/whir/tests/dump_test_vectors.rs b/crates/whir/tests/dump_test_vectors.rs new file mode 100644 index 000000000..54878f65c --- /dev/null +++ b/crates/whir/tests/dump_test_vectors.rs @@ -0,0 +1,603 @@ +//! Generates WHIR test vectors for the Python verifier. +//! +//! Run: +//! cargo test --release -p mt-whir --test dump_test_vectors -- --nocapture +//! +//! Outputs `target/whir_test_vectors/.json`. Each file contains, in +//! canonical (non-Monty) `u32` form: +//! +//! - `num_variables`, `log_inv_rate`, WHIR builder params, +//! - `statement`: list of `SparseStatement` (point + values), +//! - `proof.transcript`: the raw `Vec` written by the prover, +//! - `proof.merkle_paths`: pruned Merkle paths exactly as serialized by +//! `PrunedMerklePaths` (Python must port the restore routine), +//! - `expected_folding_randomness`: what `WhirConfig::verify` returns. + +use std::fs; +use std::path::PathBuf; + +use fiat_shamir::{ProverState, VerifierState}; +use field::{PrimeCharacteristicRing, PrimeField32, TwoAdicField}; +use koala_bear::{KoalaBear, QuinticExtensionFieldKB, default_koalabear_poseidon1_16}; +use mt_whir::*; +use poly::*; +use rand::{RngExt, SeedableRng, rngs::StdRng}; +use serde::Serialize; + +type F = KoalaBear; +type EF = QuinticExtensionFieldKB; + +const DIGEST_ELEMS: usize = 8; + +fn f_to_u32(x: F) -> u32 { + x.as_canonical_u32() +} + +fn ef_to_u32s(x: EF) -> [u32; 5] { + use field::BasedVectorSpace; + let coeffs: &[F] = x.as_basis_coefficients_slice(); + [ + f_to_u32(coeffs[0]), + f_to_u32(coeffs[1]), + f_to_u32(coeffs[2]), + f_to_u32(coeffs[3]), + f_to_u32(coeffs[4]), + ] +} + +#[derive(Serialize)] +struct SparseValueJson { + selector: usize, + value: [u32; 5], +} + +#[derive(Serialize)] +struct SparseStatementJson { + total_num_variables: usize, + is_next: bool, + point: Vec<[u32; 5]>, + values: Vec, +} + +#[derive(Serialize)] +struct PrunedPathJson { + leaf_index: usize, + siblings: Vec<[u32; DIGEST_ELEMS]>, +} + +#[derive(Serialize)] +struct PrunedMerklePathsJson { + merkle_height: usize, + original_order: Vec, + leaf_data: Vec>, + paths: Vec, + n_trailing_zeros: usize, +} + +#[derive(Serialize)] +struct ProofJson { + transcript: Vec, + merkle_paths: Vec, +} + +#[derive(Serialize)] +struct WhirBuilderJson { + security_level: usize, + max_num_variables_to_send_coeffs: usize, + pow_bits: usize, + folding_factor_first: usize, + folding_factor_subsequent: usize, + starting_log_inv_rate: usize, + rs_domain_initial_reduction_factor: usize, + soundness_type: &'static str, // "JohnsonBound" +} + +#[derive(Serialize)] +struct TestVectorJson { + name: String, + num_variables: usize, + log_inv_rate: usize, + builder: WhirBuilderJson, + statement: Vec, + proof: ProofJson, + expected_folding_randomness: Vec<[u32; 5]>, +} + +fn convert_pruned(p: &fiat_shamir::PrunedMerklePaths) -> PrunedMerklePathsJson { + PrunedMerklePathsJson { + merkle_height: p.merkle_height, + original_order: p.original_order.clone(), + leaf_data: p + .leaf_data + .iter() + .map(|v| v.iter().map(|&f| f_to_u32(f)).collect()) + .collect(), + paths: p + .paths + .iter() + .map(|(idx, siblings)| PrunedPathJson { + leaf_index: *idx, + siblings: siblings.iter().map(|d| d.map(f_to_u32)).collect(), + }) + .collect(), + n_trailing_zeros: p.n_trailing_zeros, + } +} + +fn convert_statement(s: &SparseStatement) -> SparseStatementJson { + SparseStatementJson { + total_num_variables: s.total_num_variables, + is_next: s.is_next, + point: s.point.iter().map(|&p| ef_to_u32s(p)).collect(), + values: s + .values + .iter() + .map(|v| SparseValueJson { + selector: v.selector, + value: ef_to_u32s(v.value), + }) + .collect(), + } +} + +fn run_one(name: &str, num_variables: usize, log_inv_rate: usize, seed: u64, out_dir: &PathBuf) { + let poseidon16 = default_koalabear_poseidon1_16(); + + // Use the same defaults as `lean_prover::default_whir_config`, so the + // Python side can reuse the dumped WhirConfig table. + let builder = WhirConfigBuilder { + security_level: 124, + max_num_variables_to_send_coeffs: 8, + pow_bits: 16, + folding_factor: FoldingFactor::new(7, 5), + soundness_type: SecurityAssumption::JohnsonBound, + starting_log_inv_rate: log_inv_rate, + rs_domain_initial_reduction_factor: 5, + }; + let params = WhirConfig::::new(&builder, num_variables); + + let mut rng = StdRng::seed_from_u64(seed); + let num_coeffs = 1usize << num_variables; + let polynomial: Vec = (0..num_coeffs).map(|_| rng.random::()).collect(); + + // One simple statement: a fully-dense point + its evaluation. + let dense_point: Vec = (0..num_variables).map(|_| rng.random()).collect(); + let dense_value = polynomial.evaluate_sparse(0, &MultilinearPoint(dense_point.clone())); + let statement = vec![SparseStatement::new( + num_variables, + MultilinearPoint(dense_point.clone()), + vec![SparseValue { + selector: 0, + value: dense_value, + }], + )]; + + precompute_dft_twiddles::(1 << F::TWO_ADICITY); + + let mut prover_state = ProverState::::new(poseidon16.clone()); + let polynomial_mle: MleOwned = MleOwned::Base(polynomial); + let witness = params.commit(&mut prover_state, &polynomial_mle, num_coeffs); + params.prove(&mut prover_state, statement.clone(), witness, &polynomial_mle.by_ref()); + let proof = prover_state.into_proof(); + + // Run the verifier and capture its `folding_randomness` output as the oracle. + let mut vstate = VerifierState::::new(proof.clone(), poseidon16.clone()).unwrap(); + println!("transcript[23] (pre-verify): {}", proof.transcript[23].as_canonical_u32()); + let parsed_commitment = params.parse_commitment::(&mut vstate).unwrap(); + let folding_randomness = params + .verify::(&mut vstate, &parsed_commitment, statement.clone()) + .unwrap(); + + let entry = TestVectorJson { + name: name.to_string(), + num_variables, + log_inv_rate, + builder: WhirBuilderJson { + security_level: 124, + max_num_variables_to_send_coeffs: 8, + pow_bits: 16, + folding_factor_first: 7, + folding_factor_subsequent: 5, + starting_log_inv_rate: log_inv_rate, + rs_domain_initial_reduction_factor: 5, + soundness_type: "JohnsonBound", + }, + statement: statement.iter().map(convert_statement).collect(), + proof: ProofJson { + transcript: proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), + merkle_paths: proof.merkle_paths.iter().map(convert_pruned).collect(), + }, + expected_folding_randomness: folding_randomness.iter().map(|&p| ef_to_u32s(p)).collect(), + }; + + let path = out_dir.join(format!("{name}.json")); + let json = serde_json::to_string(&entry).unwrap(); + fs::write(&path, json).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); + println!( + " {} -> {} ({:.1} KiB)", + name, + path.display(), + path.metadata().unwrap().len() as f64 / 1024.0, + ); +} + +/// Tiny sanity test for the Python Merkle restoration. Build a 16-leaf tree, +/// open a handful of indices, prune, then dump (a) the original openings (as +/// the source of truth) and (b) the pruned form Python should restore. +#[test] +fn dump_merkle_vector() { + use fiat_shamir::{MerklePath, MerklePaths, PrunedMerklePaths}; + use symetric::compress; + + let poseidon = default_koalabear_poseidon1_16(); + + // Build 16 leaves, each 8 base elements long (one RATE block). + let n = 16usize; + let leaf_len = 8usize; + let leaves: Vec> = (0..n) + .map(|i| { + (0..leaf_len) + .map(|j| F::from_u32((i * 100 + j) as u32 + 1)) + .collect() + }) + .collect(); + + // Hash each leaf to get level 0. + let hash_leaf = |data: &[F]| { + // The two-RATE-blocks requirement of hash_slice needs len(data) >= 16 + // (i.e. n_chunks >= 2). For an 8-element leaf, pad with one extra zero + // block so n_chunks = 2. + let mut padded = vec![F::ZERO; 8]; + padded.extend_from_slice(data); + symetric::hash_slice::<_, _, 16, 8, 8>(&poseidon, &padded) + }; + let hash_combine = + |l: &[F; 8], r: &[F; 8]| compress::<_, _, 8, 16>(&poseidon, [*l, *r]); + + let leaf_hashes: Vec<[F; 8]> = leaves.iter().map(|l| hash_leaf(l)).collect(); + let mut levels = vec![leaf_hashes]; + let mut log_h = n.trailing_zeros() as usize; + while levels.last().unwrap().len() > 1 { + let prev = levels.last().unwrap(); + let next: Vec<[F; 8]> = (0..prev.len() / 2) + .map(|i| hash_combine(&prev[2 * i], &prev[2 * i + 1])) + .collect(); + levels.push(next); + } + let _ = log_h; + let root: [F; 8] = *levels.last().unwrap().first().unwrap(); + + let make_path = |idx: usize| -> MerklePath { + let mut sibling_hashes = Vec::with_capacity(levels.len() - 1); + let mut k = idx; + for level in &levels[..levels.len() - 1] { + sibling_hashes.push(level[k ^ 1]); + k >>= 1; + } + // Pad leaf data with the zero-block prefix used by hash_leaf. + let mut leaf_padded = vec![F::ZERO; 8]; + leaf_padded.extend_from_slice(&leaves[idx]); + MerklePath { + leaf_data: leaf_padded, + sibling_hashes, + leaf_index: idx, + } + }; + + let indices = vec![3usize, 7, 11, 0, 11]; // duplicate included + let original = MerklePaths(indices.iter().map(|&i| make_path(i)).collect::>()); + let pruned: PrunedMerklePaths = original.clone().prune(); + + #[derive(serde::Serialize)] + struct PathJson { + leaf_index: usize, + leaf_data: Vec, + sibling_hashes: Vec<[u32; 8]>, + } + + #[derive(serde::Serialize)] + struct Out { + root: [u32; 8], + original: Vec, + pruned: PrunedMerklePathsJson, + } + + let out = Out { + root: root.map(f_to_u32), + original: original + .clone() + .0 + .into_iter() + .map(|p| PathJson { + leaf_index: p.leaf_index, + leaf_data: p.leaf_data.iter().map(|&f| f_to_u32(f)).collect(), + sibling_hashes: p.sibling_hashes.iter().map(|d| d.map(f_to_u32)).collect(), + }) + .collect(), + pruned: convert_pruned(&pruned), + }; + + let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("whir_test_vectors"); + fs::create_dir_all(&out_dir).unwrap(); + let path = out_dir.join("merkle_sanity.json"); + fs::write(&path, serde_json::to_string_pretty(&out).unwrap()).unwrap(); + println!("wrote merkle sanity to {}", path.display()); +} + +/// Replay verifier on a previously-dumped test vector to see if the saved +/// witness still satisfies the grinding check. +#[test] +fn replay_dumped_vector() { + use fiat_shamir::{ChallengeSampler, FSVerifier}; + use std::fs; + + let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("whir_test_vectors") + .join("small_lir1_nv16.json"); + let text = fs::read_to_string(&path).unwrap(); + let v: serde_json::Value = serde_json::from_str(&text).unwrap(); + let transcript_u32: Vec = v["proof"]["transcript"] + .as_array().unwrap().iter() + .map(|x| x.as_u64().unwrap() as u32) + .collect(); + let transcript: Vec = transcript_u32.iter().map(|&u| F::from_u32(u)).collect(); + let merkle_paths_json = v["proof"]["merkle_paths"].clone(); + use fiat_shamir::PrunedMerklePaths; + let merkle_paths: Vec> = merkle_paths_json + .as_array().unwrap().iter() + .map(|bucket| { + // Manually construct PrunedMerklePaths from JSON. + let merkle_height = bucket["merkle_height"].as_u64().unwrap() as usize; + let original_order: Vec = bucket["original_order"].as_array().unwrap().iter() + .map(|x| x.as_u64().unwrap() as usize).collect(); + let leaf_data: Vec> = bucket["leaf_data"].as_array().unwrap().iter() + .map(|c| c.as_array().unwrap().iter().map(|x| F::from_u32(x.as_u64().unwrap() as u32)).collect()) + .collect(); + let paths: Vec<(usize, Vec<[F; 8]>)> = bucket["paths"].as_array().unwrap().iter() + .map(|p| { + let leaf_index = p["leaf_index"].as_u64().unwrap() as usize; + let siblings: Vec<[F; 8]> = p["siblings"].as_array().unwrap().iter() + .map(|s| { + let arr: Vec = s.as_array().unwrap().iter().map(|x| F::from_u32(x.as_u64().unwrap() as u32)).collect(); + <[F; 8]>::try_from(arr).unwrap() + }).collect(); + (leaf_index, siblings) + }).collect(); + let n_trailing_zeros = bucket["n_trailing_zeros"].as_u64().unwrap() as usize; + PrunedMerklePaths { merkle_height, original_order, leaf_data, paths, n_trailing_zeros } + }).collect(); + let proof = fiat_shamir::Proof { transcript: transcript.clone(), merkle_paths }; + + let poseidon = default_koalabear_poseidon1_16(); + let mut vstate = VerifierState::::new(proof, poseidon).unwrap(); + + // Build the verifier WhirConfig and use its commitment_ood_samples count. + let builder = WhirConfigBuilder { + security_level: 124, + max_num_variables_to_send_coeffs: 8, + pow_bits: 16, + folding_factor: FoldingFactor::new(7, 5), + soundness_type: SecurityAssumption::JohnsonBound, + starting_log_inv_rate: 1, + rs_domain_initial_reduction_factor: 5, + }; + let params = WhirConfig::::new(&builder, 16); + println!("Rust params.commitment_ood_samples = {}", params.commitment_ood_samples); + println!("Rust params.starting_folding_pow_bits = {}", params.starting_folding_pow_bits); + let ood_n = params.commitment_ood_samples; + + // Walk to the first grinding check + let _root = vstate.next_base_scalars_vec(8).unwrap(); + let _ood_pts: Vec = vstate.sample_vec(ood_n); + let _ood_ans = vstate.next_extension_scalars_vec(ood_n).unwrap(); + vstate.duplex(); + let _g: EF = vstate.sample(); + let _poly0 = vstate.next_sumcheck_polynomial(3, EF::ZERO, None).unwrap(); + println!("witness u32 at transcript[23]: {}", transcript_u32[23]); + let reconstructed = F::from_u32(transcript_u32[23]); + println!("F::from_u32({}).as_canonical_u32() = {}", transcript_u32[23], reconstructed.as_canonical_u32()); + println!("transcript F repr (Debug): {:?}", transcript[23]); + println!("reconstructed F repr (Debug):{:?}", reconstructed); + match vstate.check_pow_grinding(16) { + Ok(()) => println!("Rust verifier accepts the saved witness!"), + Err(e) => println!("Rust verifier ALSO fails: {:?}", e), + } +} + +/// Cross-check that Python's Poseidon16 permutation matches Rust's on a +/// specific input. +#[test] +fn dump_permute_oracle() { + use koala_bear::symmetric::Permutation; + let poseidon = default_koalabear_poseidon1_16(); + + // Input: the state captured after_read_poly0, with rate replaced by + // [witness, 0, ..., 0] (witness = 66589315). + let input: [F; 16] = [ + 1732812002, 1231764113, 2063040591, 339182820, + 1169456582, 2099684484, 1027478197, 686152220, + 66589315, 0, 0, 0, 0, 0, 0, 0, + ].map(F::from_u32); + let mut state = input; + poseidon.permute_mut(&mut state); + + #[derive(serde::Serialize)] + struct Out { + input: Vec, + output: Vec, + } + let out = Out { + input: input.iter().map(|&f| f_to_u32(f)).collect(), + output: state.iter().map(|&f| f_to_u32(f)).collect(), + }; + let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("whir_test_vectors") + .join("permute_oracle.json"); + fs::write(&path, serde_json::to_string_pretty(&out).unwrap()).unwrap(); + println!("output: {:?}", out.output); +} + +/// Dump Rust's challenger state at every observe/sample for one test vector, +/// so we can diff vs Python. +#[test] +fn dump_state_trace() { + use fiat_shamir::{ChallengeSampler, FSProver, FSVerifier}; + + let poseidon = default_koalabear_poseidon1_16(); + let log_inv_rate = 1; + let num_variables = 16; + let builder = WhirConfigBuilder { + security_level: 124, + max_num_variables_to_send_coeffs: 8, + pow_bits: 16, + folding_factor: FoldingFactor::new(7, 5), + soundness_type: SecurityAssumption::JohnsonBound, + starting_log_inv_rate: log_inv_rate, + rs_domain_initial_reduction_factor: 5, + }; + let params = WhirConfig::::new(&builder, num_variables); + + let mut rng = StdRng::seed_from_u64(0xdead_beef); + let num_coeffs = 1usize << num_variables; + let polynomial: Vec = (0..num_coeffs).map(|_| rng.random::()).collect(); + let dense_point: Vec = (0..num_variables).map(|_| rng.random()).collect(); + let dense_value = polynomial.evaluate_sparse(0, &MultilinearPoint(dense_point.clone())); + let statement = vec![SparseStatement::new( + num_variables, + MultilinearPoint(dense_point.clone()), + vec![SparseValue { + selector: 0, + value: dense_value, + }], + )]; + + precompute_dft_twiddles::(1 << F::TWO_ADICITY); + + let mut prover_state = ProverState::::new(poseidon.clone()); + let polynomial_mle: MleOwned = MleOwned::Base(polynomial); + let witness = params.commit(&mut prover_state, &polynomial_mle, num_coeffs); + params.prove(&mut prover_state, statement.clone(), witness, &polynomial_mle.by_ref()); + let proof = prover_state.into_proof(); + + let mut vstate = VerifierState::::new(proof, poseidon.clone()).unwrap(); + + // Trace: capture full state (all 16 elements) after each significant op. + let mut trace: Vec<(String, Vec)> = Vec::new(); + let push = |t: &mut Vec<(String, Vec)>, label: &str, st: &VerifierState| { + let s = FSVerifier::::state(st); + let after_state = s.strip_prefix("state ").unwrap_or(&s); + let elems: Vec<&str> = after_state.split(", ").take(16).collect(); + let full: Vec = elems + .iter() + .map(|e| e.split(" (").next().unwrap().parse::().unwrap_or(0)) + .collect(); + t.push((label.to_string(), full)); + }; + + push(&mut trace, "init", &vstate); + let _root = vstate.next_base_scalars_vec(8).unwrap(); + push(&mut trace, "after_root", &vstate); + let _ood_points: Vec = vstate.sample_vec(params.commitment_ood_samples); + push(&mut trace, "after_sample_ood_points", &vstate); + let _ood_answers = vstate + .next_extension_scalars_vec(params.commitment_ood_samples) + .unwrap(); + push(&mut trace, "after_read_ood_answers", &vstate); + vstate.duplex(); + push(&mut trace, "after_duplex", &vstate); + let _gamma: EF = vstate.sample(); + push(&mut trace, "after_sample_gamma", &vstate); + let mut cs = EF::ZERO; + let _poly0 = vstate.next_sumcheck_polynomial(3, cs, None).unwrap(); + push(&mut trace, "after_read_poly0", &vstate); + + #[derive(serde::Serialize)] + struct Out { + trace: Vec<(String, Vec)>, + } + let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let out = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("whir_test_vectors") + .join("state_trace.json"); + fs::write(&out, serde_json::to_string_pretty(&Out { trace: trace.clone() }).unwrap()).unwrap(); + println!("trace written to {}", out.display()); + for (k, v) in &trace { + println!(" {}: state={:?}", k, v); + } +} + +/// A tiny sanity test for the Python `Challenger`: observe a fixed +/// sequence, then sample a handful of EF challenges, and dump them. +#[test] +fn dump_challenger_vector() { + use fiat_shamir::{ChallengeSampler, FSProver}; + + let poseidon16 = default_koalabear_poseidon1_16(); + let mut prover = ProverState::::new(poseidon16); + + // Observe → sample → re-observe → sample, with an explicit duplex before + // sampling-after-sample (mirrors how the verifier interleaves). + let observed: Vec = (1u32..=20).map(F::from_u32).collect(); + prover.observe_scalars(&observed); + let s1: EF = prover.sample(); // single EF + prover.duplex(); + let s_vec3: Vec = prover.sample_vec(3); + prover.duplex(); + let s_in_range: Vec = prover.sample_in_range(20, 7); + + #[derive(serde::Serialize)] + struct Out { + observed_u32: Vec, + sample_ef: [u32; 5], + sample_ef_vec3: Vec<[u32; 5]>, + sample_in_range_20_7: Vec, + } + + let out = Out { + observed_u32: observed.iter().map(|&f| f_to_u32(f)).collect(), + sample_ef: ef_to_u32s(s1), + sample_ef_vec3: s_vec3.iter().map(|&p| ef_to_u32s(p)).collect(), + sample_in_range_20_7: s_in_range, + }; + + let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("whir_test_vectors"); + fs::create_dir_all(&out_dir).unwrap(); + let path = out_dir.join("challenger_sanity.json"); + fs::write(&path, serde_json::to_string_pretty(&out).unwrap()).unwrap(); + println!("wrote challenger sanity to {}", path.display()); +} + +#[test] +fn dump_test_vectors() { + let target_dir = + std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("whir_test_vectors"); + fs::create_dir_all(&out_dir).unwrap(); + + println!("Writing test vectors to {}", out_dir.display()); + + // `n_rounds` is 0 below `num_variables = 16` with the default builder, and + // `final_round_config()` would then panic. Keep all entries above that. + run_one("small_lir1_nv16", 16, 1, 0xdead_beef, &out_dir); + run_one("small_lir2_nv18", 18, 2, 0xfeed_face, &out_dir); + run_one("medium_lir1_nv20", 20, 1, 0xcafe_babe, &out_dir); +} From 06e1e5dda947e3fe8ff8be46aaaa1820e8882d73 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 17 May 2026 10:31:19 +0200 Subject: [PATCH 02/69] wip --- crates/lean_prover/test_zkvm.py | 77 +++++++++ crates/lean_prover/tests/dump_zkvm_vector.rs | 171 +++++++++++++++++++ crates/lean_prover/verifier.py | 136 ++++++++++++--- 3 files changed, 357 insertions(+), 27 deletions(-) create mode 100644 crates/lean_prover/test_zkvm.py create mode 100644 crates/lean_prover/tests/dump_zkvm_vector.rs diff --git a/crates/lean_prover/test_zkvm.py b/crates/lean_prover/test_zkvm.py new file mode 100644 index 000000000..1bc207d6e --- /dev/null +++ b/crates/lean_prover/test_zkvm.py @@ -0,0 +1,77 @@ +"""Run the Python `verify_execution` prologue + stacked-PCS parse against +the Rust-generated zkVM test vectors. + +Regenerate the vectors with: + cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture + +Then run: + .venv/bin/python crates/lean_prover/test_zkvm.py +""" + +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from verifier import ( # noqa: E402 + Bytecode, + Fp, + MerkleOpening, + Proof, + TableInfo, + prunedpaths_from_json, + restore_merkle_paths, + verify_execution, +) + + +def _load(path: Path): + raw = json.loads(path.read_text()) + bytecode = Bytecode( + hash=[Fp(v) for v in raw["bytecode_hash"]], + log_size=raw["bytecode_log_size"], + ) + public_input = [Fp(v) for v in raw["public_input"]] + transcript = [Fp(v) for v in raw["proof"]["transcript"]] + openings: list[MerkleOpening] = [] + for bucket in raw["proof"]["merkle_paths"]: + for r in restore_merkle_paths(prunedpaths_from_json(bucket)): + openings.append(MerkleOpening(leaf_data=r.leaf_data, path=r.sibling_hashes)) + proof = Proof(transcript=transcript, merkle_openings=openings) + tables = [TableInfo(name=t["name"], n_columns=t["n_columns"]) for t in raw["tables"]] + return bytecode, public_input, proof, tables + + +def run(path: Path) -> bool: + bytecode, public_input, proof, tables = _load(path) + try: + partial = verify_execution(bytecode, public_input, proof, tables) + except Exception as e: + print(f" {path.name}: FAILED: {type(e).__name__}: {e}") + return False + pc = partial.parsed_commitment + print( + f" {path.name}: OK " + f"log_inv_rate={partial.log_inv_rate}, log_memory={partial.log_memory}, " + f"stacked_n_vars={partial.stacked_n_vars}, " + f"table_log_heights={partial.table_log_heights}, " + f"commitment ood_points={len(pc.ood_points)}" + ) + return True + + +def main() -> int: + out_dir = Path(__file__).resolve().parents[2] / "target" / "zkvm_test_vectors" + vectors = sorted(out_dir.glob("*.json")) + if not vectors: + print(f"No test vectors in {out_dir}.") + return 1 + ok = True + for v in vectors: + ok &= run(v) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/crates/lean_prover/tests/dump_zkvm_vector.rs b/crates/lean_prover/tests/dump_zkvm_vector.rs new file mode 100644 index 000000000..77c1a99fa --- /dev/null +++ b/crates/lean_prover/tests/dump_zkvm_vector.rs @@ -0,0 +1,171 @@ +//! Dumps a tiny zkVM proof + metadata so the Python `verify_execution` +//! port (`crates/lean_prover/verifier.py`) can run against it. +//! +//! Run: +//! cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture +//! +//! Output: `target/zkvm_test_vectors/small.json`. The JSON contains everything +//! Python needs to mirror `verify_execution.rs` up to (and through) any +//! sub-protocol we've ported so far. + +use std::fs; +use std::path::PathBuf; + +use backend::{PrimeField32, PrunedMerklePaths, WhirConfigBuilder}; +use lean_compiler::*; +use lean_prover::{default_whir_config, prove_execution::prove_execution}; +use lean_vm::*; +use serde::Serialize; + +type F = lean_vm::F; + +const DIGEST_ELEMS: usize = 8; + +fn f_to_u32(x: F) -> u32 { + x.as_canonical_u32() +} + +#[derive(Serialize)] +struct PrunedPathJson { + leaf_index: usize, + siblings: Vec<[u32; DIGEST_ELEMS]>, +} + +#[derive(Serialize)] +struct PrunedMerklePathsJson { + merkle_height: usize, + original_order: Vec, + leaf_data: Vec>, + paths: Vec, + n_trailing_zeros: usize, +} + +#[derive(Serialize)] +struct ProofJson { + transcript: Vec, + merkle_paths: Vec, +} + +#[derive(Serialize)] +struct BuilderJson { + security_level: usize, + max_num_variables_to_send_coeffs: usize, + pow_bits: usize, + folding_factor_first: usize, + folding_factor_subsequent: usize, + starting_log_inv_rate: usize, + rs_domain_initial_reduction_factor: usize, + soundness_type: &'static str, +} + +#[derive(Serialize)] +struct TableInfoJson { + name: &'static str, + n_columns: usize, +} + +#[derive(Serialize)] +struct OutJson { + name: String, + bytecode_log_size: usize, + bytecode_hash: [u32; DIGEST_ELEMS], + public_input: Vec, + n_tables: usize, + tables: Vec, + snark_domain_sep: [u32; DIGEST_ELEMS], + builder: BuilderJson, + proof: ProofJson, +} + +fn convert_pruned(p: &PrunedMerklePaths) -> PrunedMerklePathsJson { + PrunedMerklePathsJson { + merkle_height: p.merkle_height, + original_order: p.original_order.clone(), + leaf_data: p + .leaf_data + .iter() + .map(|v| v.iter().map(|&f| f_to_u32(f)).collect()) + .collect(), + paths: p + .paths + .iter() + .map(|(idx, siblings)| PrunedPathJson { + leaf_index: *idx, + siblings: siblings.iter().map(|d| d.map(f_to_u32)).collect(), + }) + .collect(), + n_trailing_zeros: p.n_trailing_zeros, + } +} + +fn dump_one(name: &str, program_str: &str, public_input: Vec, out_dir: &PathBuf) { + let bytecode = compile_program(&ProgramSource::Raw(program_str.to_string())); + let starting_log_inv_rate = 1; + let builder: WhirConfigBuilder = default_whir_config(starting_log_inv_rate); + let witness = ExecutionWitness::default(); + let exec_proof = prove_execution(&bytecode, &public_input, &witness, &builder, false) + .expect("prove_execution failed"); + + let table_infos: Vec = ALL_TABLES + .iter() + .map(|t| TableInfoJson { + name: t.name(), + n_columns: ::n_columns(t), + }) + .collect(); + + let out = OutJson { + name: name.to_string(), + bytecode_log_size: bytecode.log_size(), + bytecode_hash: bytecode.hash.map(f_to_u32), + public_input: public_input.iter().map(|&f| f_to_u32(f)).collect(), + n_tables: N_TABLES, + tables: table_infos, + snark_domain_sep: lean_prover::SNARK_DOMAIN_SEP.map(f_to_u32), + builder: BuilderJson { + security_level: 124, + max_num_variables_to_send_coeffs: 8, + pow_bits: 16, + folding_factor_first: 7, + folding_factor_subsequent: 5, + starting_log_inv_rate, + rs_domain_initial_reduction_factor: 5, + soundness_type: "JohnsonBound", + }, + proof: ProofJson { + transcript: exec_proof.proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), + merkle_paths: exec_proof.proof.merkle_paths.iter().map(convert_pruned).collect(), + }, + }; + + let path = out_dir.join(format!("{name}.json")); + fs::write(&path, serde_json::to_string(&out).unwrap()).unwrap(); + println!( + "{} -> {} ({:.1} KiB; bytecode_log_size={}, transcript_len={})", + name, + path.display(), + path.metadata().unwrap().len() as f64 / 1024.0, + out.bytecode_log_size, + out.proof.transcript.len(), + ); +} + +#[test] +fn dump_zkvm_vector() { + let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("zkvm_test_vectors"); + fs::create_dir_all(&out_dir).unwrap(); + + // The smallest legal program. Empty public input. + let small_program = r#" +def main(): + a = Array(1) + for i in unroll(0, 2**17): + a[0] = 1 * 2 + return +"#; + dump_one("small", small_program, vec![], &out_dir); +} diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 24fecb150..fb1da92f0 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1234,12 +1234,67 @@ def whir_verify( # --------------------------------------------------------------------------- -# Stubs still pending for the lean_prover verifier +# Stacked PCS — port of sub_protocols/stacked_pcs.rs # --------------------------------------------------------------------------- -def stacked_pcs_parse_commitment(*args, **kwargs): - raise NotImplementedError("stacked_pcs_parse_commitment: port from sub_protocols/stacked_pcs.rs") +def compute_stacked_n_vars( + log_memory: int, + log_bytecode: int, + table_log_heights: dict[str, int], + table_n_columns: dict[str, int], +) -> int: + """Mirror of `stacked_pcs::compute_stacked_n_vars`. + + The stacked polynomial concatenates: + - 2 copies of memory -> 2 * 2^log_memory + - one bytecode accumulator padded -> 2^max(log_bytecode, max_table_log_n_rows) + - per table: n_columns * 2^log_n_rows + """ + max_table_log_n_rows = max(table_log_heights.values()) + total_len = (2 << log_memory) + ( + 1 << max(log_bytecode, max_table_log_n_rows) + ) + for name, log_n_rows in table_log_heights.items(): + total_len += table_n_columns[name] << log_n_rows + return log2_ceil_usize(total_len) + + +def stacked_pcs_parse_commitment( + state: VerifierState, + log_inv_rate: int, + log_memory: int, + log_bytecode: int, + table_log_heights: dict[str, int], + table_n_columns: dict[str, int], + execution_table_name: str = "execution", +) -> ParsedCommitment: + """Port of `stacked_pcs_parse_commitment`. + + - Memory must be at least as wide as the execution table. + - The execution table must be the tallest table. + - The stacked-poly size must fit within the WHIR domain bound. + The actual commitment parsing is then delegated to `parsed_commitment_parse`. + """ + exec_log = table_log_heights[execution_table_name] + if log_memory < exec_log or exec_log < max(table_log_heights.values()): + raise ProofError("InvalidProof: memory or execution table size invariants broken") + + stacked_n_vars = compute_stacked_n_vars( + log_memory, log_bytecode, table_log_heights, table_n_columns + ) + # `WhirConfig::new` asserts stacked_n_vars + log_inv_rate - first_round <= F::TWO_ADICITY. + max_nv = BASE_TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate + if stacked_n_vars > max_nv: + raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") + + cfg = whir_config(log_inv_rate, stacked_n_vars) + return parsed_commitment_parse(state, stacked_n_vars, cfg.commitment_ood_samples) + + +# --------------------------------------------------------------------------- +# Stubs still pending for the lean_prover verifier +# --------------------------------------------------------------------------- def verify_generic_logup(*args, **kwargs): @@ -1260,29 +1315,50 @@ class ProofVerificationDetails: bytecode_evaluation: object # Evaluation — TODO +@dataclass(frozen=True) +class TableInfo: + """Minimal table metadata the verifier needs.""" + + name: str + n_columns: int + + +@dataclass +class VerifyExecutionPartial: + """What we can produce so far — extended as we port more sub-protocols.""" + + log_inv_rate: int + log_memory: int + public_input_len: int + table_log_heights: dict[str, int] + stacked_n_vars: int + parsed_commitment: ParsedCommitment + + def verify_execution( bytecode: Bytecode, public_input: Sequence[Fp], proof: Proof, - n_tables: int, -) -> ProofVerificationDetails: + tables: Sequence[TableInfo], +) -> VerifyExecutionPartial: """Port of `verify_execution` (lean_prover/src/verify_execution.rs). - Implements the prologue (dim/bound checks, transcript priming); calls into - stubs for the heavy sub-protocols. + Currently runs the prologue (dim/bound checks, transcript priming) and + parses the stacked-PCS WHIR commitment. Sub-protocols (logup, AIR sumcheck, + WHIR final verify) remain `NotImplementedError`. - NOTE: `n_tables` (= N_TABLES in lean_vm) is passed in until we port the - table enum here; same for the per-table size limits. + `tables` must be in canonical Rust order (`ALL_TABLES`) — `execution` + first, then `extension_op`, `poseidon16` — because the verifier reads + per-table `log_n_rows` in that same order from the transcript. """ state = VerifierState(proof) state.observe_scalars(list(public_input)) state.observe_scalars(poseidon16_compress_pair(bytecode.hash, SNARK_DOMAIN_SEP)) + n_tables = len(tables) dims = [int(x.value) for x in state.next_base_scalars_vec(3 + n_tables)] - log_inv_rate = dims[0] - log_memory = dims[1] - public_input_len = dims[2] + log_inv_rate, log_memory, public_input_len = dims[0], dims[1], dims[2] table_log_n_rows = dims[3 : 3 + n_tables] if public_input_len != len(public_input): @@ -1306,17 +1382,25 @@ def verify_execution( if bytecode.log_size < MIN_BYTECODE_LOG_SIZE: raise ProofError("InvalidProof: bytecode too small") - public_memory = padd_with_zero_to_next_power_of_two(public_input) # noqa: F841 (used once WHIR is wired) - - # ------------- below: not implemented yet ----------------- - # parsed_commitment = stacked_pcs_parse_commitment(state, log_memory, bytecode.log_size, table_log_n_rows) - # logup_c = state.sample() - # logup_alphas = state.sample_vec(log2_ceil_usize(max_bus_width)) - # logup_statements = verify_generic_logup(state, logup_c, logup_alphas, log_memory, ...) - # bus_beta = state.sample(); air_alpha = state.sample(); eta = state.sample() - # ... sumcheck_verify(...), per-table AIR eval, whir_verify(...) ... - raise NotImplementedError( - "verify_execution: sub-protocols (WHIR/logup/sumcheck/AIR) are not yet ported." + table_log_heights = {t.name: log_n_rows for t, log_n_rows in zip(tables, table_log_n_rows)} + table_n_columns = {t.name: t.n_columns for t in tables} + + parsed_commitment = stacked_pcs_parse_commitment( + state, + log_inv_rate=log_inv_rate, + log_memory=log_memory, + log_bytecode=bytecode.log_size, + table_log_heights=table_log_heights, + table_n_columns=table_n_columns, + ) + + return VerifyExecutionPartial( + log_inv_rate=log_inv_rate, + log_memory=log_memory, + public_input_len=public_input_len, + table_log_heights=table_log_heights, + stacked_n_vars=parsed_commitment.num_variables, + parsed_commitment=parsed_commitment, ) @@ -1371,13 +1455,11 @@ def _smoke() -> None: chal = st.sample() print(f"VerifierState sample = {chal}") - # verify_execution prologue runs but bails at the first sub-protocol stub. + # verify_execution: dummy proof should hit a bound check, not crash. bc = Bytecode(hash=[Fp(0)] * 8, log_size=10) bad_proof = Proof(transcript=[Fp(0)] * 64) try: - verify_execution(bc, [Fp(0)] * 4, bad_proof, n_tables=0) - except NotImplementedError as e: - print(f"verify_execution prologue reached sub-protocol stub: {e}") + verify_execution(bc, [Fp(0)] * 4, bad_proof, tables=[]) except ProofError as e: print(f"verify_execution failed bound check (expected with dummy proof): {e}") From 9e04b09fb312e84dc7b4fb1d08a77a1d71e04d71 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 17 May 2026 11:06:25 +0200 Subject: [PATCH 03/69] wip --- crates/lean_prover/test_gkr.py | 91 ++++++++ crates/lean_prover/tests/dump_gkr_vector.rs | 188 +++++++++++++++++ crates/lean_prover/verifier.py | 181 +++++++++++++--- crates/whir/tests/dump_test_vectors.rs | 219 -------------------- 4 files changed, 427 insertions(+), 252 deletions(-) create mode 100644 crates/lean_prover/test_gkr.py create mode 100644 crates/lean_prover/tests/dump_gkr_vector.rs diff --git a/crates/lean_prover/test_gkr.py b/crates/lean_prover/test_gkr.py new file mode 100644 index 000000000..5af87de52 --- /dev/null +++ b/crates/lean_prover/test_gkr.py @@ -0,0 +1,91 @@ +"""Validate the Python `verify_gkr_quotient` against the Rust dump. + +Regenerate the vectors with: + cargo test --release -p lean_prover --test dump_gkr_vector -- --nocapture + +Then run: + .venv/bin/python crates/lean_prover/test_gkr.py +""" + +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from verifier import ( # noqa: E402 + EF, + Fp, + MerkleOpening, + Proof, + VerifierState, + prunedpaths_from_json, + restore_merkle_paths, + verify_gkr_quotient, +) + + +def _ef(coords) -> EF: + return EF([Fp(v) for v in coords]) + + +def run(path: Path) -> bool: + raw = json.loads(path.read_text()) + transcript = [Fp(v) for v in raw["proof"]["transcript"]] + openings: list[MerkleOpening] = [] + for bucket in raw["proof"]["merkle_paths"]: + for r in restore_merkle_paths(prunedpaths_from_json(bucket)): + openings.append(MerkleOpening(leaf_data=r.leaf_data, path=r.sibling_hashes)) + proof = Proof(transcript=transcript, merkle_openings=openings) + + state = VerifierState(proof) + try: + quotient, point, claims_num, claims_den = verify_gkr_quotient(state, raw["n_vars"]) + except Exception as e: + print(f" {path.name}: FAILED: {type(e).__name__}: {e}") + return False + + expected_quotient = _ef(raw["expected_quotient"]) + expected_point = [_ef(p) for p in raw["expected_point"]] + expected_num = _ef(raw["expected_claims_num"]) + expected_den = _ef(raw["expected_claims_den"]) + + ok = True + if quotient != expected_quotient: + ok = False + print(f" {path.name}: quotient mismatch") + if len(point) != len(expected_point): + ok = False + print(f" {path.name}: gkr_point length mismatch ({len(point)} vs {len(expected_point)})") + else: + for i, (a, b) in enumerate(zip(point, expected_point)): + if a != b: + ok = False + print(f" {path.name}: gkr_point[{i}] mismatch") + break + if claims_num != expected_num: + ok = False + print(f" {path.name}: claims_num mismatch") + if claims_den != expected_den: + ok = False + print(f" {path.name}: claims_den mismatch") + + if ok: + print(f" {path.name}: OK") + return ok + + +def main() -> int: + out_dir = Path(__file__).resolve().parents[2] / "target" / "gkr_test_vectors" + vectors = sorted(out_dir.glob("*.json")) + if not vectors: + print(f"No test vectors in {out_dir}.") + return 1 + ok = True + for v in vectors: + ok &= run(v) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/crates/lean_prover/tests/dump_gkr_vector.rs b/crates/lean_prover/tests/dump_gkr_vector.rs new file mode 100644 index 000000000..18a0840c4 --- /dev/null +++ b/crates/lean_prover/tests/dump_gkr_vector.rs @@ -0,0 +1,188 @@ +//! Focused test vector for the GKR-quotient sub-protocol. Independent of the +//! rest of the zkVM verifier so the Python port can be validated in isolation. +//! +//! Run: +//! cargo test --release -p lean_prover --test dump_gkr_vector -- --nocapture + +use std::fs; +use std::path::PathBuf; + +use backend::{ + BasedVectorSpace, Field, MleOwned, PackedValue, PrimeCharacteristicRing, PrimeField32, + ProverState, PrunedMerklePaths, VerifierState, default_koalabear_poseidon1_16, pack_extension, + packing_log_width, +}; +use lean_vm::{EF, F}; +use rand::{RngExt, SeedableRng, rngs::StdRng}; +use serde::Serialize; +use sub_protocols::{ENDIANNESS_PIVOT_GKR, prove_gkr_quotient, verify_gkr_quotient}; + +const DIGEST_ELEMS: usize = 8; + +fn f_to_u32(x: F) -> u32 { + x.as_canonical_u32() +} + +fn ef_to_u32s(x: EF) -> [u32; 5] { + let coords: &[F] = x.as_basis_coefficients_slice(); + [ + f_to_u32(coords[0]), + f_to_u32(coords[1]), + f_to_u32(coords[2]), + f_to_u32(coords[3]), + f_to_u32(coords[4]), + ] +} + +#[derive(Serialize)] +struct PrunedPathJson { + leaf_index: usize, + siblings: Vec<[u32; DIGEST_ELEMS]>, +} + +#[derive(Serialize)] +struct PrunedMerklePathsJson { + merkle_height: usize, + original_order: Vec, + leaf_data: Vec>, + paths: Vec, + n_trailing_zeros: usize, +} + +#[derive(Serialize)] +struct ProofJson { + transcript: Vec, + merkle_paths: Vec, +} + +#[derive(Serialize)] +struct GkrOut { + name: String, + n_vars: usize, + expected_quotient: [u32; 5], + expected_point: Vec<[u32; 5]>, + expected_claims_num: [u32; 5], + expected_claims_den: [u32; 5], + proof: ProofJson, +} + +fn convert_pruned(p: &PrunedMerklePaths) -> PrunedMerklePathsJson { + PrunedMerklePathsJson { + merkle_height: p.merkle_height, + original_order: p.original_order.clone(), + leaf_data: p + .leaf_data + .iter() + .map(|v| v.iter().map(|&f| f_to_u32(f)).collect()) + .collect(), + paths: p + .paths + .iter() + .map(|(idx, siblings)| PrunedPathJson { + leaf_index: *idx, + siblings: siblings.iter().map(|d| d.map(f_to_u32)).collect(), + }) + .collect(), + n_trailing_zeros: p.n_trailing_zeros, + } +} + +/// Copy of the helper used in the existing GKR test (bit-reverse the input +/// chunks at `chunk_log`). +fn bit_reverse_chunks(v: &[T], chunk_log: usize) -> Vec { + let chunk = 1 << chunk_log; + let shift = (usize::BITS as usize) - chunk_log; + let mut out = Vec::with_capacity(v.len()); + for chunk_start in (0..v.len()).step_by(chunk) { + for i in 0..chunk { + out.push(v[chunk_start + (i.reverse_bits() >> shift)]); + } + } + out +} + +fn run_one(name: &str, log_n: usize, seed: u64, out_dir: &PathBuf) { + let poseidon16 = default_koalabear_poseidon1_16(); + let pivot = ENDIANNESS_PIVOT_GKR.min(log_n); + let n = 1usize << log_n; + + let mut rng = StdRng::seed_from_u64(seed); + let c: EF = rng.random(); + let numerators_raw: Vec = (0..n).map(|_| rng.random()).collect(); + let denominators_raw: Vec = (0..n).map(|_| c - F::from_usize(rng.random_range(..n))).collect(); + + // Bit-reverse + pack the inputs at `pivot` (prover convention). + let w = packing_log_width::(); + let nums_br = { + let mut br = vec![F::ZERO; n]; + let chunk = 1 << pivot; + let shift = (usize::BITS as usize) - pivot; + for c_start in (0..n).step_by(chunk) { + for i in 0..chunk { + br[c_start + i] = numerators_raw[c_start + (i.reverse_bits() >> shift)]; + } + } + use backend::PFPacking; + let packed: Vec> = PFPacking::::pack_slice(&br).to_vec(); + packed + }; + let dens_br: Vec> = pack_extension(&bit_reverse_chunks(&denominators_raw, pivot)); + let _ = w; + + let mut prover_state = ProverState::::new(poseidon16.clone()); + let (quotient_prover, claim_point_prover) = + prove_gkr_quotient::(&mut prover_state, &nums_br, &dens_br, pivot); + let proof = prover_state.into_proof(); + + let mut verifier_state = VerifierState::::new(proof.clone(), poseidon16).unwrap(); + let (retrieved_quotient, claim_point, claim_num, claim_den) = + verify_gkr_quotient::(&mut verifier_state, log_n).unwrap(); + assert_eq!(quotient_prover, retrieved_quotient); + assert_eq!(claim_point_prover, claim_point); + + // sanity: evaluate the raw inputs at claim_point and check they match. + let nums_nat = MleOwned::::Base(numerators_raw.clone()); + let dens_nat = MleOwned::::Extension(denominators_raw.clone()); + assert_eq!(nums_nat.evaluate(&claim_point), claim_num); + assert_eq!(dens_nat.evaluate(&claim_point), claim_den); + + let out = GkrOut { + name: name.to_string(), + n_vars: log_n, + expected_quotient: ef_to_u32s(retrieved_quotient), + expected_point: claim_point.0.iter().map(|&p| ef_to_u32s(p)).collect(), + expected_claims_num: ef_to_u32s(claim_num), + expected_claims_den: ef_to_u32s(claim_den), + proof: ProofJson { + transcript: proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), + merkle_paths: proof.merkle_paths.iter().map(convert_pruned).collect(), + }, + }; + + let path = out_dir.join(format!("{name}.json")); + fs::write(&path, serde_json::to_string(&out).unwrap()).unwrap(); + println!( + "{} -> {} ({:.1} KiB) [n_vars={}, transcript_len={}]", + name, + path.display(), + path.metadata().unwrap().len() as f64 / 1024.0, + log_n, + out.proof.transcript.len(), + ); +} + +#[test] +fn dump_gkr_vector() { + let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("gkr_test_vectors"); + fs::create_dir_all(&out_dir).unwrap(); + + // n_vars must exceed N_VARS_TO_SEND_GKR_COEFFS=5, and `prove_gkr_quotient` + // requires `pivot > w` (w = packing_log_width). pivot is capped at + // ENDIANNESS_PIVOT_GKR = 12, but for small inputs it shrinks to log_n — + // make sure to stay above the packing width. + run_one("small_nv13", 13, 0xdead_beef, &out_dir); +} diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index fb1da92f0..2d7c99df2 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -19,7 +19,11 @@ WHIR_SUBSEQUENT_FOLDING_FACTOR = 5 RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 -# Poseidon16 duplex sponge parameters (challenger.rs). +# Poseidon16 challenger parameters (challenger.rs). +# Note: this branch uses the older "compression with domain separator" design. +# The state is just the RATE-sized output of the last permute; sampling pulls +# fresh hashes by re-permuting with a per-call domain separator. There is no +# `rate_fresh` flag and no `duplex` call. RATE = 8 WIDTH = 16 CAPACITY = WIDTH - RATE @@ -216,22 +220,22 @@ def hash_slice(data: Sequence[Fp]) -> list[Fp]: class Challenger: - """Poseidon16 duplex sponge. + """Poseidon16 challenger (old "compression + domain-separator" design). - Mirrors `fiat_shamir::challenger`: state is a length-WIDTH array, the rate - portion lives in `state[CAPACITY..]`. `observe` overwrites the rate, then - permutes. `sample` reads the rate and asserts `rate_fresh`. + Mirrors `fiat_shamir::challenger` on this branch: + - `state` is a length-RATE buffer (8 elements). + - `observe(value)`: `state = permute(state || value)[:RATE]`. + - `sample_many(n)`: hash `(domain_sep_i || state)` for `i ∈ 0..=n`; + return the first `n`, advance `state` to the last one. """ def __init__(self) -> None: - self.state: list[Fp] = [Fp(0)] * WIDTH - self.rate_fresh: bool = False + self.state: list[Fp] = [Fp(0)] * RATE def observe(self, value: Sequence[Fp]) -> None: assert len(value) == RATE - self.state = self.state[:CAPACITY] + list(value) - self.state = poseidon16_permute(self.state) - self.rate_fresh = True + out = poseidon16_compress_in_place(list(self.state) + list(value)) + self.state = out[:RATE] def observe_many(self, scalars: Sequence[Fp]) -> None: for i in range(0, len(scalars), RATE): @@ -240,23 +244,22 @@ def observe_many(self, scalars: Sequence[Fp]) -> None: chunk = chunk + [Fp(0)] * (RATE - len(chunk)) self.observe(chunk) - def duplex(self) -> None: - self.observe([Fp(0)] * RATE) - - def sample(self) -> list[Fp]: - assert self.rate_fresh, "stale rate — insert a duplex() before sampling" - out = list(self.state[CAPACITY:]) - self.rate_fresh = False - return out + # Alias matching `Challenger::observe_scalars` on this branch. + observe_scalars = observe_many def sample_many(self, n: int) -> list[list[Fp]]: - if n == 0: - return [] - out = [self.sample()] - for _ in range(1, n): - self.duplex() - out.append(self.sample()) - return out + sampled: list[list[Fp]] = [] + last: list[Fp] | None = None + for i in range(n + 1): + domain_sep = [Fp(i)] + [Fp(0)] * (RATE - 1) + hashed = poseidon16_compress_in_place(domain_sep + list(self.state))[:RATE] + if i < n: + sampled.append(hashed) + else: + last = hashed + if last is not None: + self.state = last + return sampled def sample_ef_vec(self, n: int) -> list[EF]: """Mirrors utils::sample_vec — pulls ceil(n*5/8) blocks, takes first n*5.""" @@ -338,7 +341,9 @@ def observe_scalars(self, scalars: Sequence[Fp]) -> None: self.challenger.observe_many(list(scalars)) def duplex(self) -> None: - self.challenger.duplex() + """No-op on this branch — the older challenger has no rate-staleness + notion, so duplex calls in the WHIR verifier are simply skipped.""" + pass def next_base_scalars_vec(self, n: int) -> list[Fp]: scalars = self._read(n) @@ -373,9 +378,9 @@ def check_pow_grinding(self, bits: int) -> None: return witness = self._read(1) self.challenger.observe_many(witness) - # Rust now reads state[CAPACITY] (= state[8], i.e. the first element of - # the rate portion) after the absorb-permute. - if int(self.challenger.state[CAPACITY].value) & ((1 << bits) - 1) != 0: + # OLD challenger: state is the RATE-sized output of the last permute; + # grinding checks state[0]. + if int(self.challenger.state[0].value) & ((1 << bits) - 1) != 0: raise ProofError("InvalidGrindingWitness") self.raw_transcript.append(witness[0]) self.raw_transcript.extend([Fp(0)] * (RATE - 1)) @@ -1292,6 +1297,120 @@ def stacked_pcs_parse_commitment( return parsed_commitment_parse(state, stacked_n_vars, cfg.commitment_ood_samples) +# --------------------------------------------------------------------------- +# Generic sumcheck verifier (port of `backend/sumcheck/src/verify.rs`) +# --------------------------------------------------------------------------- + + +@dataclass +class Evaluation: + """Pair (point, value): claim that a multilinear evaluates to `value` at + `point`. Mirrors `poly::Evaluation`. + """ + + point: list[EF] + value: EF + + +def sumcheck_verify( + state: VerifierState, + n_vars: int, + degree: int, + expected_sum: EF, + eq_alphas: Sequence[EF] | None, +) -> Evaluation: + """Mirror of `sumcheck::sumcheck_verify`. + + Reads `n_vars` round polynomials, each of `degree + 1` coefficients (so the + bare polynomial is degree-`degree`; in the `eq_alphas` path the verifier + extracts the bare poly and re-expands with `eq(α_round, X)`). + Returns the final point + claimed value. + """ + target = expected_sum + challenges: list[EF] = [] + for round_idx in range(n_vars): + eq_alpha = eq_alphas[round_idx] if eq_alphas is not None else None + coeffs = state.next_sumcheck_polynomial(degree + 1, target, eq_alpha) + r = state.sample() + challenges.append(r) + target = _eval_univariate(coeffs, r) + return Evaluation(point=challenges, value=target) + + +# --------------------------------------------------------------------------- +# GKR-quotient verifier (port of `sub_protocols::quotient_gkr`) +# +# Verifies the protocol `Σ nᵢ/dᵢ` via a layered sumcheck. +# --------------------------------------------------------------------------- + + +N_VARS_TO_SEND_GKR_COEFFS = 5 + + +def verify_gkr_quotient( + state: VerifierState, + n_vars: int, +) -> tuple[EF, list[EF], EF, EF]: + """Mirror of `verify_gkr_quotient`. Returns + `(quotient, gkr_point, claims_num, claims_den)`. + """ + assert n_vars > N_VARS_TO_SEND_GKR_COEFFS + send_len = 1 << N_VARS_TO_SEND_GKR_COEFFS + + last_nums = state.next_extension_scalars_vec(send_len) + last_dens = state.next_extension_scalars_vec(send_len) + quotient = _quotient_sum(last_nums, last_dens) + + point: list[EF] = state.sample_vec(N_VARS_TO_SEND_GKR_COEFFS) + claims_num = eval_multilinear_evals(last_nums, point) + claims_den = eval_multilinear_evals(last_dens, point) + + for i in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): + point, claims_num, claims_den = _verify_gkr_quotient_step( + state, i, point, claims_num, claims_den + ) + return quotient, point, claims_num, claims_den + + +def _verify_gkr_quotient_step( + state: VerifierState, + n_vars: int, + point: list[EF], + claims_num: EF, + claims_den: EF, +) -> tuple[list[EF], EF, EF]: + alpha = state.sample() + expected_sum = claims_num + alpha * claims_den + eq_alphas_rev = list(reversed(point)) + postponed = sumcheck_verify(state, n_vars, 3, expected_sum, eq_alphas_rev) + # Rust: postponed.point.0.reverse(); + postponed_point = list(reversed(postponed.point)) + inner_evals = state.next_extension_scalars_vec(4) + + # constraints_eval = α · n_r · d_r + (n_l · d_r + n_r · d_l) + constraints_eval = ( + alpha * inner_evals[2] * inner_evals[3] + + (inner_evals[0] * inner_evals[3] + inner_evals[1] * inner_evals[2]) + ) + + if postponed.value != eq_poly_outside(point, postponed_point) * constraints_eval: + raise ProofError("GKR step: postponed value mismatch") + + beta = state.sample() + one_minus_beta = EF.one() - beta + next_num = one_minus_beta * inner_evals[0] + beta * inner_evals[1] + next_den = one_minus_beta * inner_evals[2] + beta * inner_evals[3] + next_point = postponed_point + [beta] + return next_point, next_num, next_den + + +def _quotient_sum(nums: Sequence[EF], dens: Sequence[EF]) -> EF: + acc = EF.zero() + for n, d in zip(nums, dens): + acc = acc + n * d.inv() + return acc + + # --------------------------------------------------------------------------- # Stubs still pending for the lean_prover verifier # --------------------------------------------------------------------------- @@ -1301,10 +1420,6 @@ def verify_generic_logup(*args, **kwargs): raise NotImplementedError("verify_generic_logup: port from sub_protocols/logup.rs") -def sumcheck_verify(*args, **kwargs): - raise NotImplementedError("sumcheck_verify: port from sub_protocols/air_sumcheck.rs (or sumcheck crate)") - - # --------------------------------------------------------------------------- # Top-level verifier (skeleton) # --------------------------------------------------------------------------- diff --git a/crates/whir/tests/dump_test_vectors.rs b/crates/whir/tests/dump_test_vectors.rs index 54878f65c..8649bb7a2 100644 --- a/crates/whir/tests/dump_test_vectors.rs +++ b/crates/whir/tests/dump_test_vectors.rs @@ -328,89 +328,6 @@ fn dump_merkle_vector() { println!("wrote merkle sanity to {}", path.display()); } -/// Replay verifier on a previously-dumped test vector to see if the saved -/// witness still satisfies the grinding check. -#[test] -fn replay_dumped_vector() { - use fiat_shamir::{ChallengeSampler, FSVerifier}; - use std::fs; - - let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("whir_test_vectors") - .join("small_lir1_nv16.json"); - let text = fs::read_to_string(&path).unwrap(); - let v: serde_json::Value = serde_json::from_str(&text).unwrap(); - let transcript_u32: Vec = v["proof"]["transcript"] - .as_array().unwrap().iter() - .map(|x| x.as_u64().unwrap() as u32) - .collect(); - let transcript: Vec = transcript_u32.iter().map(|&u| F::from_u32(u)).collect(); - let merkle_paths_json = v["proof"]["merkle_paths"].clone(); - use fiat_shamir::PrunedMerklePaths; - let merkle_paths: Vec> = merkle_paths_json - .as_array().unwrap().iter() - .map(|bucket| { - // Manually construct PrunedMerklePaths from JSON. - let merkle_height = bucket["merkle_height"].as_u64().unwrap() as usize; - let original_order: Vec = bucket["original_order"].as_array().unwrap().iter() - .map(|x| x.as_u64().unwrap() as usize).collect(); - let leaf_data: Vec> = bucket["leaf_data"].as_array().unwrap().iter() - .map(|c| c.as_array().unwrap().iter().map(|x| F::from_u32(x.as_u64().unwrap() as u32)).collect()) - .collect(); - let paths: Vec<(usize, Vec<[F; 8]>)> = bucket["paths"].as_array().unwrap().iter() - .map(|p| { - let leaf_index = p["leaf_index"].as_u64().unwrap() as usize; - let siblings: Vec<[F; 8]> = p["siblings"].as_array().unwrap().iter() - .map(|s| { - let arr: Vec = s.as_array().unwrap().iter().map(|x| F::from_u32(x.as_u64().unwrap() as u32)).collect(); - <[F; 8]>::try_from(arr).unwrap() - }).collect(); - (leaf_index, siblings) - }).collect(); - let n_trailing_zeros = bucket["n_trailing_zeros"].as_u64().unwrap() as usize; - PrunedMerklePaths { merkle_height, original_order, leaf_data, paths, n_trailing_zeros } - }).collect(); - let proof = fiat_shamir::Proof { transcript: transcript.clone(), merkle_paths }; - - let poseidon = default_koalabear_poseidon1_16(); - let mut vstate = VerifierState::::new(proof, poseidon).unwrap(); - - // Build the verifier WhirConfig and use its commitment_ood_samples count. - let builder = WhirConfigBuilder { - security_level: 124, - max_num_variables_to_send_coeffs: 8, - pow_bits: 16, - folding_factor: FoldingFactor::new(7, 5), - soundness_type: SecurityAssumption::JohnsonBound, - starting_log_inv_rate: 1, - rs_domain_initial_reduction_factor: 5, - }; - let params = WhirConfig::::new(&builder, 16); - println!("Rust params.commitment_ood_samples = {}", params.commitment_ood_samples); - println!("Rust params.starting_folding_pow_bits = {}", params.starting_folding_pow_bits); - let ood_n = params.commitment_ood_samples; - - // Walk to the first grinding check - let _root = vstate.next_base_scalars_vec(8).unwrap(); - let _ood_pts: Vec = vstate.sample_vec(ood_n); - let _ood_ans = vstate.next_extension_scalars_vec(ood_n).unwrap(); - vstate.duplex(); - let _g: EF = vstate.sample(); - let _poly0 = vstate.next_sumcheck_polynomial(3, EF::ZERO, None).unwrap(); - println!("witness u32 at transcript[23]: {}", transcript_u32[23]); - let reconstructed = F::from_u32(transcript_u32[23]); - println!("F::from_u32({}).as_canonical_u32() = {}", transcript_u32[23], reconstructed.as_canonical_u32()); - println!("transcript F repr (Debug): {:?}", transcript[23]); - println!("reconstructed F repr (Debug):{:?}", reconstructed); - match vstate.check_pow_grinding(16) { - Ok(()) => println!("Rust verifier accepts the saved witness!"), - Err(e) => println!("Rust verifier ALSO fails: {:?}", e), - } -} - /// Cross-check that Python's Poseidon16 permutation matches Rust's on a /// specific input. #[test] @@ -447,142 +364,6 @@ fn dump_permute_oracle() { println!("output: {:?}", out.output); } -/// Dump Rust's challenger state at every observe/sample for one test vector, -/// so we can diff vs Python. -#[test] -fn dump_state_trace() { - use fiat_shamir::{ChallengeSampler, FSProver, FSVerifier}; - - let poseidon = default_koalabear_poseidon1_16(); - let log_inv_rate = 1; - let num_variables = 16; - let builder = WhirConfigBuilder { - security_level: 124, - max_num_variables_to_send_coeffs: 8, - pow_bits: 16, - folding_factor: FoldingFactor::new(7, 5), - soundness_type: SecurityAssumption::JohnsonBound, - starting_log_inv_rate: log_inv_rate, - rs_domain_initial_reduction_factor: 5, - }; - let params = WhirConfig::::new(&builder, num_variables); - - let mut rng = StdRng::seed_from_u64(0xdead_beef); - let num_coeffs = 1usize << num_variables; - let polynomial: Vec = (0..num_coeffs).map(|_| rng.random::()).collect(); - let dense_point: Vec = (0..num_variables).map(|_| rng.random()).collect(); - let dense_value = polynomial.evaluate_sparse(0, &MultilinearPoint(dense_point.clone())); - let statement = vec![SparseStatement::new( - num_variables, - MultilinearPoint(dense_point.clone()), - vec![SparseValue { - selector: 0, - value: dense_value, - }], - )]; - - precompute_dft_twiddles::(1 << F::TWO_ADICITY); - - let mut prover_state = ProverState::::new(poseidon.clone()); - let polynomial_mle: MleOwned = MleOwned::Base(polynomial); - let witness = params.commit(&mut prover_state, &polynomial_mle, num_coeffs); - params.prove(&mut prover_state, statement.clone(), witness, &polynomial_mle.by_ref()); - let proof = prover_state.into_proof(); - - let mut vstate = VerifierState::::new(proof, poseidon.clone()).unwrap(); - - // Trace: capture full state (all 16 elements) after each significant op. - let mut trace: Vec<(String, Vec)> = Vec::new(); - let push = |t: &mut Vec<(String, Vec)>, label: &str, st: &VerifierState| { - let s = FSVerifier::::state(st); - let after_state = s.strip_prefix("state ").unwrap_or(&s); - let elems: Vec<&str> = after_state.split(", ").take(16).collect(); - let full: Vec = elems - .iter() - .map(|e| e.split(" (").next().unwrap().parse::().unwrap_or(0)) - .collect(); - t.push((label.to_string(), full)); - }; - - push(&mut trace, "init", &vstate); - let _root = vstate.next_base_scalars_vec(8).unwrap(); - push(&mut trace, "after_root", &vstate); - let _ood_points: Vec = vstate.sample_vec(params.commitment_ood_samples); - push(&mut trace, "after_sample_ood_points", &vstate); - let _ood_answers = vstate - .next_extension_scalars_vec(params.commitment_ood_samples) - .unwrap(); - push(&mut trace, "after_read_ood_answers", &vstate); - vstate.duplex(); - push(&mut trace, "after_duplex", &vstate); - let _gamma: EF = vstate.sample(); - push(&mut trace, "after_sample_gamma", &vstate); - let mut cs = EF::ZERO; - let _poly0 = vstate.next_sumcheck_polynomial(3, cs, None).unwrap(); - push(&mut trace, "after_read_poly0", &vstate); - - #[derive(serde::Serialize)] - struct Out { - trace: Vec<(String, Vec)>, - } - let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let out = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("whir_test_vectors") - .join("state_trace.json"); - fs::write(&out, serde_json::to_string_pretty(&Out { trace: trace.clone() }).unwrap()).unwrap(); - println!("trace written to {}", out.display()); - for (k, v) in &trace { - println!(" {}: state={:?}", k, v); - } -} - -/// A tiny sanity test for the Python `Challenger`: observe a fixed -/// sequence, then sample a handful of EF challenges, and dump them. -#[test] -fn dump_challenger_vector() { - use fiat_shamir::{ChallengeSampler, FSProver}; - - let poseidon16 = default_koalabear_poseidon1_16(); - let mut prover = ProverState::::new(poseidon16); - - // Observe → sample → re-observe → sample, with an explicit duplex before - // sampling-after-sample (mirrors how the verifier interleaves). - let observed: Vec = (1u32..=20).map(F::from_u32).collect(); - prover.observe_scalars(&observed); - let s1: EF = prover.sample(); // single EF - prover.duplex(); - let s_vec3: Vec = prover.sample_vec(3); - prover.duplex(); - let s_in_range: Vec = prover.sample_in_range(20, 7); - - #[derive(serde::Serialize)] - struct Out { - observed_u32: Vec, - sample_ef: [u32; 5], - sample_ef_vec3: Vec<[u32; 5]>, - sample_in_range_20_7: Vec, - } - - let out = Out { - observed_u32: observed.iter().map(|&f| f_to_u32(f)).collect(), - sample_ef: ef_to_u32s(s1), - sample_ef_vec3: s_vec3.iter().map(|&p| ef_to_u32s(p)).collect(), - sample_in_range_20_7: s_in_range, - }; - - let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("whir_test_vectors"); - fs::create_dir_all(&out_dir).unwrap(); - let path = out_dir.join("challenger_sanity.json"); - fs::write(&path, serde_json::to_string_pretty(&out).unwrap()).unwrap(); - println!("wrote challenger sanity to {}", path.display()); -} - #[test] fn dump_test_vectors() { let target_dir = From 6c1c7cebc3991f147256cdd07216fc8de9c9a636 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 17 May 2026 22:49:26 +0200 Subject: [PATCH 04/69] wip --- crates/backend/fiat-shamir/src/prover.rs | 8 +- crates/lean_prover/poseidon1_constants.json | 1 + crates/lean_prover/test_zkvm.py | 38 +- .../tests/dump_poseidon1_constants.rs | 92 ++ crates/lean_prover/tests/dump_zkvm_vector.rs | 105 +- crates/lean_prover/verifier.py | 1451 ++++++++++++++++- 6 files changed, 1664 insertions(+), 31 deletions(-) create mode 100644 crates/lean_prover/poseidon1_constants.json create mode 100644 crates/lean_prover/tests/dump_poseidon1_constants.rs diff --git a/crates/backend/fiat-shamir/src/prover.rs b/crates/backend/fiat-shamir/src/prover.rs index 2ea95580d..76d1b10cc 100644 --- a/crates/backend/fiat-shamir/src/prover.rs +++ b/crates/backend/fiat-shamir/src/prover.rs @@ -126,11 +126,13 @@ where let lanes = Packed::::WIDTH; let witness_found = Mutex::>>::new(None); - // each batch tests lanes witnesses simultaneously + // each batch tests lanes witnesses simultaneously. + // NOTE: deliberately single-threaded so the resulting witness is + // deterministic across runs (the Python verifier port relies on bit- + // for-bit reproducibility of the dumped proof). let num_batches = PF::::ORDER_U64.div_ceil(lanes as u64); (0..num_batches) - .into_par_iter() - .find_any(|&batch| { + .find(|&batch| { let base = batch * lanes as u64; let packed_witnesses = Packed::::from_fn(|lane| { diff --git a/crates/lean_prover/poseidon1_constants.json b/crates/lean_prover/poseidon1_constants.json new file mode 100644 index 000000000..e5e1af7fb --- /dev/null +++ b/crates/lean_prover/poseidon1_constants.json @@ -0,0 +1 @@ +{"half_full_rounds":4,"partial_rounds":20,"initial_constants":[[2128964168,288780357,316938561,2126233899,426817493,1714118888,1045008582,1738510837,889721787,8866516,681576474,419059826,1596305521,1583176088,1584387047,1529751136],[1863858111,1072044075,517831365,1464274176,1138001621,428001039,245709561,1641420379,1365482496,770454828,693167409,757905735,136670447,436275702,525466355,1559174242],[1030087950,869864998,322787870,267688717,948964561,740478015,679816114,113662466,2066544572,1744924186,367094720,1380455578,1842483872,416711434,1342291586,1692058446],[1493348999,1113949088,210900530,1071655077,610242121,1136339326,2020858841,1019840479,678147278,1678413261,1361743414,61132629,1209546658,64412292,1936878279,1980661727]],"final_constants":[[1983525157,1330885184,414710339,733907571,479859442,1064293389,236801732,325174861,162067568,64109120,278581904,683867016,996448498,1960361559,1782740946,415413204],[1649591052,130819424,547348827,1386569644,1307680439,38932758,1581338609,1020895732,5942549,665140992,1924917707,1910029693,1100265370,1223195250,859919676,1674792874],[321520099,942924505,1232236036,88692728,2071051492,1945027965,1433294131,531185630,879398056,291692510,1546702888,155861652,810736858,932742296,1374710679,1703184249],[1973006548,1131403964,1724233597,1086876318,669451611,1829624280,2119538869,441255155,1580936135,1396398895,1043570981,1716351438,942566442,616885102,334644983,132306927]],"sparse_m_i":[[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1176991763,1962798433,507789489,1019168605,1163325691,466620818,1131708271,931504963,918112312,86863075,882630651,84434949,754655560,375632733,210588963],[0,1406869940,217296974,97037986,2020988961,1368157387,446815816,456620646,1350101418,1922416357,1227469637,603478726,1537295456,873165878,155811605,375632733],[0,682820181,2031016045,138039228,846585925,558910395,9722937,1543529703,2088599457,1481139936,255018864,2130530098,43680256,864171667,873165878,754655560],[0,110418656,1501074676,1412834556,1032465671,563855872,962367231,788585369,1597452496,62007254,389404591,904725063,1698425244,43680256,1537295456,84434949],[0,1538375796,2102000169,1333501812,530151948,2053218304,1744692061,1352986051,701513153,1663428696,1849567553,1774105504,904725063,2130530098,603478726,882630651],[0,807210699,1559543298,304640754,399071438,1521605122,2097677068,1930489690,1512116835,467964189,1473717591,1849567553,389404591,255018864,1227469637,86863075],[0,604440680,1962894682,695994228,2105212903,935308582,1889173744,983368822,39843520,446408074,467964189,1663428696,62007254,1481139936,1922416357,918112312],[0,956061053,360112848,1035074790,1610007096,1698268692,93985685,1442713600,622937787,39843520,1512116835,701513153,1597452496,2088599457,1350101418,931504963],[0,774245966,1379974664,604491366,1621008618,166130994,1057741505,706931411,1442713600,983368822,1930489690,1352986051,788585369,1543529703,456620646,1131708271],[0,1049574470,1831059707,1284527617,1297275196,751089896,981821717,1057741505,93985685,1889173744,2097677068,1744692061,962367231,9722937,446815816,466620818],[0,639454493,1368214806,1169707859,1849562776,1603581590,751089896,166130994,1698268692,935308582,1521605122,2053218304,563855872,558910395,1368157387,1163325691],[0,1332049785,2018469344,1406223611,1533175366,1849562776,1297275196,1621008618,1610007096,2105212903,399071438,530151948,1032465671,846585925,2020988961,1019168605],[0,67923442,549746182,1490248217,1406223611,1169707859,1284527617,604491366,1035074790,695994228,304640754,1333501812,1412834556,138039228,97037986,507789489],[0,1092915095,1728246700,549746182,2018469344,1368214806,1831059707,1379974664,360112848,1962894682,1559543298,2102000169,1501074676,2031016045,217296974,1962798433],[0,1102759352,1092915095,67923442,1332049785,639454493,1049574470,774245966,956061053,604440680,807210699,1538375796,110418656,682820181,1406869940,1176991763]],"sparse_first_row":[[1,1044617752,1481433387,1878444588,2104235304,3722907,640029121,1328464283,527881075,2001559077,689032166,1880575107,358872577,108364736,1332772919,2129539744],[1,914163922,1285456931,2020639520,1453855633,1477444027,1339193063,328589713,208931151,1850882938,1462363792,869657005,805767435,796387373,400960806,755656571],[1,1787518610,1878065122,1211179805,1623392502,596876727,487353310,948630619,46137575,2011272885,785962300,141492211,527311230,1677138244,308914786,646273371],[1,475985225,1276143107,1696071196,32589944,1946884834,790695552,1297018938,1247695977,1441442697,348598749,1532810014,420302089,1768470437,2025744598,502837865],[1,189252144,933340760,1196177692,1207326122,1310289919,1018859884,1931151148,306468194,2080590861,1011822907,870184312,1169054295,299157995,550624518,761217428],[1,530026590,1465459236,1537941855,311984892,1766685979,254904495,1314612604,654252150,1786383982,886092250,951000927,1492154688,2015327114,1795152847,886334479],[1,1412702485,1137887454,520203158,1812492367,789833688,1233938788,1819934176,1614801759,746470909,1336417528,680335909,313757646,841134444,1641869241,998121965],[1,1839450507,1514473471,1397874495,427026633,825206836,1881998988,798695984,1518245734,137171104,1985410295,1204805986,708385656,135671564,181727188,1904989502],[1,1624250506,1276553090,1125040530,732235550,770503435,1098559359,1897139531,1957393256,2066469648,356890796,430889358,1662727935,736846479,86159423,2117864164],[1,1716262826,1219251453,406649991,956336998,891847704,1340399588,2120454448,1963372981,1580211744,1080244840,443212987,97408192,1276609344,864015922,791499252],[1,1506258844,428092642,616592092,491052976,1058721642,1154122014,1063147280,1468054399,738561812,458551485,1134033275,798609536,1652845715,1626650240,1834902174],[1,607388252,1341920224,661794417,844415991,1742333960,710739800,111776808,1680513373,1739278776,1261371867,485363479,9207629,1858910799,2063484755,1896740071],[1,837837165,218556391,599284291,302320589,605756188,1423640541,982100365,1395306646,2054696424,1124172688,311709517,1483301282,2057326379,468011828,195504483],[1,1289414867,961998037,641842254,1998469649,519613099,1247429126,607576114,76055325,48127247,1837975498,79401479,1108712765,543571094,1463931705,1750978183],[1,32307344,407129084,1819638694,777354771,1232160074,2126730873,1007018661,1966216114,644324697,1374455617,1280692573,1485221466,2092259084,2005955566,843003152],[1,1799257672,1148378128,1707844443,119809600,1464022250,1463207203,1189831139,80446531,29416071,995912922,1867752521,1481174533,1072217556,801048591,1832269882],[1,1030517273,477905567,1805133547,728218728,691695658,1764920569,1697028861,581984142,1322354059,843428748,903794926,1401335111,1906908186,1236851451,1854676428],[1,375781491,663573853,204251191,1828817902,317340619,1771861371,1841750301,1008632801,1793041736,369201095,488809041,46558970,875712544,1922589546,1760372266],[1,483733172,606096455,106009622,441040436,1929468092,1672504038,1906451897,986604790,1050370358,429434801,335400069,1143011095,1303702871,733751510,1165784837],[1,1108808405,2090426960,155082568,702347681,919398936,1226339182,1901110596,1230360372,1088093666,1713572740,675635302,759294455,895266739,255605669,1282509143]],"sparse_v":[[815798082,1599417173,2019487682,1495563308,1429225500,462208417,1706939096,1929713759,2037985010,1993489272,146269421,1370491063,1457031915,1571606227,442112630,0],[1672393568,1841009674,1550920329,1779211568,1449479676,1961293578,1174765549,738863811,358643257,820352444,1150799707,619173188,922229211,1138887134,409392716,0],[115734840,580126996,1525976646,1239851818,2073245456,1030589628,1377558295,494197709,238790464,719384642,134484029,231324069,639578566,636120851,568223911,0],[1300554587,1450500786,38201558,1838005083,1019142646,576859025,592297447,460824075,1486889364,199901131,793972955,1649041905,1287870494,344387188,1436973230,0],[1379067459,1918333570,591694540,245256148,1209106504,89299776,769713898,422208520,319592660,1799482307,665955244,433129386,733120015,95130246,1380689525,0],[2043404085,177610011,776806663,1124577123,324662120,532004834,57207205,807037515,1322129569,535602285,1823856965,687338970,1150883563,1938629528,1135982477,0],[125487871,771378267,895416365,1835469214,1441346099,1574070991,1051852536,1802482042,424344011,439065759,635684069,172075871,2054384866,792486292,1785646874,0],[1951012288,581143389,1449982847,2034951834,1296305314,1253043123,437690613,1533604834,1062523959,460834088,2028092965,112092247,480561016,29047112,918330564,0],[1321780723,906545729,598089205,1961814902,59242599,1763880479,1227717469,421528848,858340345,1469534917,1284739390,1593161876,70443154,1376173926,950943549,0],[111983004,815998134,480506885,2051984157,1295771849,472501509,1228066101,982351516,1168152195,2065242773,1539603922,494115877,630184518,1875163552,1430833335,0],[223627613,90799236,405797050,756408252,269447004,368791260,977004868,2000904592,658368457,581053670,1971660486,1301775976,1711019833,8812901,370847939,0],[1677802579,1959347885,1379609505,1200496457,441395130,1651239120,388457220,600596382,1851813934,1099854908,1253845511,1066124698,1415589924,943395525,1201570139,0],[1742026459,398996820,1559417452,1869434180,1650939527,1678406146,697527412,1329042656,1590738739,840532121,1919639745,1493325582,385219255,1321483334,74724398,0],[370629703,968406604,283063044,1421912803,1218525950,235983381,2097999101,1417290051,89846708,1755258584,1614636443,1923339542,1443080738,1287589955,1628527076,0],[1404617516,1387461349,960446077,316686774,1493143485,1135010996,1364787501,1366151319,1429025689,560429732,1992657421,86028332,16393928,1587924775,1099758468,0],[710831155,1269946944,1906631355,1017976477,466873715,61539759,17059176,1278714883,2061644815,240339454,235970441,1012156003,1873469407,1611775578,1163822633,0],[1188745580,1055003602,785416201,868051025,1135832507,1004853599,904741729,809824679,980810992,1178194302,1159788697,949043013,1001466621,1011628637,924759953,0],[2475856,3337618,4161263,3129126,2071505,3373463,2975691,1742470,2828204,3695590,1809935,2316312,3448583,2986173,2518923,0],[3415,9781,5292,4288,7724,13016,3835,5807,4933,8577,13125,16823,3127,8363,15859,0],[3,13,22,67,2,15,63,101,1,2,17,11,1,51,1,0]],"sparse_first_round_constants":[1423960925,886776133,1838900201,1725134361,1970838154,1349502123,1632425298,1452136978,1500653880,1694910225,1895400154,783177966,1170207886,1249553016,1486169768,387169126],"sparse_scalar_round_constants":[1358473177,1095637505,293175207,73153213,86260038,722710190,2089335770,1280052251,576313228,265102820,1685441472,670793739,1640841922,1549535807,1957713140,1556154273,1103412295,2118144716,20933114],"mds_dense":[[1,1,51,1,11,17,2,1,101,63,15,2,67,22,13,3],[3,1,1,51,1,11,17,2,1,101,63,15,2,67,22,13],[13,3,1,1,51,1,11,17,2,1,101,63,15,2,67,22],[22,13,3,1,1,51,1,11,17,2,1,101,63,15,2,67],[67,22,13,3,1,1,51,1,11,17,2,1,101,63,15,2],[2,67,22,13,3,1,1,51,1,11,17,2,1,101,63,15],[15,2,67,22,13,3,1,1,51,1,11,17,2,1,101,63],[63,15,2,67,22,13,3,1,1,51,1,11,17,2,1,101],[101,63,15,2,67,22,13,3,1,1,51,1,11,17,2,1],[1,101,63,15,2,67,22,13,3,1,1,51,1,11,17,2],[2,1,101,63,15,2,67,22,13,3,1,1,51,1,11,17],[17,2,1,101,63,15,2,67,22,13,3,1,1,51,1,11],[11,17,2,1,101,63,15,2,67,22,13,3,1,1,51,1],[1,11,17,2,1,101,63,15,2,67,22,13,3,1,1,51],[51,1,11,17,2,1,101,63,15,2,67,22,13,3,1,1],[1,51,1,11,17,2,1,101,63,15,2,67,22,13,3,1]]} \ No newline at end of file diff --git a/crates/lean_prover/test_zkvm.py b/crates/lean_prover/test_zkvm.py index 1bc207d6e..c10c5862f 100644 --- a/crates/lean_prover/test_zkvm.py +++ b/crates/lean_prover/test_zkvm.py @@ -1,13 +1,14 @@ -"""Run the Python `verify_execution` prologue + stacked-PCS parse against -the Rust-generated zkVM test vectors. +"""Run the Python `verify_execution` (prologue + stacked-PCS + generic logup) +against the Rust-generated zkVM test vector. -Regenerate the vectors with: +Regenerate the vector with: cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture Then run: .venv/bin/python crates/lean_prover/test_zkvm.py """ +import array import json import sys from pathlib import Path @@ -22,10 +23,21 @@ TableInfo, prunedpaths_from_json, restore_merkle_paths, + tables_from_json, verify_execution, ) +def _load_bytecode_mle(json_path: Path, raw: dict) -> list[Fp]: + bin_path = json_path.parent / raw["bytecode_multilinear_path"] + data = bin_path.read_bytes() + n = raw["bytecode_multilinear_len"] + assert len(data) == n * 4, (len(data), n * 4) + arr = array.array("I") + arr.frombytes(data) + return [Fp(v) for v in arr] + + def _load(path: Path): raw = json.loads(path.read_text()) bytecode = Bytecode( @@ -39,24 +51,30 @@ def _load(path: Path): for r in restore_merkle_paths(prunedpaths_from_json(bucket)): openings.append(MerkleOpening(leaf_data=r.leaf_data, path=r.sibling_hashes)) proof = Proof(transcript=transcript, merkle_openings=openings) - tables = [TableInfo(name=t["name"], n_columns=t["n_columns"]) for t in raw["tables"]] - return bytecode, public_input, proof, tables + + metas = tables_from_json(raw["tables"]) + tables = [ + TableInfo(name=m.name, n_columns=m.n_columns, bus=m.bus, lookups=m.lookups) + for m in metas + ] + bytecode_mle = _load_bytecode_mle(path, raw) + return bytecode, public_input, proof, tables, raw["constants"], bytecode_mle def run(path: Path) -> bool: - bytecode, public_input, proof, tables = _load(path) + bytecode, public_input, proof, tables, constants, bytecode_mle = _load(path) try: - partial = verify_execution(bytecode, public_input, proof, tables) + partial = verify_execution(bytecode, public_input, proof, tables, constants, bytecode_mle) except Exception as e: print(f" {path.name}: FAILED: {type(e).__name__}: {e}") return False - pc = partial.parsed_commitment + logup = partial.logup_statements print( f" {path.name}: OK " f"log_inv_rate={partial.log_inv_rate}, log_memory={partial.log_memory}, " f"stacked_n_vars={partial.stacked_n_vars}, " - f"table_log_heights={partial.table_log_heights}, " - f"commitment ood_points={len(pc.ood_points)}" + f"gkr_n_vars={logup.total_gkr_n_vars}, " + f"bytecode_eval_point.len={len(logup.bytecode_evaluation.point)}" ) return True diff --git a/crates/lean_prover/tests/dump_poseidon1_constants.rs b/crates/lean_prover/tests/dump_poseidon1_constants.rs new file mode 100644 index 000000000..585838f08 --- /dev/null +++ b/crates/lean_prover/tests/dump_poseidon1_constants.rs @@ -0,0 +1,92 @@ +//! Dumps all Poseidon1 round constants + matrices used by the leanVM AIR +//! into `crates/lean_prover/poseidon1_constants.json`, so the Python verifier +//! doesn't need to embed thousands of field literals. +//! +//! Run: +//! cargo test --release -p lean_prover --test dump_poseidon1_constants -- --nocapture + +use std::fs; +use std::path::PathBuf; + +use backend::{ + KoalaBear, POSEIDON1_HALF_FULL_ROUNDS, POSEIDON1_PARTIAL_ROUNDS, PrimeField32, + poseidon1_final_constants, poseidon1_initial_constants, poseidon1_sparse_first_round_constants, + poseidon1_sparse_first_row, poseidon1_sparse_m_i, poseidon1_sparse_scalar_round_constants, + poseidon1_sparse_v, +}; +use serde::Serialize; + +fn k(x: KoalaBear) -> u32 { + x.as_canonical_u32() +} + +#[derive(Serialize)] +struct Out { + half_full_rounds: usize, + partial_rounds: usize, + initial_constants: Vec>, + final_constants: Vec>, + sparse_m_i: Vec>, + sparse_first_row: Vec>, + sparse_v: Vec>, + sparse_first_round_constants: Vec, + sparse_scalar_round_constants: Vec, + mds_dense: Vec>, +} + +/// Reconstruct the dense MDS matrix the way `mds_dense_16` does in +/// `lean_vm::tables::poseidon_16::mod.rs` — by running `mds_circ_16` on each +/// standard basis vector and stacking the columns into a row-major matrix. +/// We avoid touching the private `mds_circ_16` directly by recreating its +/// effect via `mds_fft_16` (which is the same map for KoalaBear). +fn dense_mds_matrix() -> [[KoalaBear; 16]; 16] { + use backend::{PrimeCharacteristicRing, mds_circ_16}; + + let mut cols = [[KoalaBear::ZERO; 16]; 16]; + for j in 0..16 { + let mut e = [KoalaBear::ZERO; 16]; + e[j] = KoalaBear::ONE; + mds_circ_16::(&mut e); + cols[j] = e; + } + let mut rows = [[KoalaBear::ZERO; 16]; 16]; + for i in 0..16 { + for j in 0..16 { + rows[i][j] = cols[j][i]; + } + } + rows +} + +#[test] +fn dump_poseidon1_constants() { + let initial = poseidon1_initial_constants(); + let final_ = poseidon1_final_constants(); + let m_i = poseidon1_sparse_m_i(); + let first_row = poseidon1_sparse_first_row(); + let sparse_v = poseidon1_sparse_v(); + let first_rc = poseidon1_sparse_first_round_constants(); + let scalar_rc = poseidon1_sparse_scalar_round_constants(); + let mds = dense_mds_matrix(); + + let out = Out { + half_full_rounds: POSEIDON1_HALF_FULL_ROUNDS, + partial_rounds: POSEIDON1_PARTIAL_ROUNDS, + initial_constants: initial.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), + final_constants: final_.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), + sparse_m_i: m_i.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), + sparse_first_row: first_row.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), + sparse_v: sparse_v.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), + sparse_first_round_constants: first_rc.iter().map(|&x| k(x)).collect(), + sparse_scalar_round_constants: scalar_rc.iter().map(|&x| k(x)).collect(), + mds_dense: mds.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), + }; + + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("poseidon1_constants.json"); + fs::write(&path, serde_json::to_string(&out).unwrap()).unwrap(); + println!( + "wrote poseidon1 constants ({:.1} KiB) to {}", + path.metadata().unwrap().len() as f64 / 1024.0, + path.display() + ); +} diff --git a/crates/lean_prover/tests/dump_zkvm_vector.rs b/crates/lean_prover/tests/dump_zkvm_vector.rs index 77c1a99fa..b16780ce6 100644 --- a/crates/lean_prover/tests/dump_zkvm_vector.rs +++ b/crates/lean_prover/tests/dump_zkvm_vector.rs @@ -11,11 +11,14 @@ use std::fs; use std::path::PathBuf; -use backend::{PrimeField32, PrunedMerklePaths, WhirConfigBuilder}; +use backend::{Air, PrimeField32, PrunedMerklePaths, WhirConfigBuilder}; use lean_compiler::*; -use lean_prover::{default_whir_config, prove_execution::prove_execution}; +use lean_prover::{default_whir_config, prove_execution::prove_execution, verify_execution::verify_execution}; +// `verify_execution` is imported so the dump test self-checks the proof +// before writing it. use lean_vm::*; use serde::Serialize; +use std::io::Write; type F = lean_vm::F; @@ -58,10 +61,45 @@ struct BuilderJson { soundness_type: &'static str, } +#[derive(Serialize)] +#[serde(tag = "kind", content = "value")] +enum BusDataJson { + Column(usize), + Constant(usize), +} + +#[derive(Serialize)] +struct BusJson { + direction: &'static str, + selector: usize, + data: Vec, +} + +#[derive(Serialize)] +struct LookupJson { + index: usize, + values: Vec, +} + #[derive(Serialize)] struct TableInfoJson { name: &'static str, n_columns: usize, + bus: BusJson, + lookups: Vec, +} + +#[derive(Serialize)] +struct ConstantsJson { + n_instruction_columns: usize, + n_runtime_columns: usize, + col_pc: usize, + logup_memory_domainsep: usize, + logup_precompile_domainsep: usize, + logup_bytecode_domainsep: usize, + max_precompile_bus_width: usize, + starting_pc: usize, + ending_pc: usize, } #[derive(Serialize)] @@ -69,9 +107,13 @@ struct OutJson { name: String, bytecode_log_size: usize, bytecode_hash: [u32; DIGEST_ELEMS], + /// Path (relative to JSON) of the raw u32-LE bytecode multilinear sidecar. + bytecode_multilinear_path: String, + bytecode_multilinear_len: usize, public_input: Vec, n_tables: usize, tables: Vec, + constants: ConstantsJson, snark_domain_sep: [u32; DIGEST_ELEMS], builder: BuilderJson, proof: ProofJson, @@ -106,21 +148,71 @@ fn dump_one(name: &str, program_str: &str, public_input: Vec, out_dir: &PathB let exec_proof = prove_execution(&bytecode, &public_input, &witness, &builder, false) .expect("prove_execution failed"); + // Run the Rust verifier as a self-check before writing the dump. + verify_execution(&bytecode, &public_input, exec_proof.proof.clone()) + .expect("Rust verify_execution failed"); + + let convert_bus = |bus: Bus| BusJson { + direction: match bus.direction { + BusDirection::Pull => "Pull", + BusDirection::Push => "Push", + }, + selector: bus.selector, + data: bus + .data + .into_iter() + .map(|d| match d { + BusData::Column(c) => BusDataJson::Column(c), + BusData::Constant(v) => BusDataJson::Constant(v), + }) + .collect(), + }; + let table_infos: Vec = ALL_TABLES .iter() .map(|t| TableInfoJson { name: t.name(), - n_columns:
::n_columns(t), + n_columns:
::n_columns(t), + bus: convert_bus(t.bus()), + lookups: t + .lookups() + .into_iter() + .map(|l| LookupJson { index: l.index, values: l.values }) + .collect(), }) .collect(); + // Dump the bytecode multilinear to a sidecar binary file. Length is + // `2^cumulated_n_vars` where `cumulated_n_vars = log_size + ceil(log2(N_INSTRUCTION_COLUMNS))`. + let mle_path_rel = format!("{name}.bytecode_mle.bin"); + let mle_full_path = out_dir.join(&mle_path_rel); + { + let mut f = fs::File::create(&mle_full_path).unwrap(); + for v in &bytecode.instructions_multilinear { + f.write_all(&f_to_u32(*v).to_le_bytes()).unwrap(); + } + } + let out = OutJson { name: name.to_string(), bytecode_log_size: bytecode.log_size(), bytecode_hash: bytecode.hash.map(f_to_u32), + bytecode_multilinear_path: mle_path_rel.clone(), + bytecode_multilinear_len: bytecode.instructions_multilinear.len(), public_input: public_input.iter().map(|&f| f_to_u32(f)).collect(), n_tables: N_TABLES, tables: table_infos, + constants: ConstantsJson { + n_instruction_columns: N_INSTRUCTION_COLUMNS, + n_runtime_columns: N_RUNTIME_COLUMNS, + col_pc: COL_PC, + logup_memory_domainsep: LOGUP_MEMORY_DOMAINSEP, + logup_precompile_domainsep: LOGUP_PRECOMPILE_DOMAINSEP, + logup_bytecode_domainsep: LOGUP_BYTECODE_DOMAINSEP, + max_precompile_bus_width: MAX_PRECOMPILE_BUS_WIDTH, + starting_pc: STARTING_PC, + ending_pc: ENDING_PC, + }, snark_domain_sep: lean_prover::SNARK_DOMAIN_SEP.map(f_to_u32), builder: BuilderJson { security_level: 124, @@ -159,12 +251,13 @@ fn dump_zkvm_vector() { .join("zkvm_test_vectors"); fs::create_dir_all(&out_dir).unwrap(); - // The smallest legal program. Empty public input. + // Use a small program (no big unroll). Empty public input. The compiler + // pads bytecode to at least MIN_BYTECODE_LOG_SIZE so we still get a valid + // proof; keeping the bytecode small keeps the dumped multilinear small. let small_program = r#" def main(): a = Array(1) - for i in unroll(0, 2**17): - a[0] = 1 * 2 + a[0] = 1 * 2 return "#; dump_one("small", small_program, vec![], &out_dir); diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 2d7c99df2..aa399bf19 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -101,12 +101,22 @@ def from_basis_coefficients(coeffs: Sequence[Fp]) -> "EF": # --- arithmetic ------------------------------------------------------- - def __add__(self, other: "EF") -> "EF": + def __add__(self, other) -> "EF": + if isinstance(other, Fp): + return EF([self.c[0] + other, *self.c[1:]]) return EF([a + b for a, b in zip(self.c, other.c)]) - def __sub__(self, other: "EF") -> "EF": + __radd__ = __add__ + + def __sub__(self, other) -> "EF": + if isinstance(other, Fp): + return EF([self.c[0] - other, *self.c[1:]]) return EF([a - b for a, b in zip(self.c, other.c)]) + def __rsub__(self, other) -> "EF": + # other - self, where other is Fp + return EF([other - self.c[0], *[-c for c in self.c[1:]]]) + def __neg__(self) -> "EF": return EF([-a for a in self.c]) @@ -760,6 +770,15 @@ def new_(total: int, point: list[EF], values: list[SparseValue]) -> "SparseState def dense(point: list[EF], value: EF) -> "SparseStatement": return SparseStatement(len(point), point, [SparseValue(0, value)], is_next=False) + @staticmethod + def unique_value(total: int, index: int, value: EF) -> "SparseStatement": + return SparseStatement(total, [], [SparseValue(index, value)], is_next=False) + + @staticmethod + def new_next(total: int, point: list[EF], values: list[SparseValue]) -> "SparseStatement": + assert total >= len(point) + return SparseStatement(total, point, values, is_next=True) + # --------------------------------------------------------------------------- # WHIR config helpers: derive integer-only parameters from the trimmed JSON @@ -1238,6 +1257,81 @@ def whir_verify( return folding_randomness_flat +# --------------------------------------------------------------------------- +# Table metadata (mirror of lean_vm::tables::table_trait) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class BusData: + """One field of a precompile bus message: either a column index or a + constant. Mirrors `lean_vm::BusData`. + """ + + kind: str # "Column" or "Constant" + value: int + + @classmethod + def from_json(cls, obj: dict) -> "BusData": + return cls(kind=obj["kind"], value=int(obj["value"])) + + +@dataclass(frozen=True) +class Bus: + """Per-table bus descriptor. `direction` is "Pull" or "Push".""" + + direction: str + selector: int + data: tuple[BusData, ...] + + @property + def direction_flag(self) -> Fp: + # Mirrors `BusDirection::to_field_flag`: Pull = -1, Push = +1. + return Fp(P - 1) if self.direction == "Pull" else Fp(1) + + +@dataclass(frozen=True) +class Lookup: + """A single memory lookup descriptor (`LookupIntoMemory` in Rust).""" + + index: int + values: tuple[int, ...] + + +@dataclass(frozen=True) +class TableMeta: + """Bundle of everything the verifier needs about one table.""" + + name: str + n_columns: int + bus: Bus + lookups: tuple[Lookup, ...] + + +def tables_from_json(obj: list[dict]) -> list[TableMeta]: + out: list[TableMeta] = [] + for t in obj: + bus_obj = t["bus"] + bus = Bus( + direction=bus_obj["direction"], + selector=int(bus_obj["selector"]), + data=tuple(BusData.from_json(d) for d in bus_obj["data"]), + ) + lookups = tuple( + Lookup(index=int(l["index"]), values=tuple(int(v) for v in l["values"])) + for l in t["lookups"] + ) + out.append( + TableMeta( + name=t["name"], + n_columns=int(t["n_columns"]), + bus=bus, + lookups=lookups, + ) + ) + return out + + # --------------------------------------------------------------------------- # Stacked PCS — port of sub_protocols/stacked_pcs.rs # --------------------------------------------------------------------------- @@ -1265,6 +1359,81 @@ def compute_stacked_n_vars( return log2_ceil_usize(total_len) +def stacked_pcs_global_statements( + stacked_n_vars: int, + memory_n_vars: int, + bytecode_n_vars: int, + previous_statements: list[SparseStatement], + table_log_heights: dict[str, int], + committed_statements: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]], + tables: dict[str, TableMeta], + constants: dict, +) -> list[SparseStatement]: + """Port of `stacked_pcs::stacked_pcs_global_statements`. + + Stacks the per-table column claims into the global statement list passed to + `WhirConfig::verify`. Tables are processed in descending-height order. + """ + assert len(table_log_heights) == len(committed_statements) + tables_sorted = sort_tables_by_height(table_log_heights) + + out = list(previous_statements) + offset = 2 << memory_n_vars # memory + memory_acc + max_table_n_vars = tables_sorted[0][1] + offset += 1 << max(bytecode_n_vars, max_table_n_vars) # bytecode acc + + col_pc = constants["col_pc"] + starting_pc = constants["starting_pc"] + ending_pc = constants["ending_pc"] + + for name, n_vars in tables_sorted: + if name == "execution": + out.append( + SparseStatement.unique_value( + stacked_n_vars, + offset + (col_pc << n_vars), + EF.from_base(Fp(starting_pc)), + ) + ) + out.append( + SparseStatement.unique_value( + stacked_n_vars, + offset + ((col_pc + 1) << n_vars) - 1, + EF.from_base(Fp(ending_pc)), + ) + ) + + for (point, eq_values, next_values) in committed_statements[name]: + # Rust uses BTreeMap → ascending-key iteration. Python dicts are + # insertion-ordered, so we have to sort explicitly here. + if next_values: + out.append( + SparseStatement.new_next( + stacked_n_vars, + list(point), + [ + SparseValue((offset >> n_vars) + col_index, value) + for col_index, value in sorted(next_values.items()) + ], + ) + ) + out.append( + SparseStatement( + total_num_variables=stacked_n_vars, + point=list(point), + values=[ + SparseValue((offset >> n_vars) + col_index, value) + for col_index, value in sorted(eq_values.items()) + ], + is_next=False, + ) + ) + + offset += tables[name].n_columns << n_vars + + return out + + def stacked_pcs_parse_commitment( state: VerifierState, log_inv_rate: int, @@ -1412,16 +1581,1167 @@ def _quotient_sum(nums: Sequence[EF], dens: Sequence[EF]) -> EF: # --------------------------------------------------------------------------- -# Stubs still pending for the lean_prover verifier +# Logup helpers (utils + poly) # --------------------------------------------------------------------------- -def verify_generic_logup(*args, **kwargs): - raise NotImplementedError("verify_generic_logup: port from sub_protocols/logup.rs") +def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: + """Mirror of `poly::to_big_endian_in_field`. + + Returns the `bit_count` bits of `value` MSB-first, each as `EF::ZERO`/`EF::ONE`. + """ + return [EF.one() if (value >> (bit_count - 1 - i)) & 1 else EF.zero() for i in range(bit_count)] + + +def from_end(seq: Sequence, n: int) -> list: + """`utils::from_end` — the last `n` elements.""" + if n == 0: + return [] + return list(seq[len(seq) - n :]) + + +def mle_of_01234567_etc(point: Sequence[EF]) -> EF: + """Mirror of `utils::mle_of_01234567_etc`. + + Evaluates the multilinear polynomial whose evaluations on `{0,1}^n` are + `f(i) = i` (with `i` interpreted big-endian), at `point`. + """ + if not point: + return EF.zero() + e = mle_of_01234567_etc(point[1:]) + bit = EF(_from_int(1 << (len(point) - 1)).c) + return (EF.one() - point[0]) * e + point[0] * (e + bit) + + +def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: + """Mirror of `poly::mle_of_zeros_then_ones`. + + Evaluates the multilinear of `[0, ..., 0, 1, ..., 1]` (`n_zeros` zeros, then + `2^len(point) - n_zeros` ones) at `point`. + """ + n_values = 1 << len(point) + assert n_zeros <= n_values + if n_zeros == 0: + return EF.one() + if n_zeros == n_values: + return EF.zero() + half = n_values >> 1 + if n_zeros < half: + return (EF.one() - point[0]) * mle_of_zeros_then_ones(n_zeros, point[1:]) + point[0] + return point[0] * mle_of_zeros_then_ones(n_zeros - half, point[1:]) + + +def finger_print(table: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: + """Mirror of `utils::finger_print`. + + Computes `Σᵢ alphas_eq_poly[i] · data[i] + alphas_eq_poly[-1] · table`. + """ + assert len(alphas_eq_poly) > len(data) + acc = EF.zero() + for a, d in zip(alphas_eq_poly, data): + acc = acc + a * d + acc = acc + alphas_eq_poly[-1] * EF.from_base(table) + return acc + + +def _from_int(x: int) -> EF: + return EF.from_base(Fp(x % P)) + + +def sort_tables_by_height( + table_log_heights: dict[str, int], +) -> list[tuple[str, int]]: + """Mirror of `lean_vm::sort_tables_by_height` — descending by `log_n_rows`, + `BTreeMap` ordering (= alphabetical) breaks ties. + """ + items = sorted(table_log_heights.items()) # alphabetical + items.sort(key=lambda kv: -kv[1]) + return items + + +def eval_eq(point: Sequence[EF]) -> list[EF]: + """Evaluation table of `eq(point, ·)`: the length-`2^n` vector with + `eq[i] = Πⱼ (point[j] if bitⱼ(i) else 1 - point[j])` for big-endian `i`. + """ + out = [EF.one()] + for p in point: + nxt: list[EF] = [] + for v in out: + nxt.append(v * (EF.one() - p)) + nxt.append(v * p) + out = nxt + return out # --------------------------------------------------------------------------- -# Top-level verifier (skeleton) +# Generic logup verifier — port of sub_protocols/logup.rs::verify_generic_logup +# --------------------------------------------------------------------------- + + +@dataclass +class GenericLogupStatements: + """Mirror of `GenericLogupStatements` returned by `verify_generic_logup`.""" + + memory_and_acc_point: list[EF] + value_memory: EF + value_memory_acc: EF + bytecode_and_acc_point: list[EF] + value_bytecode_acc: EF + bus_numerators_values: dict[str, EF] + bus_denominators_values: dict[str, EF] + gkr_point: list[EF] + columns_values: dict[str, dict[int, EF]] + total_gkr_n_vars: int + bytecode_evaluation: Evaluation + + +def _compute_total_active_len_logup( + log_memory: int, + log_bytecode: int, + tables_sorted: list[tuple[str, int]], + table_lookups: dict[str, list[Lookup]], + execution_name: str, +) -> int: + """Mirror of `logup::compute_total_active_len`.""" + max_table_height = 1 << tables_sorted[0][1] + log_n_cycles = next(h for n, h in tables_sorted if n == execution_name) + + def offset_for_table(name: str, log_n_rows: int) -> int: + num_cols = sum(len(l.values) for l in table_lookups[name]) + 1 # +1 for the bus + return num_cols << log_n_rows + + return ( + (1 << log_memory) + + max(1 << log_bytecode, max_table_height) + + (1 << log_n_cycles) + + sum(offset_for_table(name, h) for name, h in tables_sorted) + ) + + +def verify_generic_logup( + state: VerifierState, + c: EF, + alphas: list[EF], + alphas_eq_poly: list[EF], + log_memory: int, + bytecode_multilinear: list[Fp], + table_log_heights: dict[str, int], + tables: dict[str, TableMeta], + constants: dict, + execution_name: str = "execution", +) -> GenericLogupStatements: + """Port of `verify_generic_logup`. + + `bytecode_multilinear` is the flat coefficient vector of length + `2^(log_bytecode + ceil(log2(N_INSTRUCTION_COLUMNS)))` — what the Rust + verifier holds as `&bytecode.instructions_multilinear`. + + `alphas` and `alphas_eq_poly` come from sampling `c` and `log2_ceil(max_bus_width)` + extension-field elements (per the leanVM `verify_execution`). + """ + + n_instr_cols = constants["n_instruction_columns"] + n_runtime_cols = constants["n_runtime_columns"] + col_pc = constants["col_pc"] + dom_mem = constants["logup_memory_domainsep"] + dom_byte = constants["logup_bytecode_domainsep"] + + tables_sorted = sort_tables_by_height(table_log_heights) + log_bytecode = log2_strict_usize( + len(bytecode_multilinear) // _next_pow_two(n_instr_cols) + ) + + table_lookups = {name: list(tables[name].lookups) for name in table_log_heights} + total_active_len = _compute_total_active_len_logup( + log_memory, log_bytecode, tables_sorted, table_lookups, execution_name + ) + total_gkr_n_vars = log2_ceil_usize(total_active_len) + + quotient, point_gkr, numerators_value, denominators_value = verify_gkr_quotient( + state, total_gkr_n_vars + ) + + if quotient != EF.zero(): + raise ProofError("logup: GKR sum != 0") + + retrieved_num = EF.zero() + retrieved_den = EF.zero() + + def pref_at(offset: int, log_height: int) -> EF: + n_missing = total_gkr_n_vars - log_height + bits = to_big_endian_in_field(offset >> log_height, n_missing) + return eq_poly_outside(bits, point_gkr[:n_missing]) + + # ---- Memory section -------------------------------------------------- + memory_and_acc_point = from_end(point_gkr, log_memory) + pref = pref_at(0, log_memory) + + value_memory_acc = state.next_extension_scalar() + retrieved_num = retrieved_num - pref * value_memory_acc + + value_memory = state.next_extension_scalar() + value_index = mle_of_01234567_etc(memory_and_acc_point) + retrieved_den = retrieved_den + pref * ( + c - finger_print(Fp(dom_mem), [value_memory, value_index], alphas_eq_poly) + ) + offset = 1 << log_memory + + # ---- Bytecode section ------------------------------------------------ + log_bytecode_padded = max(log_bytecode, tables_sorted[0][1]) + bytecode_and_acc_point = from_end(point_gkr, log_bytecode) + pref = pref_at(offset, log_bytecode) + pref_padded = pref_at(offset, log_bytecode_padded) + + value_bytecode_acc = state.next_extension_scalar() + retrieved_num = retrieved_num - pref * value_bytecode_acc + + bytecode_index_value = mle_of_01234567_etc(bytecode_and_acc_point) + log_instr = log2_ceil_usize(n_instr_cols) + bytecode_point = list(bytecode_and_acc_point) + list(from_end(alphas, log_instr)) + bytecode_value = eval_multilinear_evals( + [EF.from_base(x) for x in bytecode_multilinear], bytecode_point + ) + # Correction: `(1 - alpha[0]) * (1 - alpha[1]) * ... * (1 - alpha[k-1])` + # over the alphas BEFORE the last `log_instr` (= the bus-data slot bits). + correction = EF.one() + for a in alphas[: len(alphas) - log_instr]: + correction = correction * (EF.one() - a) + bytecode_value_corrected = bytecode_value * correction + retrieved_den = retrieved_den + pref * ( + c + - ( + bytecode_value_corrected + + bytecode_index_value * alphas_eq_poly[n_instr_cols] + + alphas_eq_poly[-1] * EF.from_base(Fp(dom_byte)) + ) + ) + + # Padding for bytecode (bytecode_acc shorter than max_table_height). + retrieved_den = retrieved_den + pref_padded * mle_of_zeros_then_ones( + 1 << log_bytecode, from_end(point_gkr, log_bytecode_padded) + ) + offset += 1 << log_bytecode_padded + + # ---- Per-table sections ---------------------------------------------- + bus_num_vals: dict[str, EF] = {} + bus_den_vals: dict[str, EF] = {} + columns_values: dict[str, dict[int, EF]] = {} + + for name, log_n_rows in tables_sorted: + meta = tables[name] + table_values: dict[int, EF] = {} + + if name == execution_name: + # 0] Bytecode lookup for the execution table. + eval_on_pc = state.next_extension_scalar() + table_values[col_pc] = eval_on_pc + + instr_evals = state.next_extension_scalars_vec(n_instr_cols) + for i, e in enumerate(instr_evals): + table_values[n_runtime_cols + i] = e + + pref = pref_at(offset, log_n_rows) + retrieved_num = retrieved_num + pref # numerator is 1 + retrieved_den = retrieved_den + pref * ( + c + - finger_print( + Fp(dom_byte), + list(instr_evals) + [eval_on_pc], + alphas_eq_poly, + ) + ) + offset += 1 << log_n_rows + + # I] Bus (data flow between tables) + eval_on_selector = state.next_extension_scalar() + pref = pref_at(offset, log_n_rows) + retrieved_num = retrieved_num + pref * eval_on_selector + + eval_on_data = state.next_extension_scalar() + retrieved_den = retrieved_den + pref * eval_on_data + + bus_num_vals[name] = eval_on_selector + bus_den_vals[name] = eval_on_data + offset += 1 << log_n_rows + + # II] Lookups into memory + for lookup in meta.lookups: + index_eval = state.next_extension_scalar() + assert lookup.index not in table_values + table_values[lookup.index] = index_eval + + for i, col_index in enumerate(lookup.values): + value_eval = state.next_extension_scalar() + assert col_index not in table_values + table_values[col_index] = value_eval + + pref = pref_at(offset, log_n_rows) + retrieved_num = retrieved_num + pref + retrieved_den = retrieved_den + pref * ( + c + - finger_print( + Fp(dom_mem), + [value_eval, index_eval + EF.from_base(Fp(i))], + alphas_eq_poly, + ) + ) + offset += 1 << log_n_rows + + columns_values[name] = table_values + + # Padding tail (xxx..xxx111...1 region beyond `offset`). + retrieved_den = retrieved_den + mle_of_zeros_then_ones(offset, point_gkr) + + if retrieved_num != numerators_value: + raise ProofError("logup: numerators value mismatch") + if retrieved_den != denominators_value: + raise ProofError("logup: denominators value mismatch") + + return GenericLogupStatements( + memory_and_acc_point=list(memory_and_acc_point), + value_memory=value_memory, + value_memory_acc=value_memory_acc, + bytecode_and_acc_point=list(bytecode_and_acc_point), + value_bytecode_acc=value_bytecode_acc, + bus_numerators_values=bus_num_vals, + bus_denominators_values=bus_den_vals, + gkr_point=list(point_gkr), + columns_values=columns_values, + total_gkr_n_vars=total_gkr_n_vars, + bytecode_evaluation=Evaluation(point=bytecode_point, value=bytecode_value), + ) + + +def _next_pow_two(x: int) -> int: + if x <= 1: + return 1 + return 1 << (x - 1).bit_length() + + +# --------------------------------------------------------------------------- +# AIR sumcheck helpers (port of sub_protocols/air_sumcheck.rs) +# --------------------------------------------------------------------------- + + +def natural_ordering_point_for_session( + sumcheck_air_point: Sequence[EF], log_n_rows: int +) -> list[EF]: + """Mirror of `air_sumcheck::natural_ordering_point_for_session`. + + Takes the last `log_n_rows` coordinates of the AIR sumcheck point and + reverses them. + """ + if log_n_rows == 0: + return [] + return list(reversed(sumcheck_air_point[-log_n_rows:])) + + +def back_loaded_table_contribution( + bus_point: Sequence[EF], + sumcheck_air_point: Sequence[EF], + natural_ordering_point: Sequence[EF], + constraint_eval: EF, + eta_power: EF, +) -> EF: + """Mirror of `verify_execution::back_loaded_table_contribution`. + + eta^t · (Π i∈[0, n_max - n_t) sumcheck_point[i]) · eq(bus_point, natural_point) · constraint_eval + """ + n_t = len(bus_point) + n_max = len(sumcheck_air_point) + suffix_start = n_max - n_t + assert len(natural_ordering_point) == n_t + eq_val = eq_poly_outside(bus_point, natural_ordering_point) + k_t = EF.one() + for x in sumcheck_air_point[:suffix_start]: + k_t = k_t * x + return eta_power * k_t * eq_val * constraint_eval + + +def columns_evals_up_and_down( + n_columns: int, + down_column_indexes: Sequence[int], + col_evals: Sequence[EF], + natural_ordering_point: Sequence[EF], +) -> tuple[list[EF], dict[int, EF], dict[int, EF]]: + """Mirror of `air_sumcheck::columns_evals_up_and_down`. + + Splits `col_evals` into the per-column evaluation map plus the "next" + (= `down`) columns. Returns `(point, eq_values, next_values)`. + """ + n_up = n_columns + assert len(col_evals) == n_up + len(down_column_indexes) + eq_values = {i: col_evals[i] for i in range(n_up)} + next_values = { + idx: col_evals[n_up + j] for j, idx in enumerate(down_column_indexes) + } + return list(natural_ordering_point), eq_values, next_values + + +# --------------------------------------------------------------------------- +# Pluggable per-table AIR constraint evaluator +# --------------------------------------------------------------------------- + + +class ConstraintFolder: + """Mirror of `air::constraint_folder::ConstraintFolder` over EF. + + Each `assert_zero(x)` (or `assert_zero_ef`) contributes + `alpha_powers[constraint_index] · x` to the accumulator. + """ + + def __init__(self, up: Sequence[EF], down: Sequence[EF], alpha_powers: Sequence[EF]) -> None: + self.up = list(up) + self.down = list(down) + self.alpha_powers = list(alpha_powers) + self.accumulator: EF = EF.zero() + self.constraint_index = 0 + + def assert_zero(self, x: EF) -> None: + a = self.alpha_powers[self.constraint_index] + self.accumulator = self.accumulator + a * x + self.constraint_index += 1 + + # `assert_zero_ef` is identical in EF. + assert_zero_ef = assert_zero + + def assert_eq(self, x: EF, y: EF) -> None: + self.assert_zero(x - y) + + def assert_bool(self, x: EF) -> None: + # bool_check(x) = x * (1 - x). Zero iff x is 0 or 1. + self.assert_zero(x * (EF.one() - x)) + + +# Registry of per-table AIR constraint evaluators. Each function takes +# `(folder, table_meta, extra_data)` and emits constraints via the folder. +_AIR_EVALUATORS: dict[str, "callable"] = {} + + +def register_air_evaluator(name: str): + def decorator(fn): + _AIR_EVALUATORS[name] = fn + return fn + return decorator + + +def _eval_virtual_bus_column(extra_data: dict, flag: EF, data: Sequence[EF]) -> EF: + """Port of `tables::utils::eval_virtual_bus_column`. + + `(Σ alphas[i] · data[i] + alphas[-1] · LOGUP_PRECOMPILE_DOMAINSEP) · bus_beta + flag`. + """ + alphas: list[EF] = extra_data["logup_alphas_eq_poly"] + bus_beta: EF = extra_data["bus_beta"] + assert len(data) < len(alphas) + inner = EF.zero() + for a, d in zip(alphas, data): + inner = inner + a * d + inner = inner + alphas[-1] * EF.from_base(Fp(1)) # LOGUP_PRECOMPILE_DOMAINSEP = 1 + return inner * bus_beta + flag + + +def air_constraint_eval( + table: TableMeta, + col_evals: Sequence[EF], + alpha_powers: Sequence[EF], + extra_data: dict, +) -> EF: + """Evaluate the table's AIR constraint polynomial at `col_evals`. + + `col_evals[:n_columns]` is the `up` row, `col_evals[n_columns:]` is the + `down` row (only for tables with `down_column_indexes`). + `extra_data` carries `logup_alphas_eq_poly`, `bus_beta`, `c` (logup_c) — + used by the precompile bus constraints. + """ + n_up = table.n_columns + folder = ConstraintFolder(col_evals[:n_up], col_evals[n_up:], alpha_powers) + impl = _AIR_EVALUATORS.get(table.name) + if impl is None: + raise NotImplementedError(f"AIR evaluator not yet ported for table {table.name!r}") + impl(folder, table, extra_data) + return folder.accumulator + + +# --------------------------------------------------------------------------- +# Execution-table AIR (lean_vm/src/tables/execution/air.rs) +# --------------------------------------------------------------------------- + + +# Column indexes for the execution table (mirrors execution/air.rs). +_EXEC = { + "PC": 0, "FP": 1, + "MEM_ADDRESS_A": 2, "MEM_ADDRESS_B": 3, "MEM_ADDRESS_C": 4, + "MEM_VALUE_A": 5, "MEM_VALUE_B": 6, "MEM_VALUE_C": 7, + "OPERAND_A": 8, "OPERAND_B": 9, "OPERAND_C": 10, + "FLAG_A": 11, "FLAG_B": 12, "FLAG_C": 13, "FLAG_C_FP": 14, + "FLAG_AB_FP": 15, "MUL": 16, "JUMP": 17, "AUX": 18, "PRECOMPILE_DATA": 19, +} + + +@register_air_evaluator("execution") +def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: + up = folder.up + down = folder.down + one = EF.one() + + next_pc = down[0] + next_fp = down[1] + + operand_a = up[_EXEC["OPERAND_A"]] + operand_b = up[_EXEC["OPERAND_B"]] + operand_c = up[_EXEC["OPERAND_C"]] + flag_a = up[_EXEC["FLAG_A"]] + flag_b = up[_EXEC["FLAG_B"]] + flag_c = up[_EXEC["FLAG_C"]] + flag_c_fp = up[_EXEC["FLAG_C_FP"]] + flag_ab_fp = up[_EXEC["FLAG_AB_FP"]] + mul = up[_EXEC["MUL"]] + jump = up[_EXEC["JUMP"]] + aux = up[_EXEC["AUX"]] + precompile_data = up[_EXEC["PRECOMPILE_DATA"]] + + value_a = up[_EXEC["MEM_VALUE_A"]] + value_b = up[_EXEC["MEM_VALUE_B"]] + value_c = up[_EXEC["MEM_VALUE_C"]] + pc = up[_EXEC["PC"]] + fp = up[_EXEC["FP"]] + addr_a = up[_EXEC["MEM_ADDRESS_A"]] + addr_b = up[_EXEC["MEM_ADDRESS_B"]] + addr_c = up[_EXEC["MEM_ADDRESS_C"]] + + one_minus_flag_a_and_flag_ab_fp = -(flag_a + flag_ab_fp - one) + one_minus_flag_b_and_flag_ab_fp = -(flag_b + flag_ab_fp - one) + one_minus_flag_c_and_flag_c_fp = -(flag_c + flag_c_fp - one) + + nu_a = ( + flag_a * operand_a + + one_minus_flag_a_and_flag_ab_fp * value_a + + flag_ab_fp * (fp + operand_a) + ) + nu_b = ( + flag_b * operand_b + + one_minus_flag_b_and_flag_ab_fp * value_b + + flag_ab_fp * (fp + operand_b) + ) + nu_c = ( + flag_c * operand_c + + one_minus_flag_c_and_flag_c_fp * value_c + + flag_c_fp * (fp + operand_c) + ) + + fp_plus_op_a = fp + operand_a + fp_plus_op_b = fp + operand_b + fp_plus_op_c = fp + operand_c + pc_plus_one = pc + one + nu_a_minus_one = nu_a - one + + add = aux * EF.from_base(Fp(2)) - aux * aux + deref = _ef_halve(aux * (aux - one)) + is_precompile = -(add + mul + deref + jump - one) + + # Constraint 1: bus column (assert_zero_ef) + folder.assert_zero_ef( + _eval_virtual_bus_column( + extra_data, is_precompile, [precompile_data, nu_a, nu_b, nu_c] + ) + ) + + # Constraints 2-4: address consistency + folder.assert_zero(one_minus_flag_a_and_flag_ab_fp * (addr_a - fp_plus_op_a)) + folder.assert_zero(one_minus_flag_b_and_flag_ab_fp * (addr_b - fp_plus_op_b)) + folder.assert_zero(one_minus_flag_c_and_flag_c_fp * (addr_c - fp_plus_op_c)) + + # Constraints 5-6: add/mul + folder.assert_zero(add * (nu_b - (nu_a + nu_c))) + folder.assert_zero(mul * (nu_b - nu_a * nu_c)) + + # Constraints 7-8: deref + folder.assert_zero(deref * (addr_b - (value_a + operand_b))) + folder.assert_zero(deref * (value_b - nu_c)) + + # Constraints 9-13: jump + jump_and_condition = jump * nu_a + folder.assert_zero(jump_and_condition * nu_a_minus_one) + folder.assert_zero(jump_and_condition * (next_pc - nu_b)) + folder.assert_zero(jump_and_condition * (next_fp - nu_c)) + not_jump_and_condition = -(jump_and_condition - one) + folder.assert_zero(not_jump_and_condition * (next_pc - pc_plus_one)) + folder.assert_zero(not_jump_and_condition * (next_fp - fp)) + + +# --------------------------------------------------------------------------- +# Extension-op-table AIR (lean_vm/src/tables/extension_op/air.rs) +# --------------------------------------------------------------------------- + + +_EXT_OP_COL = { + "IS_BE": 0, "START": 1, "FLAG_ADD": 2, "FLAG_MUL": 3, "FLAG_POLY_EQ": 4, + "LEN": 5, "IDX_A": 6, "IDX_B": 7, "IDX_RES": 8, + "VA": 9, "VB": 14, "VRES": 19, "COMP": 24, +} + +_EXT_OP_FLAG_IS_BE = 4 +_EXT_OP_FLAG_ADD = 8 +_EXT_OP_FLAG_MUL = 16 +_EXT_OP_FLAG_POLY_EQ = 32 +_EXT_OP_LEN_MULTIPLIER = 64 + + +def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: + """Quintic-extension multiplication on 5-element EF arrays. + + Direct port of `quintic_mul` from koalabear/quintic_extension/extension.rs + using EF-level arithmetic — the dot-product becomes `Σ a[i]·b'[i]`. + """ + assert len(a) == 5 and len(b) == 5 + b0m3 = b[0] - b[3] + b1m4 = b[1] - b[4] + b4m2 = b[4] - b[2] + + def dot(av: Sequence[EF], bv: Sequence[EF]) -> EF: + acc = EF.zero() + for x, y in zip(av, bv): + acc = acc + x * y + return acc + + return [ + dot(a, [b[0], b[4], b[3], b[2], b1m4]), + dot(a, [b[1], b[0], b[4], b[3], b[2]]), + dot(a, [b[2], b1m4, b0m3, b4m2, b[3] - b1m4]), + dot(a, [b[3], b[2], b1m4, b0m3, b4m2]), + dot(a, [b[4], b[3], b[2], b1m4, b0m3]), + ] + + +@register_air_evaluator("extension_op") +def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: + up = folder.up + down = folder.down + one = EF.one() + + is_be = up[_EXT_OP_COL["IS_BE"]] + start = up[_EXT_OP_COL["START"]] + flag_add = up[_EXT_OP_COL["FLAG_ADD"]] + flag_mul = up[_EXT_OP_COL["FLAG_MUL"]] + flag_poly_eq = up[_EXT_OP_COL["FLAG_POLY_EQ"]] + len_col = up[_EXT_OP_COL["LEN"]] + idx_a = up[_EXT_OP_COL["IDX_A"]] + idx_b = up[_EXT_OP_COL["IDX_B"]] + idx_res = up[_EXT_OP_COL["IDX_RES"]] + + va = [up[_EXT_OP_COL["VA"] + k] for k in range(5)] + vb = [up[_EXT_OP_COL["VB"] + k] for k in range(5)] + vres = [up[_EXT_OP_COL["VRES"] + k] for k in range(5)] + comp = [up[_EXT_OP_COL["COMP"] + k] for k in range(5)] + + start_down = down[0] + is_be_down = down[1] + len_down = down[2] + flag_add_down = down[3] + flag_mul_down = down[4] + flag_poly_eq_down = down[5] + idx_a_down = down[6] + idx_b_down = down[7] + comp_down = [down[8 + k] for k in range(5)] + + active = flag_add + flag_mul + flag_poly_eq + activation_flag = start * active + + aux = ( + is_be * EF.from_base(Fp(_EXT_OP_FLAG_IS_BE)) + + flag_add * EF.from_base(Fp(_EXT_OP_FLAG_ADD)) + + flag_mul * EF.from_base(Fp(_EXT_OP_FLAG_MUL)) + + flag_poly_eq * EF.from_base(Fp(_EXT_OP_FLAG_POLY_EQ)) + + len_col * EF.from_base(Fp(_EXT_OP_LEN_MULTIPLIER)) + ) + + # Constraint 1: bus + folder.assert_zero_ef( + _eval_virtual_bus_column(extra_data, activation_flag, [aux, idx_a, idx_b, idx_res]) + ) + + is_ee = -(is_be - one) + not_start_down = -(start_down - one) + + va_f_or_ef = [va[0]] + [va[k] * is_ee for k in range(1, 5)] + comp_tail = [comp_down[k] * not_start_down for k in range(5)] + + # Constraints 2-6: bool flags + folder.assert_bool(is_be) + folder.assert_bool(start) + folder.assert_bool(flag_add) + folder.assert_bool(flag_mul) + folder.assert_bool(flag_poly_eq) + + # Constraints 7-11: add + for k in range(5): + folder.assert_zero((comp[k] - (va_f_or_ef[k] + vb[k] + comp_tail[k])) * flag_add) + + va_times_vb = _quintic_mul_ef(va_f_or_ef, vb) + + # Constraints 12-16: mul + for k in range(5): + folder.assert_zero((comp[k] - (va_times_vb[k] + comp_tail[k])) * flag_mul) + + # Constraints 17-21: poly_eq + poly_eq_val = [] + for k in range(5): + base = va_times_vb[k] + va_times_vb[k] - va_f_or_ef[k] - vb[k] + poly_eq_val.append(base + one if k == 0 else base) + comp_down_or_one = [] + for k in range(5): + if k == 0: + comp_down_or_one.append(comp_down[0] * not_start_down + start_down) + else: + comp_down_or_one.append(comp_down[k] * not_start_down) + poly_eq_result = _quintic_mul_ef(poly_eq_val, comp_down_or_one) + for k in range(5): + folder.assert_zero((comp[k] - poly_eq_result[k]) * flag_poly_eq) + + # Constraints 22-26: result matches comp when start + for k in range(5): + folder.assert_zero((comp[k] - vres[k]) * start) + + # Constraints 27-31: down-row consistency on non-start rows + folder.assert_zero(not_start_down * (len_col - len_down - one)) + folder.assert_zero(not_start_down * (is_be - is_be_down)) + folder.assert_zero(not_start_down * (flag_add - flag_add_down)) + folder.assert_zero(not_start_down * (flag_mul - flag_mul_down)) + folder.assert_zero(not_start_down * (flag_poly_eq - flag_poly_eq_down)) + + # Constraint 32-33: idx_a / idx_b increment + a_increment = is_be + is_ee * EF.from_base(Fp(5)) # DIMENSION = 5 + folder.assert_zero(not_start_down * (idx_a_down - idx_a - a_increment)) + folder.assert_zero(not_start_down * (idx_b_down - idx_b - EF.from_base(Fp(5)))) + + # Constraint 34: start_down enforces len=1 + folder.assert_zero(start_down * (len_col - one)) + + +# --------------------------------------------------------------------------- +# Poseidon16-compress AIR (lean_vm/src/tables/poseidon_16/mod.rs) +# --------------------------------------------------------------------------- + + +def _ef_cube(x: EF) -> EF: + return x * x * x + + +def _load_poseidon1_constants() -> dict: + import json + from pathlib import Path + + raw = json.loads( + (Path(__file__).with_name("poseidon1_constants.json")).read_text() + ) + + def to_fp_mat(m: list[list[int]]) -> list[list[Fp]]: + return [[Fp(v) for v in row] for row in m] + + return { + "half_full_rounds": raw["half_full_rounds"], + "partial_rounds": raw["partial_rounds"], + "initial_constants": to_fp_mat(raw["initial_constants"]), + "final_constants": to_fp_mat(raw["final_constants"]), + "sparse_m_i": to_fp_mat(raw["sparse_m_i"]), + "sparse_first_row": to_fp_mat(raw["sparse_first_row"]), + "sparse_v": to_fp_mat(raw["sparse_v"]), + "sparse_first_rc": [Fp(v) for v in raw["sparse_first_round_constants"]], + "sparse_scalar_rc": [Fp(v) for v in raw["sparse_scalar_round_constants"]], + "mds_dense": to_fp_mat(raw["mds_dense"]), + } + + +_POSEIDON1_CONSTS: dict | None = None + + +def _p1c() -> dict: + global _POSEIDON1_CONSTS + if _POSEIDON1_CONSTS is None: + _POSEIDON1_CONSTS = _load_poseidon1_constants() + return _POSEIDON1_CONSTS + + +_POSEIDON_WIDTH = 16 +_HALF_DIGEST_LEN = 4 +_POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1 # = 2 +_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT = 1 << 2 # = 4 +_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = 1 << 3 # = 8 + + +def _mds_dense_apply(state: list[EF]) -> list[EF]: + """state := mds_dense × state (dense MDS matrix multiplication).""" + mds = _p1c()["mds_dense"] + out: list[EF] = [] + for i in range(_POSEIDON_WIDTH): + acc = EF.zero() + for j in range(_POSEIDON_WIDTH): + acc = acc + state[j] * mds[i][j] + out.append(acc) + return out + + +def _add_kb_vec(state: list[EF], rc: list[Fp]) -> list[EF]: + return [s + EF.from_base(r) for s, r in zip(state, rc)] + + +def _cube_vec(state: list[EF]) -> list[EF]: + return [_ef_cube(s) for s in state] + + +def _eval_2_full_rounds( + folder: ConstraintFolder, + state: list[EF], + post_full_round: Sequence[EF], + rc1: list[Fp], + rc2: list[Fp], +) -> list[EF]: + """Mirror of `eval_2_full_rounds_16` (16 constraints emitted).""" + state = _cube_vec(_add_kb_vec(state, rc1)) + state = _mds_dense_apply(state) + state = _cube_vec(_add_kb_vec(state, rc2)) + state = _mds_dense_apply(state) + for i in range(_POSEIDON_WIDTH): + folder.assert_eq(state[i], post_full_round[i]) + state[i] = post_full_round[i] + return state + + +def _eval_last_2_full_rounds( + folder: ConstraintFolder, + initial_state: Sequence[EF], + state: list[EF], + outputs: Sequence[EF], + rc1: list[Fp], + rc2: list[Fp], + flag_half_output: EF, +) -> None: + """Mirror of `eval_last_2_full_rounds_16` (4 + 4 = 8 constraints).""" + state = _cube_vec(_add_kb_vec(state, rc1)) + state = _mds_dense_apply(state) + state = _cube_vec(_add_kb_vec(state, rc2)) + state = _mds_dense_apply(state) + # Davies-Meyer: state += initial_state. + state = [s + init for s, init in zip(state, initial_state)] + one_minus_half = EF.one() - flag_half_output + for idx in range(_POSEIDON_WIDTH // 2): + if idx < _HALF_DIGEST_LEN: + folder.assert_eq(state[idx], outputs[idx]) + else: + folder.assert_zero(one_minus_half * (state[idx] - outputs[idx])) + + +def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) -> None: + """Mirror of `eval_poseidon1_16`. Emits 80 (non-bus) constraints.""" + const = _p1c() + state = list(cols["inputs"]) + initial_state = list(cols["inputs"]) # used for compression at the end + + # --- initial full rounds (HALF_INITIAL_FULL_ROUNDS = 2) --- + half_initial = const["half_full_rounds"] // 2 + initial_consts = const["initial_constants"] + for r in range(half_initial): + state = _eval_2_full_rounds( + folder, state, cols["beginning_full_rounds"][r], + initial_consts[2 * r], initial_consts[2 * r + 1], + ) + + # --- transition into partial rounds (no constraints emitted here) --- + # Rust uses the sparse `m_i` matrix, NOT the dense MDS. + state = _add_kb_vec(state, const["sparse_first_rc"]) + state = _matvec_kb(const["sparse_m_i"], state) + + first_rows = const["sparse_first_row"] + v_vecs = const["sparse_v"] + scalar_rc = const["sparse_scalar_rc"] + n_partial = const["partial_rounds"] + for r in range(n_partial): + # S-box on state[0]; the cubed value is committed in `partial_rounds[r]`. + cubed = _ef_cube(state[0]) + folder.assert_eq(cubed, cols["partial_rounds"][r]) # assert_eq_low ≡ assert_eq + state[0] = cols["partial_rounds"][r] + if r < n_partial - 1: + state[0] = state[0] + scalar_rc[r] + # Sparse mat: new_s0 = first_row[r] · state; state[i] += old_s0 * v[r][i-1]. + old_s0 = state[0] + new_s0 = EF.zero() + for j in range(_POSEIDON_WIDTH): + new_s0 = new_s0 + state[j] * first_rows[r][j] + state[0] = new_s0 + for i in range(1, _POSEIDON_WIDTH): + state[i] = state[i] + old_s0 * v_vecs[r][i - 1] + + # --- ending full rounds (HALF_FINAL_FULL_ROUNDS - 1 = 1) --- + half_final = const["half_full_rounds"] // 2 + final_consts = const["final_constants"] + for r in range(half_final - 1): + state = _eval_2_full_rounds( + folder, state, cols["ending_full_rounds"][r], + final_consts[2 * r], final_consts[2 * r + 1], + ) + + # --- last 2 full rounds (8 constraints) --- + last_idx = 2 * (half_final - 1) + _eval_last_2_full_rounds( + folder, initial_state, state, cols["outputs"], + final_consts[last_idx], final_consts[last_idx + 1], + cols["flag_half_output"], + ) + + +def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: + """16x16 base-field matrix · EF-vector.""" + out = [] + for i in range(_POSEIDON_WIDTH): + acc = EF.zero() + for j in range(_POSEIDON_WIDTH): + acc = acc + state[j] * mat[i][j] + out.append(acc) + return out + + +@register_air_evaluator("poseidon16_compress") +def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: + up = folder.up + one = EF.one() + const = _p1c() + + # Decode the Poseidon1Cols16 layout. + o = 0 + flag_active = up[o]; o += 1 + index_b = up[o]; o += 1 + index_res = up[o]; o += 1 + flag_half_output = up[o]; o += 1 + flag_hardcoded_left = up[o]; o += 1 + offset_hardcoded_left = up[o]; o += 1 + effective_index_left_first = up[o]; o += 1 + effective_index_left_second = up[o]; o += 1 + inputs = up[o : o + _POSEIDON_WIDTH]; o += _POSEIDON_WIDTH + half_initial = const["half_full_rounds"] // 2 + beginning_full_rounds = [] + for _ in range(half_initial): + beginning_full_rounds.append(up[o : o + _POSEIDON_WIDTH]) + o += _POSEIDON_WIDTH + partial_cols = up[o : o + const["partial_rounds"]]; o += const["partial_rounds"] + half_final = const["half_full_rounds"] // 2 + ending_full_rounds = [] + for _ in range(half_final - 1): + ending_full_rounds.append(up[o : o + _POSEIDON_WIDTH]) + o += _POSEIDON_WIDTH + outputs = up[o : o + _POSEIDON_WIDTH // 2]; o += _POSEIDON_WIDTH // 2 + + precompile_data_reconstructed = ( + one + + flag_half_output * EF.from_base(Fp(_POSEIDON_HALF_OUTPUT_SHIFT)) + + flag_hardcoded_left * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT)) + + flag_hardcoded_left + * offset_hardcoded_left + * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT)) + ) + + one_minus_flag_hardcoded_left = one - flag_hardcoded_left + index_a = effective_index_left_second - one_minus_flag_hardcoded_left * EF.from_base( + Fp(_HALF_DIGEST_LEN) + ) + + # Constraint 1: bus + folder.assert_zero_ef( + _eval_virtual_bus_column( + extra_data, flag_active, [precompile_data_reconstructed, index_a, index_b, index_res] + ) + ) + + # Constraints 2-4: bool flags + folder.assert_bool(flag_active) + folder.assert_bool(flag_half_output) + folder.assert_bool(flag_hardcoded_left) + + # Constraints 5-6: hardcoded-left consistency + folder.assert_zero( + flag_hardcoded_left * (offset_hardcoded_left - effective_index_left_first) + ) + folder.assert_zero( + one_minus_flag_hardcoded_left * (index_a - effective_index_left_first) + ) + + _eval_poseidon1_16( + folder, + { + "inputs": list(inputs), + "beginning_full_rounds": [list(r) for r in beginning_full_rounds], + "partial_rounds": list(partial_cols), + "ending_full_rounds": [list(r) for r in ending_full_rounds], + "outputs": list(outputs), + "flag_half_output": flag_half_output, + }, + extra_data, + ) + + +# --------------------------------------------------------------------------- +# AIR-stage orchestration in verify_execution +# --------------------------------------------------------------------------- + + +@dataclass +class AirStageResult: + """Outputs of the AIR sumcheck stage, fed into the WHIR finale.""" + + sumcheck_air_point: list[EF] + bus_beta: EF + air_alpha: EF + eta: EF + committed_statements: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]] + public_memory_random_point: list[EF] + public_memory_eval: EF + + +def _max_air_constraints(tables: dict[str, TableMeta]) -> int: + # Hardcoded mirrors of `
::n_constraints` for each table. + NC = {"execution": 13, "extension_op": 16, "poseidon16_compress": 81} + return max(NC[t] for t in tables) + + +def _table_degree_air(table_name: str) -> int: + # Hardcoded mirrors of `
::degree_air`. + return {"execution": 5, "extension_op": 4, "poseidon16_compress": 10}[table_name] + + +def _table_down_column_indexes(table_name: str) -> list[int]: + """Hardcoded mirrors of `
::down_column_indexes`.""" + if table_name == "execution": + # COL_PC=0, COL_FP=1 + return [0, 1] + if table_name == "extension_op": + # COL_START, COL_IS_BE, COL_LEN, COL_FLAG_ADD, COL_FLAG_MUL, + # COL_FLAG_POLY_EQ, COL_IDX_A, COL_IDX_B, COL_COMP+0..5 + return [1, 0, 5, 2, 3, 4, 6, 7, 24, 25, 26, 27, 28] + if table_name == "poseidon16_compress": + return [] + raise KeyError(table_name) + + +def _table_n_down_columns(table_name: str) -> int: + return len(_table_down_column_indexes(table_name)) + + +def verify_air_stage( + state: VerifierState, + logup: GenericLogupStatements, + logup_c: EF, + logup_alphas_eq_poly: list[EF], + table_log_heights: dict[str, int], + tables: dict[str, TableMeta], + public_input: Sequence[Fp], + log_memory: int, +) -> AirStageResult: + """Port of the AIR-sumcheck block in `verify_execution.rs` (lines 100–179). + + Returns the per-table committed statements (point + eq_values + next_values) + and the public-memory random point + its evaluation. + """ + bus_beta = state.sample() + air_alpha = state.sample() + + max_air_constraints = _max_air_constraints(tables) + alpha_powers: list[EF] = [] + cur = EF.one() + for _ in range(max_air_constraints + 1): + alpha_powers.append(cur) + cur = cur * air_alpha + + eta = state.sample() + + tables_sorted = sort_tables_by_height(table_log_heights) + + # Build initial_sum. + initial_sum = EF.zero() + eta_powers: list[EF] = [] + cur = EF.one() + for name, _ in tables_sorted: + bus_num = logup.bus_numerators_values[name] + bus_den = logup.bus_denominators_values[name] + flag = ( + EF.zero() - EF.one() + if tables[name].bus.direction == "Pull" + else EF.one() + ) + bus_final_value = bus_num * flag + bus_beta * (bus_den - logup_c) + initial_sum = initial_sum + cur * bus_final_value + eta_powers.append(cur) + cur = cur * eta + + max_full_degree = max(_table_degree_air(name) + 1 for name, _ in tables_sorted) + n_max = tables_sorted[0][1] + + sumcheck_result = sumcheck_verify(state, n_max, max_full_degree, initial_sum, None) + sumcheck_air_point = sumcheck_result.point + claimed_air_final_value = sumcheck_result.value + + # Per-table loop: read col_evals, evaluate AIR, accumulate, build claims. + my_air_final_value = EF.zero() + + # Seed committed_statements with the logup entry per table, mirroring the + # init loop in `verify_execution.rs` (lines 88-98). + committed: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]] = {} + for name in tables: + log_n = table_log_heights[name] + logup_point = from_end(logup.gkr_point, log_n) + committed[name] = [ + (list(logup_point), dict(logup.columns_values[name]), {}), + ] + extra_data = { + "logup_alphas_eq_poly": logup_alphas_eq_poly, + "bus_beta": bus_beta, + "c": logup_c, + } + for (name, log_n_rows), eta_pow in zip(tables_sorted, eta_powers): + meta = tables[name] + n_down = _table_n_down_columns(name) + n_cols_total = meta.n_columns + n_down + col_evals = state.next_extension_scalars_vec(n_cols_total) + + alpha_powers_table = alpha_powers # same list — folder reads constraint_index + constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers_table, extra_data) + + bus_point = from_end(logup.gkr_point, log_n_rows) + natural_pt = natural_ordering_point_for_session(sumcheck_air_point, log_n_rows) + my_air_final_value = my_air_final_value + back_loaded_table_contribution( + bus_point, sumcheck_air_point, natural_pt, constraint_eval, eta_pow + ) + + point, eq_values, next_values = columns_evals_up_and_down( + meta.n_columns, + _table_down_column_indexes(name), + col_evals, + natural_pt, + ) + committed[name].append((point, eq_values, next_values)) + + if my_air_final_value != claimed_air_final_value: + raise ProofError("AIR sumcheck: my_air_final_value != claimed_air_final_value") + + # Public memory evaluation (length is next power of two of public_input). + public_memory = padd_with_zero_to_next_power_of_two(public_input) + log_pm = log2_strict_usize(len(public_memory)) + public_memory_random_point = state.sample_vec(log_pm) + public_memory_eval = eval_multilinear_evals( + [EF.from_base(f) for f in public_memory], public_memory_random_point + ) + + return AirStageResult( + sumcheck_air_point=list(sumcheck_air_point), + bus_beta=bus_beta, + air_alpha=air_alpha, + eta=eta, + committed_statements=committed, + public_memory_random_point=list(public_memory_random_point), + public_memory_eval=public_memory_eval, + ) + + +# --------------------------------------------------------------------------- +# Top-level verifier # --------------------------------------------------------------------------- @@ -1432,10 +2752,18 @@ class ProofVerificationDetails: @dataclass(frozen=True) class TableInfo: - """Minimal table metadata the verifier needs.""" + """Minimal table metadata the verifier needs (bus + lookups + n_columns). + + Built from the Rust-dumped table metadata via `tables_from_json`. + """ name: str n_columns: int + bus: Bus + lookups: tuple[Lookup, ...] + + def to_meta(self) -> TableMeta: + return TableMeta(self.name, self.n_columns, self.bus, self.lookups) @dataclass @@ -1448,6 +2776,8 @@ class VerifyExecutionPartial: table_log_heights: dict[str, int] stacked_n_vars: int parsed_commitment: ParsedCommitment + logup_statements: GenericLogupStatements + air_stage: AirStageResult | None = None def verify_execution( @@ -1455,16 +2785,20 @@ def verify_execution( public_input: Sequence[Fp], proof: Proof, tables: Sequence[TableInfo], + constants: dict, + bytecode_multilinear: list[Fp], ) -> VerifyExecutionPartial: """Port of `verify_execution` (lean_prover/src/verify_execution.rs). - Currently runs the prologue (dim/bound checks, transcript priming) and - parses the stacked-PCS WHIR commitment. Sub-protocols (logup, AIR sumcheck, - WHIR final verify) remain `NotImplementedError`. + Runs the prologue, parses the stacked-PCS WHIR commitment, samples the + logup challenges, and verifies the generic logup argument. AIR sumcheck + + WHIR final-eval are still TODO. `tables` must be in canonical Rust order (`ALL_TABLES`) — `execution` first, then `extension_op`, `poseidon16` — because the verifier reads per-table `log_n_rows` in that same order from the transcript. + + `constants` and `bytecode_multilinear` come from the Rust dump. """ state = VerifierState(proof) @@ -1485,7 +2819,6 @@ def verify_execution( for log_n_rows in table_log_n_rows: if log_n_rows < MIN_LOG_N_ROWS_PER_TABLE: raise ProofError("InvalidProof: table too small") - # TODO: per-table upper bound (max_log_n_rows_per_table). max_table_log = max(table_log_n_rows) if table_log_n_rows else 0 if log_memory < max(max_table_log, bytecode.log_size): @@ -1499,6 +2832,7 @@ def verify_execution( table_log_heights = {t.name: log_n_rows for t, log_n_rows in zip(tables, table_log_n_rows)} table_n_columns = {t.name: t.n_columns for t in tables} + tables_by_name = {t.name: t.to_meta() for t in tables} parsed_commitment = stacked_pcs_parse_commitment( state, @@ -1509,6 +2843,82 @@ def verify_execution( table_n_columns=table_n_columns, ) + # Logup challenges. + logup_c = state.sample() + max_bus_width = 1 + max( + constants["max_precompile_bus_width"], constants["n_instruction_columns"] + ) + logup_alphas = state.sample_vec(log2_ceil_usize(max_bus_width)) + logup_alphas_eq_poly = eval_eq(logup_alphas) + + logup_statements = verify_generic_logup( + state, + logup_c, + logup_alphas, + logup_alphas_eq_poly, + log_memory, + bytecode_multilinear, + table_log_heights, + tables_by_name, + constants, + ) + + air_stage = verify_air_stage( + state, + logup_statements, + logup_c, + logup_alphas_eq_poly, + table_log_heights, + tables_by_name, + public_input, + log_memory, + ) + + # Build the global WHIR statement list and run the final WHIR verify. + stacked_n_vars = parsed_commitment.num_variables + previous_statements = [ + SparseStatement( + total_num_variables=stacked_n_vars, + point=list(logup_statements.memory_and_acc_point), + values=[ + SparseValue(0, logup_statements.value_memory), + SparseValue(1, logup_statements.value_memory_acc), + ], + is_next=False, + ), + SparseStatement( + total_num_variables=stacked_n_vars, + point=list(air_stage.public_memory_random_point), + values=[SparseValue(0, air_stage.public_memory_eval)], + is_next=False, + ), + SparseStatement( + total_num_variables=stacked_n_vars, + point=list(logup_statements.bytecode_and_acc_point), + values=[ + SparseValue( + (2 << log_memory) >> bytecode.log_size, + logup_statements.value_bytecode_acc, + ), + ], + is_next=False, + ), + ] + + global_statements = stacked_pcs_global_statements( + stacked_n_vars, + log_memory, + bytecode.log_size, + previous_statements, + table_log_heights, + air_stage.committed_statements, + tables_by_name, + constants, + ) + + whir_cfg = whir_config(log_inv_rate, stacked_n_vars) + whir_verify(state, whir_cfg, parsed_commitment, global_statements) + return VerifyExecutionPartial( log_inv_rate=log_inv_rate, log_memory=log_memory, @@ -1516,6 +2926,8 @@ def verify_execution( table_log_heights=table_log_heights, stacked_n_vars=parsed_commitment.num_variables, parsed_commitment=parsed_commitment, + logup_statements=logup_statements, + air_stage=air_stage, ) @@ -1574,7 +2986,22 @@ def _smoke() -> None: bc = Bytecode(hash=[Fp(0)] * 8, log_size=10) bad_proof = Proof(transcript=[Fp(0)] * 64) try: - verify_execution(bc, [Fp(0)] * 4, bad_proof, tables=[]) + verify_execution( + bc, + [Fp(0)] * 4, + bad_proof, + tables=[], + constants={ + "n_instruction_columns": 12, + "n_runtime_columns": 8, + "col_pc": 0, + "logup_memory_domainsep": 0, + "logup_precompile_domainsep": 1, + "logup_bytecode_domainsep": 2, + "max_precompile_bus_width": 4, + }, + bytecode_multilinear=[Fp(0)] * 16, + ) except ProofError as e: print(f"verify_execution failed bound check (expected with dummy proof): {e}") From ac97fa4c13bf93bc8642e3932de6d6e2ba1f1742 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 17 May 2026 23:14:44 +0200 Subject: [PATCH 05/69] wip --- Cargo.lock | 2 - crates/lean_prover/test_gkr.py | 91 ----- crates/lean_prover/test_whir.py | 100 ----- crates/lean_prover/test_zkvm.py | 95 ----- crates/lean_prover/tests/dump_gkr_vector.rs | 188 --------- crates/lean_prover/tests/dump_zkvm_vector.rs | 154 ++++--- crates/lean_prover/verifier.py | 400 ++++++++++--------- crates/whir/Cargo.toml | 2 - crates/whir/tests/dump_test_vectors.rs | 384 ------------------ 9 files changed, 275 insertions(+), 1141 deletions(-) delete mode 100644 crates/lean_prover/test_gkr.py delete mode 100644 crates/lean_prover/test_whir.py delete mode 100644 crates/lean_prover/test_zkvm.py delete mode 100644 crates/lean_prover/tests/dump_gkr_vector.rs delete mode 100644 crates/whir/tests/dump_test_vectors.rs diff --git a/Cargo.lock b/Cargo.lock index f7b279a83..ab658e6b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,8 +711,6 @@ dependencies = [ "mt-utils", "rand", "rayon", - "serde", - "serde_json", "system-info", "tracing", "tracing-forest", diff --git a/crates/lean_prover/test_gkr.py b/crates/lean_prover/test_gkr.py deleted file mode 100644 index 5af87de52..000000000 --- a/crates/lean_prover/test_gkr.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Validate the Python `verify_gkr_quotient` against the Rust dump. - -Regenerate the vectors with: - cargo test --release -p lean_prover --test dump_gkr_vector -- --nocapture - -Then run: - .venv/bin/python crates/lean_prover/test_gkr.py -""" - -import json -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent)) - -from verifier import ( # noqa: E402 - EF, - Fp, - MerkleOpening, - Proof, - VerifierState, - prunedpaths_from_json, - restore_merkle_paths, - verify_gkr_quotient, -) - - -def _ef(coords) -> EF: - return EF([Fp(v) for v in coords]) - - -def run(path: Path) -> bool: - raw = json.loads(path.read_text()) - transcript = [Fp(v) for v in raw["proof"]["transcript"]] - openings: list[MerkleOpening] = [] - for bucket in raw["proof"]["merkle_paths"]: - for r in restore_merkle_paths(prunedpaths_from_json(bucket)): - openings.append(MerkleOpening(leaf_data=r.leaf_data, path=r.sibling_hashes)) - proof = Proof(transcript=transcript, merkle_openings=openings) - - state = VerifierState(proof) - try: - quotient, point, claims_num, claims_den = verify_gkr_quotient(state, raw["n_vars"]) - except Exception as e: - print(f" {path.name}: FAILED: {type(e).__name__}: {e}") - return False - - expected_quotient = _ef(raw["expected_quotient"]) - expected_point = [_ef(p) for p in raw["expected_point"]] - expected_num = _ef(raw["expected_claims_num"]) - expected_den = _ef(raw["expected_claims_den"]) - - ok = True - if quotient != expected_quotient: - ok = False - print(f" {path.name}: quotient mismatch") - if len(point) != len(expected_point): - ok = False - print(f" {path.name}: gkr_point length mismatch ({len(point)} vs {len(expected_point)})") - else: - for i, (a, b) in enumerate(zip(point, expected_point)): - if a != b: - ok = False - print(f" {path.name}: gkr_point[{i}] mismatch") - break - if claims_num != expected_num: - ok = False - print(f" {path.name}: claims_num mismatch") - if claims_den != expected_den: - ok = False - print(f" {path.name}: claims_den mismatch") - - if ok: - print(f" {path.name}: OK") - return ok - - -def main() -> int: - out_dir = Path(__file__).resolve().parents[2] / "target" / "gkr_test_vectors" - vectors = sorted(out_dir.glob("*.json")) - if not vectors: - print(f"No test vectors in {out_dir}.") - return 1 - ok = True - for v in vectors: - ok &= run(v) - return 0 if ok else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/crates/lean_prover/test_whir.py b/crates/lean_prover/test_whir.py deleted file mode 100644 index 05758d782..000000000 --- a/crates/lean_prover/test_whir.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Run the Python WHIR verifier against the Rust-generated test vectors. - -Generate the vectors first (in this repo's `target/whir_test_vectors/`): - cargo test --release -p mt-whir --test dump_test_vectors -- --nocapture -Then run: - .venv/bin/python crates/lean_prover/test_whir.py -""" - -import json -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent)) - -from verifier import ( # noqa: E402 - EF, - Fp, - MerkleOpening, - ParsedCommitment, - Proof, - ProofError, - SparseStatement, - SparseValue, - VerifierState, - prunedpaths_from_json, - restore_merkle_paths, - whir_config, - whir_verify, -) - - -def _ef(coords: list[int]) -> EF: - return EF([Fp(v) for v in coords]) - - -def _load(path: Path) -> tuple: - raw = json.loads(path.read_text()) - statement = [ - SparseStatement( - total_num_variables=s["total_num_variables"], - point=[_ef(p) for p in s["point"]], - values=[SparseValue(v["selector"], _ef(v["value"])) for v in s["values"]], - is_next=s["is_next"], - ) - for s in raw["statement"] - ] - transcript = [Fp(v) for v in raw["proof"]["transcript"]] - openings: list[MerkleOpening] = [] - for bucket in raw["proof"]["merkle_paths"]: - restored = restore_merkle_paths(prunedpaths_from_json(bucket)) - for r in restored: - openings.append(MerkleOpening(leaf_data=r.leaf_data, path=r.sibling_hashes)) - proof = Proof(transcript=transcript, merkle_openings=openings) - expected = [_ef(p) for p in raw["expected_folding_randomness"]] - return raw["num_variables"], raw["log_inv_rate"], statement, proof, expected - - -def run(path: Path) -> bool: - num_vars, log_inv_rate, statement, proof, expected = _load(path) - cfg = whir_config(log_inv_rate, num_vars) - state = VerifierState(proof) - parsed = ParsedCommitment( - num_variables=num_vars, - root=state.next_base_scalars_vec(8), - ood_points=state.sample_vec(cfg.commitment_ood_samples) if cfg.commitment_ood_samples > 0 else [], - ood_answers=[], - ) - if cfg.commitment_ood_samples > 0: - parsed.ood_answers = state.next_extension_scalars_vec(cfg.commitment_ood_samples) - try: - got = whir_verify(state, cfg, parsed, statement) - except ProofError as e: - print(f" {path.name}: ProofError: {e}") - return False - if len(got) != len(expected): - print(f" {path.name}: evaluation-point length mismatch ({len(got)} vs {len(expected)})") - return False - for i, (a, b) in enumerate(zip(got, expected)): - if a != b: - print(f" {path.name}: evaluation_point[{i}] mismatch (got {a}, expected {b})") - return False - print(f" {path.name}: OK") - return True - - -def main() -> int: - out_dir = Path(__file__).resolve().parents[2] / "target" / "whir_test_vectors" - skip = {"challenger_sanity.json", "merkle_sanity.json", "state_trace.json", "permute_oracle.json"} - vectors = sorted(p for p in out_dir.glob("*.json") if p.name not in skip) - if not vectors: - print(f"No test vectors in {out_dir}.") - return 1 - ok = True - for v in vectors: - ok &= run(v) - return 0 if ok else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/crates/lean_prover/test_zkvm.py b/crates/lean_prover/test_zkvm.py deleted file mode 100644 index c10c5862f..000000000 --- a/crates/lean_prover/test_zkvm.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Run the Python `verify_execution` (prologue + stacked-PCS + generic logup) -against the Rust-generated zkVM test vector. - -Regenerate the vector with: - cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture - -Then run: - .venv/bin/python crates/lean_prover/test_zkvm.py -""" - -import array -import json -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent)) - -from verifier import ( # noqa: E402 - Bytecode, - Fp, - MerkleOpening, - Proof, - TableInfo, - prunedpaths_from_json, - restore_merkle_paths, - tables_from_json, - verify_execution, -) - - -def _load_bytecode_mle(json_path: Path, raw: dict) -> list[Fp]: - bin_path = json_path.parent / raw["bytecode_multilinear_path"] - data = bin_path.read_bytes() - n = raw["bytecode_multilinear_len"] - assert len(data) == n * 4, (len(data), n * 4) - arr = array.array("I") - arr.frombytes(data) - return [Fp(v) for v in arr] - - -def _load(path: Path): - raw = json.loads(path.read_text()) - bytecode = Bytecode( - hash=[Fp(v) for v in raw["bytecode_hash"]], - log_size=raw["bytecode_log_size"], - ) - public_input = [Fp(v) for v in raw["public_input"]] - transcript = [Fp(v) for v in raw["proof"]["transcript"]] - openings: list[MerkleOpening] = [] - for bucket in raw["proof"]["merkle_paths"]: - for r in restore_merkle_paths(prunedpaths_from_json(bucket)): - openings.append(MerkleOpening(leaf_data=r.leaf_data, path=r.sibling_hashes)) - proof = Proof(transcript=transcript, merkle_openings=openings) - - metas = tables_from_json(raw["tables"]) - tables = [ - TableInfo(name=m.name, n_columns=m.n_columns, bus=m.bus, lookups=m.lookups) - for m in metas - ] - bytecode_mle = _load_bytecode_mle(path, raw) - return bytecode, public_input, proof, tables, raw["constants"], bytecode_mle - - -def run(path: Path) -> bool: - bytecode, public_input, proof, tables, constants, bytecode_mle = _load(path) - try: - partial = verify_execution(bytecode, public_input, proof, tables, constants, bytecode_mle) - except Exception as e: - print(f" {path.name}: FAILED: {type(e).__name__}: {e}") - return False - logup = partial.logup_statements - print( - f" {path.name}: OK " - f"log_inv_rate={partial.log_inv_rate}, log_memory={partial.log_memory}, " - f"stacked_n_vars={partial.stacked_n_vars}, " - f"gkr_n_vars={logup.total_gkr_n_vars}, " - f"bytecode_eval_point.len={len(logup.bytecode_evaluation.point)}" - ) - return True - - -def main() -> int: - out_dir = Path(__file__).resolve().parents[2] / "target" / "zkvm_test_vectors" - vectors = sorted(out_dir.glob("*.json")) - if not vectors: - print(f"No test vectors in {out_dir}.") - return 1 - ok = True - for v in vectors: - ok &= run(v) - return 0 if ok else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/crates/lean_prover/tests/dump_gkr_vector.rs b/crates/lean_prover/tests/dump_gkr_vector.rs deleted file mode 100644 index 18a0840c4..000000000 --- a/crates/lean_prover/tests/dump_gkr_vector.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Focused test vector for the GKR-quotient sub-protocol. Independent of the -//! rest of the zkVM verifier so the Python port can be validated in isolation. -//! -//! Run: -//! cargo test --release -p lean_prover --test dump_gkr_vector -- --nocapture - -use std::fs; -use std::path::PathBuf; - -use backend::{ - BasedVectorSpace, Field, MleOwned, PackedValue, PrimeCharacteristicRing, PrimeField32, - ProverState, PrunedMerklePaths, VerifierState, default_koalabear_poseidon1_16, pack_extension, - packing_log_width, -}; -use lean_vm::{EF, F}; -use rand::{RngExt, SeedableRng, rngs::StdRng}; -use serde::Serialize; -use sub_protocols::{ENDIANNESS_PIVOT_GKR, prove_gkr_quotient, verify_gkr_quotient}; - -const DIGEST_ELEMS: usize = 8; - -fn f_to_u32(x: F) -> u32 { - x.as_canonical_u32() -} - -fn ef_to_u32s(x: EF) -> [u32; 5] { - let coords: &[F] = x.as_basis_coefficients_slice(); - [ - f_to_u32(coords[0]), - f_to_u32(coords[1]), - f_to_u32(coords[2]), - f_to_u32(coords[3]), - f_to_u32(coords[4]), - ] -} - -#[derive(Serialize)] -struct PrunedPathJson { - leaf_index: usize, - siblings: Vec<[u32; DIGEST_ELEMS]>, -} - -#[derive(Serialize)] -struct PrunedMerklePathsJson { - merkle_height: usize, - original_order: Vec, - leaf_data: Vec>, - paths: Vec, - n_trailing_zeros: usize, -} - -#[derive(Serialize)] -struct ProofJson { - transcript: Vec, - merkle_paths: Vec, -} - -#[derive(Serialize)] -struct GkrOut { - name: String, - n_vars: usize, - expected_quotient: [u32; 5], - expected_point: Vec<[u32; 5]>, - expected_claims_num: [u32; 5], - expected_claims_den: [u32; 5], - proof: ProofJson, -} - -fn convert_pruned(p: &PrunedMerklePaths) -> PrunedMerklePathsJson { - PrunedMerklePathsJson { - merkle_height: p.merkle_height, - original_order: p.original_order.clone(), - leaf_data: p - .leaf_data - .iter() - .map(|v| v.iter().map(|&f| f_to_u32(f)).collect()) - .collect(), - paths: p - .paths - .iter() - .map(|(idx, siblings)| PrunedPathJson { - leaf_index: *idx, - siblings: siblings.iter().map(|d| d.map(f_to_u32)).collect(), - }) - .collect(), - n_trailing_zeros: p.n_trailing_zeros, - } -} - -/// Copy of the helper used in the existing GKR test (bit-reverse the input -/// chunks at `chunk_log`). -fn bit_reverse_chunks(v: &[T], chunk_log: usize) -> Vec { - let chunk = 1 << chunk_log; - let shift = (usize::BITS as usize) - chunk_log; - let mut out = Vec::with_capacity(v.len()); - for chunk_start in (0..v.len()).step_by(chunk) { - for i in 0..chunk { - out.push(v[chunk_start + (i.reverse_bits() >> shift)]); - } - } - out -} - -fn run_one(name: &str, log_n: usize, seed: u64, out_dir: &PathBuf) { - let poseidon16 = default_koalabear_poseidon1_16(); - let pivot = ENDIANNESS_PIVOT_GKR.min(log_n); - let n = 1usize << log_n; - - let mut rng = StdRng::seed_from_u64(seed); - let c: EF = rng.random(); - let numerators_raw: Vec = (0..n).map(|_| rng.random()).collect(); - let denominators_raw: Vec = (0..n).map(|_| c - F::from_usize(rng.random_range(..n))).collect(); - - // Bit-reverse + pack the inputs at `pivot` (prover convention). - let w = packing_log_width::(); - let nums_br = { - let mut br = vec![F::ZERO; n]; - let chunk = 1 << pivot; - let shift = (usize::BITS as usize) - pivot; - for c_start in (0..n).step_by(chunk) { - for i in 0..chunk { - br[c_start + i] = numerators_raw[c_start + (i.reverse_bits() >> shift)]; - } - } - use backend::PFPacking; - let packed: Vec> = PFPacking::::pack_slice(&br).to_vec(); - packed - }; - let dens_br: Vec> = pack_extension(&bit_reverse_chunks(&denominators_raw, pivot)); - let _ = w; - - let mut prover_state = ProverState::::new(poseidon16.clone()); - let (quotient_prover, claim_point_prover) = - prove_gkr_quotient::(&mut prover_state, &nums_br, &dens_br, pivot); - let proof = prover_state.into_proof(); - - let mut verifier_state = VerifierState::::new(proof.clone(), poseidon16).unwrap(); - let (retrieved_quotient, claim_point, claim_num, claim_den) = - verify_gkr_quotient::(&mut verifier_state, log_n).unwrap(); - assert_eq!(quotient_prover, retrieved_quotient); - assert_eq!(claim_point_prover, claim_point); - - // sanity: evaluate the raw inputs at claim_point and check they match. - let nums_nat = MleOwned::::Base(numerators_raw.clone()); - let dens_nat = MleOwned::::Extension(denominators_raw.clone()); - assert_eq!(nums_nat.evaluate(&claim_point), claim_num); - assert_eq!(dens_nat.evaluate(&claim_point), claim_den); - - let out = GkrOut { - name: name.to_string(), - n_vars: log_n, - expected_quotient: ef_to_u32s(retrieved_quotient), - expected_point: claim_point.0.iter().map(|&p| ef_to_u32s(p)).collect(), - expected_claims_num: ef_to_u32s(claim_num), - expected_claims_den: ef_to_u32s(claim_den), - proof: ProofJson { - transcript: proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), - merkle_paths: proof.merkle_paths.iter().map(convert_pruned).collect(), - }, - }; - - let path = out_dir.join(format!("{name}.json")); - fs::write(&path, serde_json::to_string(&out).unwrap()).unwrap(); - println!( - "{} -> {} ({:.1} KiB) [n_vars={}, transcript_len={}]", - name, - path.display(), - path.metadata().unwrap().len() as f64 / 1024.0, - log_n, - out.proof.transcript.len(), - ); -} - -#[test] -fn dump_gkr_vector() { - let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("gkr_test_vectors"); - fs::create_dir_all(&out_dir).unwrap(); - - // n_vars must exceed N_VARS_TO_SEND_GKR_COEFFS=5, and `prove_gkr_quotient` - // requires `pivot > w` (w = packing_log_width). pivot is capped at - // ENDIANNESS_PIVOT_GKR = 12, but for small inputs it shrinks to log_n — - // make sure to stay above the packing width. - run_one("small_nv13", 13, 0xdead_beef, &out_dir); -} diff --git a/crates/lean_prover/tests/dump_zkvm_vector.rs b/crates/lean_prover/tests/dump_zkvm_vector.rs index b16780ce6..6a521ab83 100644 --- a/crates/lean_prover/tests/dump_zkvm_vector.rs +++ b/crates/lean_prover/tests/dump_zkvm_vector.rs @@ -1,24 +1,22 @@ -//! Dumps a tiny zkVM proof + metadata so the Python `verify_execution` -//! port (`crates/lean_prover/verifier.py`) can run against it. +//! Single end-to-end test vector for the Python verifier: aggregate one XMSS +//! signature using rec-aggregation, then dump the resulting bytecode, public +//! input, table metadata, and proof. //! //! Run: //! cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture //! -//! Output: `target/zkvm_test_vectors/small.json`. The JSON contains everything -//! Python needs to mirror `verify_execution.rs` up to (and through) any -//! sub-protocol we've ported so far. +//! Output: `target/zkvm_test_vectors/proof.json` + `proof.bytecode_mle.bin`. use std::fs; use std::path::PathBuf; -use backend::{Air, PrimeField32, PrunedMerklePaths, WhirConfigBuilder}; -use lean_compiler::*; -use lean_prover::{default_whir_config, prove_execution::prove_execution, verify_execution::verify_execution}; -// `verify_execution` is imported so the dump test self-checks the proof -// before writing it. +use backend::{Air, PrimeField32, PrunedMerklePaths}; use lean_vm::*; +use rec_aggregation::{aggregate_type_1, get_aggregation_bytecode, init_aggregation_bytecode, verify_type_1}; use serde::Serialize; use std::io::Write; +use utils::poseidon_compress_slice; +use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; type F = lean_vm::F; @@ -49,18 +47,6 @@ struct ProofJson { merkle_paths: Vec, } -#[derive(Serialize)] -struct BuilderJson { - security_level: usize, - max_num_variables_to_send_coeffs: usize, - pow_bits: usize, - folding_factor_first: usize, - folding_factor_subsequent: usize, - starting_log_inv_rate: usize, - rs_domain_initial_reduction_factor: usize, - soundness_type: &'static str, -} - #[derive(Serialize)] #[serde(tag = "kind", content = "value")] enum BusDataJson { @@ -104,18 +90,24 @@ struct ConstantsJson { #[derive(Serialize)] struct OutJson { - name: String, + /// Aggregation bytecode metadata. The multilinear is in the sidecar. bytecode_log_size: usize, bytecode_hash: [u32; DIGEST_ELEMS], - /// Path (relative to JSON) of the raw u32-LE bytecode multilinear sidecar. bytecode_multilinear_path: String, bytecode_multilinear_len: usize, - public_input: Vec, + + /// Public input to `verify_execution` (the hashed `input_data`). + public_input: [u32; DIGEST_ELEMS], + + /// Pre-image of `public_input`. Dumped so Python can re-derive the hash. + input_data: Vec, + + /// Per-table metadata + global constants. n_tables: usize, tables: Vec, constants: ConstantsJson, snark_domain_sep: [u32; DIGEST_ELEMS], - builder: BuilderJson, + proof: ProofJson, } @@ -140,17 +132,39 @@ fn convert_pruned(p: &PrunedMerklePaths) -> PrunedMerklePathsJson { } } -fn dump_one(name: &str, program_str: &str, public_input: Vec, out_dir: &PathBuf) { - let bytecode = compile_program(&ProgramSource::Raw(program_str.to_string())); - let starting_log_inv_rate = 1; - let builder: WhirConfigBuilder = default_whir_config(starting_log_inv_rate); - let witness = ExecutionWitness::default(); - let exec_proof = prove_execution(&bytecode, &public_input, &witness, &builder, false) - .expect("prove_execution failed"); +#[test] +fn dump_zkvm_vector() { + let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(&target_dir) + .join("zkvm_test_vectors"); + fs::create_dir_all(&out_dir).unwrap(); + + // Compile the aggregation program once. + init_aggregation_bytecode(); + let bytecode = get_aggregation_bytecode(); + + // Aggregate one raw XMSS signature into a TypeOneMultiSignature. + let sig = { + let (pk, xmss_sig) = get_benchmark_signatures()[0].clone(); + aggregate_type_1( + &[], + vec![(pk, xmss_sig)], + message_for_benchmark(), + BENCHMARK_SLOT, + /* log_inv_rate = */ 1, + ) + .expect("aggregate_type_1 failed") + }; - // Run the Rust verifier as a self-check before writing the dump. - verify_execution(&bytecode, &public_input, exec_proof.proof.clone()) - .expect("Rust verify_execution failed"); + // `verify_type_1` rebuilds `input_data` from the public info and runs the + // Rust verifier as a self-check. We grab `input_data` from the returned + // `InnerVerified` and reuse `sig.proof.proof` for the serialized proof. + let proof = sig.proof.proof.clone(); + let verified = verify_type_1(&sig).expect("Rust verify_type_1 failed"); + let input_data = verified.input_data; + let public_input = poseidon_compress_slice(&input_data, true); let convert_bus = |bus: Bus| BusJson { direction: match bus.direction { @@ -182,24 +196,22 @@ fn dump_one(name: &str, program_str: &str, public_input: Vec, out_dir: &PathB }) .collect(); - // Dump the bytecode multilinear to a sidecar binary file. Length is - // `2^cumulated_n_vars` where `cumulated_n_vars = log_size + ceil(log2(N_INSTRUCTION_COLUMNS))`. - let mle_path_rel = format!("{name}.bytecode_mle.bin"); - let mle_full_path = out_dir.join(&mle_path_rel); + // Sidecar: raw u32-LE bytecode multilinear. + let mle_path = "proof.bytecode_mle.bin"; { - let mut f = fs::File::create(&mle_full_path).unwrap(); + let mut f = fs::File::create(out_dir.join(mle_path)).unwrap(); for v in &bytecode.instructions_multilinear { f.write_all(&f_to_u32(*v).to_le_bytes()).unwrap(); } } let out = OutJson { - name: name.to_string(), bytecode_log_size: bytecode.log_size(), bytecode_hash: bytecode.hash.map(f_to_u32), - bytecode_multilinear_path: mle_path_rel.clone(), + bytecode_multilinear_path: mle_path.to_string(), bytecode_multilinear_len: bytecode.instructions_multilinear.len(), - public_input: public_input.iter().map(|&f| f_to_u32(f)).collect(), + public_input: public_input.map(f_to_u32), + input_data: input_data.iter().map(|&f| f_to_u32(f)).collect(), n_tables: N_TABLES, tables: table_infos, constants: ConstantsJson { @@ -214,51 +226,27 @@ fn dump_one(name: &str, program_str: &str, public_input: Vec, out_dir: &PathB ending_pc: ENDING_PC, }, snark_domain_sep: lean_prover::SNARK_DOMAIN_SEP.map(f_to_u32), - builder: BuilderJson { - security_level: 124, - max_num_variables_to_send_coeffs: 8, - pow_bits: 16, - folding_factor_first: 7, - folding_factor_subsequent: 5, - starting_log_inv_rate, - rs_domain_initial_reduction_factor: 5, - soundness_type: "JohnsonBound", - }, proof: ProofJson { - transcript: exec_proof.proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), - merkle_paths: exec_proof.proof.merkle_paths.iter().map(convert_pruned).collect(), + transcript: proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), + merkle_paths: proof.merkle_paths.iter().map(convert_pruned).collect(), }, }; - let path = out_dir.join(format!("{name}.json")); - fs::write(&path, serde_json::to_string(&out).unwrap()).unwrap(); + let json_path = out_dir.join("proof.json"); + fs::write(&json_path, serde_json::to_string(&out).unwrap()).unwrap(); + + let mle_bytes = out_dir.join(mle_path).metadata().unwrap().len(); + println!( + "wrote test vector:\n {} ({:.1} KiB)\n {} ({:.1} KiB)", + json_path.display(), + json_path.metadata().unwrap().len() as f64 / 1024.0, + out_dir.join(mle_path).display(), + mle_bytes as f64 / 1024.0, + ); println!( - "{} -> {} ({:.1} KiB; bytecode_log_size={}, transcript_len={})", - name, - path.display(), - path.metadata().unwrap().len() as f64 / 1024.0, + " bytecode_log_size={}, transcript_len={}, input_data_len={}", out.bytecode_log_size, out.proof.transcript.len(), + out.input_data.len(), ); } - -#[test] -fn dump_zkvm_vector() { - let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("zkvm_test_vectors"); - fs::create_dir_all(&out_dir).unwrap(); - - // Use a small program (no big unroll). Empty public input. The compiler - // pads bytecode to at least MIN_BYTECODE_LOG_SIZE so we still get a valid - // proof; keeping the bytecode small keeps the dumped multilinear small. - let small_program = r#" -def main(): - a = Array(1) - a[0] = 1 * 2 - return -"#; - dump_one("small", small_program, vec![], &out_dir); -} diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index aa399bf19..c405432d9 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1,36 +1,49 @@ -""" -Setup: +"""Pure-Python verifier for leanVM execution proofs. + +Single end-to-end test vector — a rec-aggregation proof over one XMSS +signature — is generated by the Rust side and stored under `target/`. +Run this script to verify it. + +Setup (one-time): + # Python venv + lean_spec (gives us KoalaBear `Fp` and the Poseidon1 + # permutation). uv venv .venv --python 3.12 VIRTUAL_ENV=.venv uv pip install "git+https://github.com/leanEthereum/leanSpec.git" + + # Rust-side data (hardcoded soundness numbers + Poseidon round constants). + cargo test --release -p lean_prover --test dump_whir_configs -- --nocapture + cargo test --release -p lean_prover --test dump_poseidon1_constants -- --nocapture + + # The end-to-end test vector (~17 MiB; takes a minute or two). + cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture + +Run: .venv/bin/python crates/lean_prover/verifier.py """ from __future__ import annotations from dataclasses import dataclass, field -from typing import Iterable, Sequence +from typing import Sequence from lean_spec.subspecs.koalabear import Fp, P from lean_spec.subspecs.poseidon1 import PARAMS_16, Poseidon1 -SECURITY_BITS = 124 -MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 +# WHIR builder constants (lean_prover/src/lib.rs). WHIR_INITIAL_FOLDING_FACTOR = 7 WHIR_SUBSEQUENT_FOLDING_FACTOR = 5 +MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 -# Poseidon16 challenger parameters (challenger.rs). -# Note: this branch uses the older "compression with domain separator" design. -# The state is just the RATE-sized output of the last permute; sampling pulls -# fresh hashes by re-permuting with a per-call domain separator. There is no -# `rate_fresh` flag and no `duplex` call. +# Poseidon16 sponge parameters. This branch uses the older compression-with- +# domain-separator challenger (no `rate_fresh`/`duplex`); state is RATE-sized +# and sampling re-permutes with a per-call domain separator. RATE = 8 WIDTH = 16 CAPACITY = WIDTH - RATE -DIGEST_LEN_FE = 8 DIGEST_ELEMS = 8 -# Hardcoded leanVM SNARK domain separator (lean_prover/src/lib.rs) +# leanVM SNARK domain separator (lean_prover/src/lib.rs). SNARK_DOMAIN_SEP = [ Fp(v) for v in ( 130704175, 1303721200, 493664240, 1035493700, @@ -38,11 +51,9 @@ ) ] -# Bounds (mirrors lean_vm/src/core/constants.rs). -MIN_WHIR_LOG_INV_RATE = 1 -MAX_WHIR_LOG_INV_RATE = 4 -MIN_LOG_MEMORY_SIZE = 16 -MAX_LOG_MEMORY_SIZE = 26 +# Bounds (lean_vm/src/core/constants.rs). +MIN_WHIR_LOG_INV_RATE, MAX_WHIR_LOG_INV_RATE = 1, 4 +MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 MIN_LOG_N_ROWS_PER_TABLE = 8 MIN_BYTECODE_LOG_SIZE = 8 BASE_TWO_ADICITY = 24 # KoalaBear @@ -52,19 +63,18 @@ WHIR_CONFIGS_PATH = "whir_configs.json" -# --------------------------------------------------------------------------- -# Error type -# --------------------------------------------------------------------------- + +# ─── Error type ────────────────────────────────────────────────────────── class ProofError(Exception): """Mirrors backend::ProofError.""" -# --------------------------------------------------------------------------- + # Quintic extension field: EF = Fp[X] / (X^5 + X^2 - 1) # Reduction rule: X^5 = 1 - X^2. -# --------------------------------------------------------------------------- + class EF: @@ -179,9 +189,8 @@ def pow(self, n: int) -> "EF": return result -# --------------------------------------------------------------------------- -# Poseidon16-based Challenger (duplex sponge) -# --------------------------------------------------------------------------- + +# ─── Poseidon16-based Challenger (duplex sponge) ────────────────────────────────────────────────────────── _POSEIDON16 = Poseidon1(PARAMS_16) @@ -294,9 +303,8 @@ def sample_in_range(self, bits: int, n_samples: int) -> list[int]: return [int(x.value) & mask for x in flat[:n_samples]] -# --------------------------------------------------------------------------- -# Proof container + VerifierState (transcript reader) -# --------------------------------------------------------------------------- + +# ─── Proof container + VerifierState (transcript reader) ────────────────────────────────────────────────────────── @dataclass @@ -437,35 +445,20 @@ def next_extension_scalars_vec_no_record(self, n: int) -> list[EF]: return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] -# --------------------------------------------------------------------------- -# Bytecode (minimal placeholder) + helpers -# --------------------------------------------------------------------------- + +# ─── Bytecode (minimal placeholder) + helpers ────────────────────────────────────────────────────────── @dataclass class Bytecode: - """Subset of lean_vm::Bytecode needed by the verifier.""" - - hash: list[Fp] # 8 base elements - log_size: int # log2 of bytecode length - instructions_multilinear: object | None = None # TODO: multilinear repr - - def log_size_(self) -> int: - return self.log_size - - -poseidon16_compress_pair = poseidon16_compress # alias for utils::poseidon16_compress_pair - + """The bytecode metadata `verify_execution` needs (hash + log size).""" -# --- small helpers used by next_sumcheck_polynomial --- + hash: list[Fp] + log_size: int -def _halve_fp() -> Fp: - # Multiplicative inverse of 2 mod P. Computed once at import time. - return Fp(pow(2, P - 2, P)) - - -_HALVE_FP = _halve_fp() +# Multiplicative inverse of 2 mod P (KoalaBear). Used by halve operations. +_HALVE_FP = Fp(pow(2, P - 2, P)) def _ef_halve(x: EF) -> EF: @@ -518,10 +511,10 @@ def padd_with_zero_to_next_power_of_two(values: Sequence[Fp]) -> list[Fp]: return list(values) + [Fp(0)] * (n - len(values)) -# --------------------------------------------------------------------------- + # Merkle: hashing primitives, pruned-paths restoration, Merkle verify. # Mirrors symetric::merkle + fiat_shamir::merkle_pruning. -# --------------------------------------------------------------------------- + @dataclass @@ -659,9 +652,8 @@ def prunedpaths_from_json(obj: dict) -> PrunedMerklePaths: ) -# --------------------------------------------------------------------------- -# WHIR polynomial primitives (poly + whir crates) -# --------------------------------------------------------------------------- + +# ─── WHIR polynomial primitives (poly + whir crates) ────────────────────────────────────────────────────────── def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: @@ -780,9 +772,8 @@ def new_next(total: int, point: list[EF], values: list[SparseValue]) -> "SparseS return SparseStatement(total, point, values, is_next=True) -# --------------------------------------------------------------------------- -# WHIR config helpers: derive integer-only parameters from the trimmed JSON -# --------------------------------------------------------------------------- + +# ─── WHIR config helpers: derive integer-only parameters from the trimmed JSON ────────────────────────────────────────────────────────── def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: @@ -918,9 +909,8 @@ def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: return _WHIR_CONFIGS[key] -# --------------------------------------------------------------------------- -# WHIR verifier (port of crates/whir/src/verify.rs) -# --------------------------------------------------------------------------- + +# ─── WHIR verifier (port of crates/whir/src/verify.rs) ────────────────────────────────────────────────────────── @dataclass @@ -1257,9 +1247,8 @@ def whir_verify( return folding_randomness_flat -# --------------------------------------------------------------------------- -# Table metadata (mirror of lean_vm::tables::table_trait) -# --------------------------------------------------------------------------- + +# ─── Table metadata (mirror of lean_vm::tables::table_trait) ────────────────────────────────────────────────────────── @dataclass(frozen=True) @@ -1332,9 +1321,8 @@ def tables_from_json(obj: list[dict]) -> list[TableMeta]: return out -# --------------------------------------------------------------------------- -# Stacked PCS — port of sub_protocols/stacked_pcs.rs -# --------------------------------------------------------------------------- + +# ─── Stacked PCS — port of sub_protocols/stacked_pcs.rs ────────────────────────────────────────────────────────── def compute_stacked_n_vars( @@ -1466,9 +1454,8 @@ def stacked_pcs_parse_commitment( return parsed_commitment_parse(state, stacked_n_vars, cfg.commitment_ood_samples) -# --------------------------------------------------------------------------- -# Generic sumcheck verifier (port of `backend/sumcheck/src/verify.rs`) -# --------------------------------------------------------------------------- + +# ─── Generic sumcheck verifier (port of `backend/sumcheck/src/verify.rs`) ────────────────────────────────────────────────────────── @dataclass @@ -1580,9 +1567,8 @@ def _quotient_sum(nums: Sequence[EF], dens: Sequence[EF]) -> EF: return acc -# --------------------------------------------------------------------------- -# Logup helpers (utils + poly) -# --------------------------------------------------------------------------- + +# ─── Logup helpers (utils + poly) ────────────────────────────────────────────────────────── def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: @@ -1673,9 +1659,8 @@ def eval_eq(point: Sequence[EF]) -> list[EF]: return out -# --------------------------------------------------------------------------- -# Generic logup verifier — port of sub_protocols/logup.rs::verify_generic_logup -# --------------------------------------------------------------------------- + +# ─── Generic logup verifier — port of sub_protocols/logup.rs::verify_generic_logup ────────────────────────────────────────────────────────── @dataclass @@ -1918,9 +1903,8 @@ def _next_pow_two(x: int) -> int: return 1 << (x - 1).bit_length() -# --------------------------------------------------------------------------- -# AIR sumcheck helpers (port of sub_protocols/air_sumcheck.rs) -# --------------------------------------------------------------------------- + +# ─── AIR sumcheck helpers (port of sub_protocols/air_sumcheck.rs) ────────────────────────────────────────────────────────── def natural_ordering_point_for_session( @@ -1978,9 +1962,8 @@ def columns_evals_up_and_down( return list(natural_ordering_point), eq_values, next_values -# --------------------------------------------------------------------------- -# Pluggable per-table AIR constraint evaluator -# --------------------------------------------------------------------------- + +# ─── Pluggable per-table AIR constraint evaluator ────────────────────────────────────────────────────────── class ConstraintFolder: @@ -2062,9 +2045,8 @@ def air_constraint_eval( return folder.accumulator -# --------------------------------------------------------------------------- -# Execution-table AIR (lean_vm/src/tables/execution/air.rs) -# --------------------------------------------------------------------------- + +# ─── Execution-table AIR (lean_vm/src/tables/execution/air.rs) ────────────────────────────────────────────────────────── # Column indexes for the execution table (mirrors execution/air.rs). @@ -2169,9 +2151,8 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: folder.assert_zero(not_jump_and_condition * (next_fp - fp)) -# --------------------------------------------------------------------------- -# Extension-op-table AIR (lean_vm/src/tables/extension_op/air.rs) -# --------------------------------------------------------------------------- + +# ─── Extension-op-table AIR (lean_vm/src/tables/extension_op/air.rs) ────────────────────────────────────────────────────────── _EXT_OP_COL = { @@ -2318,9 +2299,8 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat folder.assert_zero(start_down * (len_col - one)) -# --------------------------------------------------------------------------- -# Poseidon16-compress AIR (lean_vm/src/tables/poseidon_16/mod.rs) -# --------------------------------------------------------------------------- + +# ─── Poseidon16-compress AIR (lean_vm/src/tables/poseidon_16/mod.rs) ────────────────────────────────────────────────────────── def _ef_cube(x: EF) -> EF: @@ -2578,9 +2558,8 @@ def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: ) -# --------------------------------------------------------------------------- -# AIR-stage orchestration in verify_execution -# --------------------------------------------------------------------------- + +# ─── AIR-stage orchestration in verify_execution ────────────────────────────────────────────────────────── @dataclass @@ -2740,22 +2719,13 @@ def verify_air_stage( ) -# --------------------------------------------------------------------------- -# Top-level verifier -# --------------------------------------------------------------------------- - -@dataclass -class ProofVerificationDetails: - bytecode_evaluation: object # Evaluation — TODO +# ─── Top-level verifier ────────────────────────────────────────────────────────── @dataclass(frozen=True) class TableInfo: - """Minimal table metadata the verifier needs (bus + lookups + n_columns). - - Built from the Rust-dumped table metadata via `tables_from_json`. - """ + """Table metadata (bus + lookups + n_columns). Built by `tables_from_json`.""" name: str n_columns: int @@ -2767,17 +2737,14 @@ def to_meta(self) -> TableMeta: @dataclass -class VerifyExecutionPartial: - """What we can produce so far — extended as we port more sub-protocols.""" +class VerifyResult: + """High-level outputs of `verify_execution` (mostly for diagnostics).""" log_inv_rate: int log_memory: int - public_input_len: int - table_log_heights: dict[str, int] stacked_n_vars: int - parsed_commitment: ParsedCommitment - logup_statements: GenericLogupStatements - air_stage: AirStageResult | None = None + bytecode_evaluation_point: list[EF] + bytecode_evaluation_value: EF def verify_execution( @@ -2787,23 +2754,25 @@ def verify_execution( tables: Sequence[TableInfo], constants: dict, bytecode_multilinear: list[Fp], -) -> VerifyExecutionPartial: - """Port of `verify_execution` (lean_prover/src/verify_execution.rs). - - Runs the prologue, parses the stacked-PCS WHIR commitment, samples the - logup challenges, and verifies the generic logup argument. AIR sumcheck + - WHIR final-eval are still TODO. - - `tables` must be in canonical Rust order (`ALL_TABLES`) — `execution` - first, then `extension_op`, `poseidon16` — because the verifier reads - per-table `log_n_rows` in that same order from the transcript. - - `constants` and `bytecode_multilinear` come from the Rust dump. +) -> VerifyResult: + """Verify a leanVM execution proof. Port of `verify_execution.rs`. + + The flow is: + 1. observe public input + SNARK domain separator + 2. read dims, validate bounds + 3. parse stacked-PCS WHIR commitment (root + OOD) + 4. verify generic logup (GKR + sum reconstruction) + 5. AIR sumcheck across all tables + per-table constraint evaluation + 6. assemble global WHIR statements and run the final WHIR verify + + `tables` must be in canonical Rust order (= `ALL_TABLES`): execution, + extension_op, poseidon16. `constants` and `bytecode_multilinear` come from + the Rust dump. """ state = VerifierState(proof) state.observe_scalars(list(public_input)) - state.observe_scalars(poseidon16_compress_pair(bytecode.hash, SNARK_DOMAIN_SEP)) + state.observe_scalars(poseidon16_compress(bytecode.hash, SNARK_DOMAIN_SEP)) n_tables = len(tables) dims = [int(x.value) for x in state.next_base_scalars_vec(3 + n_tables)] @@ -2919,92 +2888,131 @@ def verify_execution( whir_cfg = whir_config(log_inv_rate, stacked_n_vars) whir_verify(state, whir_cfg, parsed_commitment, global_statements) - return VerifyExecutionPartial( + return VerifyResult( log_inv_rate=log_inv_rate, log_memory=log_memory, - public_input_len=public_input_len, - table_log_heights=table_log_heights, - stacked_n_vars=parsed_commitment.num_variables, - parsed_commitment=parsed_commitment, - logup_statements=logup_statements, - air_stage=air_stage, + stacked_n_vars=stacked_n_vars, + bytecode_evaluation_point=list(logup_statements.bytecode_evaluation.point), + bytecode_evaluation_value=logup_statements.bytecode_evaluation.value, ) -# --------------------------------------------------------------------------- -# Self-test: foundations only -# --------------------------------------------------------------------------- +# ─── Test vector loader + entry point ────────────────────────────────────────────────────────── -def _smoke() -> None: - print(f"KoalaBear P = {P}") - # EF sanity: (X) * (X^4 + X) should reduce since X^5 = 1 - X^2. - X = EF([Fp(0), Fp(1), Fp(0), Fp(0), Fp(0)]) - X4 = X.pow(4) - X5 = X * X4 - expected = EF.one() - EF([Fp(0), Fp(0), Fp(1), Fp(0), Fp(0)]) # 1 - X^2 - assert X5 == expected, (X5, expected) - one = EF.one() - a = EF([Fp(3), Fp(1), Fp(4), Fp(1), Fp(5)]) - assert a * a.inv() == one - print("EF arithmetic OK") - - # Challenger / sponge sanity: deterministic outputs. - ch1 = Challenger() - ch1.observe_many([Fp(1), Fp(2), Fp(3)]) - s1 = ch1.sample_ef() - ch2 = Challenger() - ch2.observe_many([Fp(1), Fp(2), Fp(3)]) - s2 = ch2.sample_ef() - assert s1 == s2, "Challenger not deterministic" - print(f"Challenger sample (deterministic) = {s1}") - - # WHIR config table: sample lookup. - cfg = whir_config(log_inv_rate=1, num_variables=20) - print( - f"WHIR cfg(log_inv_rate=1, num_vars=20): rounds={len(cfg.rounds)}, " - f"final_queries={cfg.final_queries}, final_query_pow_bits={cfg.final_query_pow_bits}" +def poseidon_compress_slice(data: Sequence[Fp], use_iv: bool) -> list[Fp]: + """Port of `utils::poseidon_compress_slice`. + + Hash a length-multiple-of-8 sequence into one 8-element digest using + Poseidon16 in Davies-Meyer compression mode. If `use_iv` is false the + first 16 elements seed the sponge directly; if true an all-zero IV is used. + """ + assert data and len(data) % 8 == 0 + if use_iv: + h = [Fp(0)] * 8 + for i in range(0, len(data), 8): + h = poseidon16_compress(h, list(data[i : i + 8])) + return h + if len(data) <= 16: + padded = list(data) + [Fp(0)] * (16 - len(data)) + return poseidon16_compress_in_place(padded)[:8] + h = poseidon16_compress_in_place(list(data[:16]))[:8] + for i in range(16, len(data), 8): + h = poseidon16_compress(h, list(data[i : i + 8])) + return h + + +def _load_test_vector(json_path): + """Load the Rust-dumped end-to-end test vector.""" + import array + import json + from pathlib import Path + + json_path = Path(json_path) + raw = json.loads(json_path.read_text()) + + # Bytecode multilinear (raw u32 LE sidecar). + mle_bin = (json_path.parent / raw["bytecode_multilinear_path"]).read_bytes() + arr = array.array("I") + arr.frombytes(mle_bin) + assert len(arr) == raw["bytecode_multilinear_len"] + bytecode_multilinear = [Fp(v) for v in arr] + + bytecode = Bytecode( + hash=[Fp(v) for v in raw["bytecode_hash"]], + log_size=raw["bytecode_log_size"], ) - if cfg.rounds: - r0 = cfg.rounds[0] + public_input = [Fp(v) for v in raw["public_input"]] + input_data = [Fp(v) for v in raw["input_data"]] + + transcript = [Fp(v) for v in raw["proof"]["transcript"]] + openings: list[MerkleOpening] = [] + for bucket in raw["proof"]["merkle_paths"]: + for path in restore_merkle_paths(prunedpaths_from_json(bucket)): + openings.append(MerkleOpening(leaf_data=path.leaf_data, path=path.sibling_hashes)) + proof = Proof(transcript=transcript, merkle_openings=openings) + + metas = tables_from_json(raw["tables"]) + tables = [ + TableInfo(name=m.name, n_columns=m.n_columns, bus=m.bus, lookups=m.lookups) + for m in metas + ] + + return { + "bytecode": bytecode, + "bytecode_multilinear": bytecode_multilinear, + "public_input": public_input, + "input_data": input_data, + "proof": proof, + "tables": tables, + "constants": raw["constants"], + } + + +def main() -> int: + """Load the end-to-end test vector and run `verify_execution`.""" + import sys + from pathlib import Path + + vector_path = Path(__file__).resolve().parents[2] / "target" / "zkvm_test_vectors" / "proof.json" + if not vector_path.exists(): print( - f" round[0]: num_queries={r0.num_queries}, ood_samples={r0.ood_samples}, " - f"query_pow_bits={r0.query_pow_bits}, folding_pow_bits={r0.folding_pow_bits}" + f"Test vector not found at {vector_path}. Generate it first with:\n" + " cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture" ) + return 1 + + print(f"Loading {vector_path.name}...") + v = _load_test_vector(vector_path) + + # Sanity: re-derive `public_input` from `input_data` to check the hash. + derived = poseidon_compress_slice(v["input_data"], use_iv=True) + if derived != v["public_input"]: + print("FAIL: poseidon_compress_slice(input_data) doesn't match dumped public_input") + return 1 - # VerifierState read/sample round-trip. - proof = Proof(transcript=[Fp(i) for i in range(20)]) - st = VerifierState(proof) - st.observe_scalars([Fp(7)]) - base = st.next_base_scalars_vec(3) - print(f"VerifierState read 3 base scalars: {base}") - chal = st.sample() - print(f"VerifierState sample = {chal}") - - # verify_execution: dummy proof should hit a bound check, not crash. - bc = Bytecode(hash=[Fp(0)] * 8, log_size=10) - bad_proof = Proof(transcript=[Fp(0)] * 64) try: - verify_execution( - bc, - [Fp(0)] * 4, - bad_proof, - tables=[], - constants={ - "n_instruction_columns": 12, - "n_runtime_columns": 8, - "col_pc": 0, - "logup_memory_domainsep": 0, - "logup_precompile_domainsep": 1, - "logup_bytecode_domainsep": 2, - "max_precompile_bus_width": 4, - }, - bytecode_multilinear=[Fp(0)] * 16, + result = verify_execution( + v["bytecode"], + v["public_input"], + v["proof"], + v["tables"], + v["constants"], + v["bytecode_multilinear"], ) except ProofError as e: - print(f"verify_execution failed bound check (expected with dummy proof): {e}") + print(f"FAIL: {e}") + return 1 + + print( + f"OK: rec-aggregation proof verified " + f"(log_inv_rate={result.log_inv_rate}, log_memory={result.log_memory}, " + f"stacked_n_vars={result.stacked_n_vars})" + ) + return 0 if __name__ == "__main__": - _smoke() + import sys as _sys + _sys.exit(main()) diff --git a/crates/whir/Cargo.toml b/crates/whir/Cargo.toml index aba785d2b..1c2a2b0a7 100644 --- a/crates/whir/Cargo.toml +++ b/crates/whir/Cargo.toml @@ -21,5 +21,3 @@ tracing.workspace = true [dev-dependencies] tracing-forest.workspace = true tracing-subscriber.workspace = true -serde.workspace = true -serde_json.workspace = true diff --git a/crates/whir/tests/dump_test_vectors.rs b/crates/whir/tests/dump_test_vectors.rs deleted file mode 100644 index 8649bb7a2..000000000 --- a/crates/whir/tests/dump_test_vectors.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! Generates WHIR test vectors for the Python verifier. -//! -//! Run: -//! cargo test --release -p mt-whir --test dump_test_vectors -- --nocapture -//! -//! Outputs `target/whir_test_vectors/.json`. Each file contains, in -//! canonical (non-Monty) `u32` form: -//! -//! - `num_variables`, `log_inv_rate`, WHIR builder params, -//! - `statement`: list of `SparseStatement` (point + values), -//! - `proof.transcript`: the raw `Vec` written by the prover, -//! - `proof.merkle_paths`: pruned Merkle paths exactly as serialized by -//! `PrunedMerklePaths` (Python must port the restore routine), -//! - `expected_folding_randomness`: what `WhirConfig::verify` returns. - -use std::fs; -use std::path::PathBuf; - -use fiat_shamir::{ProverState, VerifierState}; -use field::{PrimeCharacteristicRing, PrimeField32, TwoAdicField}; -use koala_bear::{KoalaBear, QuinticExtensionFieldKB, default_koalabear_poseidon1_16}; -use mt_whir::*; -use poly::*; -use rand::{RngExt, SeedableRng, rngs::StdRng}; -use serde::Serialize; - -type F = KoalaBear; -type EF = QuinticExtensionFieldKB; - -const DIGEST_ELEMS: usize = 8; - -fn f_to_u32(x: F) -> u32 { - x.as_canonical_u32() -} - -fn ef_to_u32s(x: EF) -> [u32; 5] { - use field::BasedVectorSpace; - let coeffs: &[F] = x.as_basis_coefficients_slice(); - [ - f_to_u32(coeffs[0]), - f_to_u32(coeffs[1]), - f_to_u32(coeffs[2]), - f_to_u32(coeffs[3]), - f_to_u32(coeffs[4]), - ] -} - -#[derive(Serialize)] -struct SparseValueJson { - selector: usize, - value: [u32; 5], -} - -#[derive(Serialize)] -struct SparseStatementJson { - total_num_variables: usize, - is_next: bool, - point: Vec<[u32; 5]>, - values: Vec, -} - -#[derive(Serialize)] -struct PrunedPathJson { - leaf_index: usize, - siblings: Vec<[u32; DIGEST_ELEMS]>, -} - -#[derive(Serialize)] -struct PrunedMerklePathsJson { - merkle_height: usize, - original_order: Vec, - leaf_data: Vec>, - paths: Vec, - n_trailing_zeros: usize, -} - -#[derive(Serialize)] -struct ProofJson { - transcript: Vec, - merkle_paths: Vec, -} - -#[derive(Serialize)] -struct WhirBuilderJson { - security_level: usize, - max_num_variables_to_send_coeffs: usize, - pow_bits: usize, - folding_factor_first: usize, - folding_factor_subsequent: usize, - starting_log_inv_rate: usize, - rs_domain_initial_reduction_factor: usize, - soundness_type: &'static str, // "JohnsonBound" -} - -#[derive(Serialize)] -struct TestVectorJson { - name: String, - num_variables: usize, - log_inv_rate: usize, - builder: WhirBuilderJson, - statement: Vec, - proof: ProofJson, - expected_folding_randomness: Vec<[u32; 5]>, -} - -fn convert_pruned(p: &fiat_shamir::PrunedMerklePaths) -> PrunedMerklePathsJson { - PrunedMerklePathsJson { - merkle_height: p.merkle_height, - original_order: p.original_order.clone(), - leaf_data: p - .leaf_data - .iter() - .map(|v| v.iter().map(|&f| f_to_u32(f)).collect()) - .collect(), - paths: p - .paths - .iter() - .map(|(idx, siblings)| PrunedPathJson { - leaf_index: *idx, - siblings: siblings.iter().map(|d| d.map(f_to_u32)).collect(), - }) - .collect(), - n_trailing_zeros: p.n_trailing_zeros, - } -} - -fn convert_statement(s: &SparseStatement) -> SparseStatementJson { - SparseStatementJson { - total_num_variables: s.total_num_variables, - is_next: s.is_next, - point: s.point.iter().map(|&p| ef_to_u32s(p)).collect(), - values: s - .values - .iter() - .map(|v| SparseValueJson { - selector: v.selector, - value: ef_to_u32s(v.value), - }) - .collect(), - } -} - -fn run_one(name: &str, num_variables: usize, log_inv_rate: usize, seed: u64, out_dir: &PathBuf) { - let poseidon16 = default_koalabear_poseidon1_16(); - - // Use the same defaults as `lean_prover::default_whir_config`, so the - // Python side can reuse the dumped WhirConfig table. - let builder = WhirConfigBuilder { - security_level: 124, - max_num_variables_to_send_coeffs: 8, - pow_bits: 16, - folding_factor: FoldingFactor::new(7, 5), - soundness_type: SecurityAssumption::JohnsonBound, - starting_log_inv_rate: log_inv_rate, - rs_domain_initial_reduction_factor: 5, - }; - let params = WhirConfig::::new(&builder, num_variables); - - let mut rng = StdRng::seed_from_u64(seed); - let num_coeffs = 1usize << num_variables; - let polynomial: Vec = (0..num_coeffs).map(|_| rng.random::()).collect(); - - // One simple statement: a fully-dense point + its evaluation. - let dense_point: Vec = (0..num_variables).map(|_| rng.random()).collect(); - let dense_value = polynomial.evaluate_sparse(0, &MultilinearPoint(dense_point.clone())); - let statement = vec![SparseStatement::new( - num_variables, - MultilinearPoint(dense_point.clone()), - vec![SparseValue { - selector: 0, - value: dense_value, - }], - )]; - - precompute_dft_twiddles::(1 << F::TWO_ADICITY); - - let mut prover_state = ProverState::::new(poseidon16.clone()); - let polynomial_mle: MleOwned = MleOwned::Base(polynomial); - let witness = params.commit(&mut prover_state, &polynomial_mle, num_coeffs); - params.prove(&mut prover_state, statement.clone(), witness, &polynomial_mle.by_ref()); - let proof = prover_state.into_proof(); - - // Run the verifier and capture its `folding_randomness` output as the oracle. - let mut vstate = VerifierState::::new(proof.clone(), poseidon16.clone()).unwrap(); - println!("transcript[23] (pre-verify): {}", proof.transcript[23].as_canonical_u32()); - let parsed_commitment = params.parse_commitment::(&mut vstate).unwrap(); - let folding_randomness = params - .verify::(&mut vstate, &parsed_commitment, statement.clone()) - .unwrap(); - - let entry = TestVectorJson { - name: name.to_string(), - num_variables, - log_inv_rate, - builder: WhirBuilderJson { - security_level: 124, - max_num_variables_to_send_coeffs: 8, - pow_bits: 16, - folding_factor_first: 7, - folding_factor_subsequent: 5, - starting_log_inv_rate: log_inv_rate, - rs_domain_initial_reduction_factor: 5, - soundness_type: "JohnsonBound", - }, - statement: statement.iter().map(convert_statement).collect(), - proof: ProofJson { - transcript: proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), - merkle_paths: proof.merkle_paths.iter().map(convert_pruned).collect(), - }, - expected_folding_randomness: folding_randomness.iter().map(|&p| ef_to_u32s(p)).collect(), - }; - - let path = out_dir.join(format!("{name}.json")); - let json = serde_json::to_string(&entry).unwrap(); - fs::write(&path, json).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); - println!( - " {} -> {} ({:.1} KiB)", - name, - path.display(), - path.metadata().unwrap().len() as f64 / 1024.0, - ); -} - -/// Tiny sanity test for the Python Merkle restoration. Build a 16-leaf tree, -/// open a handful of indices, prune, then dump (a) the original openings (as -/// the source of truth) and (b) the pruned form Python should restore. -#[test] -fn dump_merkle_vector() { - use fiat_shamir::{MerklePath, MerklePaths, PrunedMerklePaths}; - use symetric::compress; - - let poseidon = default_koalabear_poseidon1_16(); - - // Build 16 leaves, each 8 base elements long (one RATE block). - let n = 16usize; - let leaf_len = 8usize; - let leaves: Vec> = (0..n) - .map(|i| { - (0..leaf_len) - .map(|j| F::from_u32((i * 100 + j) as u32 + 1)) - .collect() - }) - .collect(); - - // Hash each leaf to get level 0. - let hash_leaf = |data: &[F]| { - // The two-RATE-blocks requirement of hash_slice needs len(data) >= 16 - // (i.e. n_chunks >= 2). For an 8-element leaf, pad with one extra zero - // block so n_chunks = 2. - let mut padded = vec![F::ZERO; 8]; - padded.extend_from_slice(data); - symetric::hash_slice::<_, _, 16, 8, 8>(&poseidon, &padded) - }; - let hash_combine = - |l: &[F; 8], r: &[F; 8]| compress::<_, _, 8, 16>(&poseidon, [*l, *r]); - - let leaf_hashes: Vec<[F; 8]> = leaves.iter().map(|l| hash_leaf(l)).collect(); - let mut levels = vec![leaf_hashes]; - let mut log_h = n.trailing_zeros() as usize; - while levels.last().unwrap().len() > 1 { - let prev = levels.last().unwrap(); - let next: Vec<[F; 8]> = (0..prev.len() / 2) - .map(|i| hash_combine(&prev[2 * i], &prev[2 * i + 1])) - .collect(); - levels.push(next); - } - let _ = log_h; - let root: [F; 8] = *levels.last().unwrap().first().unwrap(); - - let make_path = |idx: usize| -> MerklePath { - let mut sibling_hashes = Vec::with_capacity(levels.len() - 1); - let mut k = idx; - for level in &levels[..levels.len() - 1] { - sibling_hashes.push(level[k ^ 1]); - k >>= 1; - } - // Pad leaf data with the zero-block prefix used by hash_leaf. - let mut leaf_padded = vec![F::ZERO; 8]; - leaf_padded.extend_from_slice(&leaves[idx]); - MerklePath { - leaf_data: leaf_padded, - sibling_hashes, - leaf_index: idx, - } - }; - - let indices = vec![3usize, 7, 11, 0, 11]; // duplicate included - let original = MerklePaths(indices.iter().map(|&i| make_path(i)).collect::>()); - let pruned: PrunedMerklePaths = original.clone().prune(); - - #[derive(serde::Serialize)] - struct PathJson { - leaf_index: usize, - leaf_data: Vec, - sibling_hashes: Vec<[u32; 8]>, - } - - #[derive(serde::Serialize)] - struct Out { - root: [u32; 8], - original: Vec, - pruned: PrunedMerklePathsJson, - } - - let out = Out { - root: root.map(f_to_u32), - original: original - .clone() - .0 - .into_iter() - .map(|p| PathJson { - leaf_index: p.leaf_index, - leaf_data: p.leaf_data.iter().map(|&f| f_to_u32(f)).collect(), - sibling_hashes: p.sibling_hashes.iter().map(|d| d.map(f_to_u32)).collect(), - }) - .collect(), - pruned: convert_pruned(&pruned), - }; - - let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("whir_test_vectors"); - fs::create_dir_all(&out_dir).unwrap(); - let path = out_dir.join("merkle_sanity.json"); - fs::write(&path, serde_json::to_string_pretty(&out).unwrap()).unwrap(); - println!("wrote merkle sanity to {}", path.display()); -} - -/// Cross-check that Python's Poseidon16 permutation matches Rust's on a -/// specific input. -#[test] -fn dump_permute_oracle() { - use koala_bear::symmetric::Permutation; - let poseidon = default_koalabear_poseidon1_16(); - - // Input: the state captured after_read_poly0, with rate replaced by - // [witness, 0, ..., 0] (witness = 66589315). - let input: [F; 16] = [ - 1732812002, 1231764113, 2063040591, 339182820, - 1169456582, 2099684484, 1027478197, 686152220, - 66589315, 0, 0, 0, 0, 0, 0, 0, - ].map(F::from_u32); - let mut state = input; - poseidon.permute_mut(&mut state); - - #[derive(serde::Serialize)] - struct Out { - input: Vec, - output: Vec, - } - let out = Out { - input: input.iter().map(|&f| f_to_u32(f)).collect(), - output: state.iter().map(|&f| f_to_u32(f)).collect(), - }; - let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("whir_test_vectors") - .join("permute_oracle.json"); - fs::write(&path, serde_json::to_string_pretty(&out).unwrap()).unwrap(); - println!("output: {:?}", out.output); -} - -#[test] -fn dump_test_vectors() { - let target_dir = - std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("whir_test_vectors"); - fs::create_dir_all(&out_dir).unwrap(); - - println!("Writing test vectors to {}", out_dir.display()); - - // `n_rounds` is 0 below `num_variables = 16` with the default builder, and - // `final_round_config()` would then panic. Keep all entries above that. - run_one("small_lir1_nv16", 16, 1, 0xdead_beef, &out_dir); - run_one("small_lir2_nv18", 18, 2, 0xfeed_face, &out_dir); - run_one("medium_lir1_nv20", 20, 1, 0xcafe_babe, &out_dir); -} From fd42d0bafd98eb7c7e6f47ae6d68cea7d202af95 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 17 May 2026 23:18:55 +0200 Subject: [PATCH 06/69] fiat-shamir: restore parallel pow-grinding Reverts the temporary single-threaded `find` introduced for deterministic debugging during the Python verifier port. With the bug found (sorted dict iteration in `stacked_pcs_global_statements`) the determinism aid is no longer needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/backend/fiat-shamir/src/prover.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/backend/fiat-shamir/src/prover.rs b/crates/backend/fiat-shamir/src/prover.rs index 76d1b10cc..2ea95580d 100644 --- a/crates/backend/fiat-shamir/src/prover.rs +++ b/crates/backend/fiat-shamir/src/prover.rs @@ -126,13 +126,11 @@ where let lanes = Packed::::WIDTH; let witness_found = Mutex::>>::new(None); - // each batch tests lanes witnesses simultaneously. - // NOTE: deliberately single-threaded so the resulting witness is - // deterministic across runs (the Python verifier port relies on bit- - // for-bit reproducibility of the dumped proof). + // each batch tests lanes witnesses simultaneously let num_batches = PF::::ORDER_U64.div_ceil(lanes as u64); (0..num_batches) - .find(|&batch| { + .into_par_iter() + .find_any(|&batch| { let base = batch * lanes as u64; let packed_witnesses = Packed::::from_fn(|lane| { From 50629930b894c4851704451a7dd7f7fbdf222925 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 17 May 2026 23:56:38 +0200 Subject: [PATCH 07/69] verifier.py: simplify and clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge `TableInfo` and `TableMeta` into a single dataclass. - Drop `BusData`, `Bus.data`, `Bus.selector`, `Bus.direction_flag` — only `bus_direction` is ever read by the verifier; flatten it onto `TableMeta`. - Drop the `register_air_evaluator` decorator + global registry; replace with a direct dispatch table in `air_constraint_eval`. - Collapse per-table helpers (`_max_air_constraints`, `_table_degree_air`, `_table_down_column_indexes`, `_table_n_down_columns`) into one `_TABLE_SPECS` dict keyed by table name. - Replace ad-hoc lazy-init globals (`_WHIR_CONFIGS`, `_POSEIDON1_CONSTS`, `_load_whir_configs`, `_load_poseidon1_constants`, `_p1c`) with `functools.cache`d helpers. - Drop unused helpers: `poseidon16_permute`, `_from_int`, `_next_pow_two`, `whir_num_variables_at_round`, `_load_test_vector`, `EF.from_basis_coefficients`. - Trim `AirStageResult` to the three fields actually consumed downstream. - Strip "Port of X" / "Mirror of X" boilerplate from ~36 docstrings. - Condense `# ─── Section ───` banners to single lines. - Fold `test_zkvm.py` loader into `verifier.py:main()`. 3010 → 2742 lines, all four prior checks still pass (rec-aggregation proof verifies end-to-end). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/lean_prover/verifier.py | 548 ++++++++------------------------- 1 file changed, 136 insertions(+), 412 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index c405432d9..3a617d36a 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -23,6 +23,7 @@ from __future__ import annotations +import functools from dataclasses import dataclass, field from typing import Sequence @@ -196,36 +197,20 @@ def pow(self, n: int) -> "EF": _POSEIDON16 = Poseidon1(PARAMS_16) -def poseidon16_permute(state: list[Fp]) -> list[Fp]: - """Apply the Poseidon16 permutation to a length-WIDTH state. - - NOTE: this is the bare permutation. For the Davies-Meyer-style - *compression* used by Merkle trees use `poseidon16_compress_in_place`. - The Fiat-Shamir challenger uses the bare permutation (no feed-forward). - """ - assert len(state) == WIDTH - return _POSEIDON16.permute(state) - - def poseidon16_compress_in_place(state: list[Fp]) -> list[Fp]: - """`compress_in_place`: out = permute(state) + state (feed-forward).""" + """Davies-Meyer compression: `permute(state) + state` (length WIDTH).""" assert len(state) == WIDTH - permuted = _POSEIDON16.permute(state) - return [a + b for a, b in zip(permuted, state)] + return [a + b for a, b in zip(_POSEIDON16.permute(state), state)] def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: - """2:1 Merkle compression: `compress_in_place(left || right)[:DIGEST_ELEMS]`.""" + """2:1 Merkle compression: top DIGEST_ELEMS of `compress_in_place(left || right)`.""" assert len(left) == DIGEST_ELEMS and len(right) == DIGEST_ELEMS return poseidon16_compress_in_place(list(left) + list(right))[:DIGEST_ELEMS] def hash_slice(data: Sequence[Fp]) -> list[Fp]: - """`symetric::hash_slice` with WIDTH=16, RATE=OUT=8 (right-to-left absorbing). - - Uses the same `compress_in_place` (feed-forward) primitive as Merkle, NOT - the bare permutation used by the challenger. - """ + """`symetric::hash_slice` with WIDTH=16, RATE=OUT=8 (right-to-left absorbing).""" assert len(data) % RATE == 0 n_chunks = len(data) // RATE assert n_chunks >= 2 @@ -519,7 +504,6 @@ def padd_with_zero_to_next_power_of_two(values: Sequence[Fp]) -> list[Fp]: @dataclass class MerklePath: - """Mirror of fiat_shamir::MerklePath (the un-pruned form).""" leaf_data: list[Fp] sibling_hashes: list[list[Fp]] # each entry has DIGEST_ELEMS Fp @@ -528,7 +512,6 @@ class MerklePath: @dataclass class PrunedMerklePaths: - """Mirror of fiat_shamir::PrunedMerklePaths — input to restore().""" merkle_height: int original_order: list[int] @@ -544,9 +527,7 @@ def _lca_level(a: int, b: int) -> int: def restore_merkle_paths(p: PrunedMerklePaths) -> list[MerklePath]: - """Port of `merkle_pruning::restore` (fiat_shamir). - - Reconstructs full sibling arrays from the pruned form using leaf hashing + """Reconstructs full sibling arrays from the pruned form using leaf hashing and 2:1 compression (Poseidon16). Raises ProofError on malformed inputs. """ @@ -626,7 +607,6 @@ def merkle_verify_path( opened_values: Sequence[Fp], opening_proof: Sequence[list[Fp]], ) -> bool: - """Mirror of symetric::merkle::merkle_verify (length-DIGEST_ELEMS digests).""" if len(opening_proof) != log_height: return False @@ -677,7 +657,6 @@ def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: - """Port of poly::next_mle (the "next" multilinear on the boolean cube).""" assert len(x) == len(y) n = len(x) one = EF.one() @@ -738,7 +717,6 @@ class SparseValue: @dataclass class SparseStatement: - """Mirror of whir::SparseStatement.""" total_num_variables: int point: list[EF] # the "inner" point, length = inner_num_variables @@ -802,18 +780,8 @@ def whir_log_inv_rate_at(starting_log_inv_rate: int, round_index: int) -> int: return rate -def whir_num_variables_at_round(num_variables: int, round_index: int) -> int: - """num_variables remaining at the START of round `round_index` (the verifier - parses a new commitment at this num_variables for that round). - """ - rem = num_variables - for r in range(round_index + 1): - rem -= whir_folding_factor_at_round(r) - return rem - - -# KoalaBear two-adic generators: index `bits` is the primitive 2^bits-th root of unity. -# Mirrors KoalaBearParameters::TWO_ADIC_GENERATORS (canonical-form u32 values). +# KoalaBear two-adic generators: index `bits` is the primitive 2^bits-th root +# of unity (canonical-form u32 values, mirrors `TWO_ADIC_GENERATORS`). KB_TWO_ADIC_GENERATORS: list[int] = [ 0x1, 0x7F000000, 0x7E010002, 0x6832FE4A, 0x08DBD69C, 0x0A28F031, 0x5C4A5B99, 0x29B75A80, 0x17668B8A, 0x27AD539B, 0x334D48C7, 0x7744959C, 0x768FC6FA, @@ -823,7 +791,6 @@ def whir_num_variables_at_round(num_variables: int, round_index: int) -> int: def two_adic_generator(bits: int) -> Fp: - """Mirror of KoalaBear::two_adic_generator(bits).""" assert 0 <= bits <= BASE_TWO_ADICITY return Fp(KB_TWO_ADIC_GENERATORS[bits]) @@ -870,43 +837,28 @@ class WhirConfig: rounds: tuple[WhirRoundConfig, ...] -def _load_whir_configs() -> dict[tuple[int, int], WhirConfig]: +@functools.cache +def _whir_configs() -> dict[tuple[int, int], WhirConfig]: import json from pathlib import Path - - path = Path(__file__).with_name(WHIR_CONFIGS_PATH) - with open(path) as f: - raw = json.load(f) - - out: dict[tuple[int, int], WhirConfig] = {} - for c in raw: - cfg = WhirConfig( - log_inv_rate=c["log_inv_rate"], - num_variables=c["num_variables"], - commitment_ood_samples=c["commitment_ood_samples"], - starting_folding_pow_bits=c["starting_folding_pow_bits"], - final_queries=c["final_queries"], - final_query_pow_bits=c["final_query_pow_bits"], + raw = json.loads(Path(__file__).with_name(WHIR_CONFIGS_PATH).read_text()) + return { + (c["log_inv_rate"], c["num_variables"]): WhirConfig( + **{k: c[k] for k in WhirConfig.__annotations__ if k != "rounds"}, rounds=tuple(WhirRoundConfig(**r) for r in c["rounds"]), ) - out[(cfg.log_inv_rate, cfg.num_variables)] = cfg - return out - - -_WHIR_CONFIGS: dict[tuple[int, int], WhirConfig] | None = None + for c in raw + } def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: - global _WHIR_CONFIGS - if _WHIR_CONFIGS is None: - _WHIR_CONFIGS = _load_whir_configs() - key = (log_inv_rate, num_variables) - if key not in _WHIR_CONFIGS: + try: + return _whir_configs()[(log_inv_rate, num_variables)] + except KeyError: raise KeyError( - f"No WHIR config for log_inv_rate={log_inv_rate}, num_variables={num_variables}. " - f"Regenerate with: cargo test -p lean_prover --test dump_whir_configs" - ) - return _WHIR_CONFIGS[key] + f"No WHIR config for (log_inv_rate={log_inv_rate}, num_variables={num_variables}). " + "Regenerate with: cargo test -p lean_prover --test dump_whir_configs" + ) from None @@ -915,7 +867,6 @@ def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: @dataclass class ParsedCommitment: - """Mirror of whir::ParsedCommitment.""" num_variables: int root: list[Fp] # length DIGEST_ELEMS @@ -932,7 +883,6 @@ def oods_constraints(self) -> list[SparseStatement]: def parsed_commitment_parse(state: VerifierState, num_variables: int, ood_samples: int) -> ParsedCommitment: - """Port of ParsedCommitment::parse.""" root = state.next_base_scalars_vec(DIGEST_ELEMS) ood_points: list[EF] = [] ood_answers: list[EF] = [] @@ -953,9 +903,7 @@ def verify_sumcheck_rounds( rounds: int, pow_bits: int, ) -> list[EF]: - """Port of whir::verify::verify_sumcheck_rounds. - - Returns the folding randomness for these rounds. Mutates `claimed_sum_ref[0]`. + """Returns the folding randomness for these rounds. Mutates `claimed_sum_ref[0]`. """ randomness: list[EF] = [] for _ in range(rounds): @@ -982,7 +930,6 @@ def combine_constraints( claimed_sum_ref: list[EF], constraints: list[SparseStatement], ) -> list[EF]: - """Port of combine_constraints — mutates claimed_sum_ref[0] in-place.""" gamma: EF = state.sample() combination = [EF.one()] for smt in constraints: @@ -1007,9 +954,7 @@ def verify_stir_challenges( commitment: ParsedCommitment, folding_randomness: list[EF], ) -> list[SparseStatement]: - """Port of WhirConfig::verify_stir_challenges (incl. Merkle verification). - - `folding_factor` is the folding factor applied AT this round (i.e. how the + """`folding_factor` is the folding factor applied AT this round (i.e. how the leaves are arranged). `next_folding_factor` is the AIR sumcheck folding for the *next* hop; for the final pseudo-round it equals `folding_factor`. @@ -1054,9 +999,7 @@ def verify_stir_challenges( def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> bool: - """Port of verify_constraint_coeffs. - - Checks that the constraint's point is `[α, α^2, α^4, ...]` and that + """Checks that the constraint's point is `[α, α^2, α^4, ...]` and that the univariate polynomial (Horner) evaluates to each claimed value. """ assert constraint.selector_num_variables == 0 @@ -1075,9 +1018,7 @@ def eval_constraints_poly( constraints: list[tuple[list[EF], list[SparseStatement]]], point: list[EF], ) -> EF: - """Port of WhirConfig::eval_constraints_poly. - - `constraints` is a list of (combination_randomness, sparse_statements) per + """`constraints` is a list of (combination_randomness, sparse_statements) per round. `point` is the global folding randomness; it is sliced down by the folding factor of each preceding round before use. """ @@ -1112,7 +1053,6 @@ def whir_verify( parsed_commitment: ParsedCommitment, statement: list[SparseStatement], ) -> list[EF]: - """Port of WhirConfig::verify. Returns the folding randomness.""" for s in statement: assert s.total_num_variables == parsed_commitment.num_variables @@ -1176,52 +1116,33 @@ def whir_verify( round_folding.append(folding_rand_r) prev_commitment = new_commitment - # Final round: read the final polynomial coefficients (length 2^n_vars_final). + # Final round: read the final polynomial in coefficient form, then run a + # last batch of STIR queries against the last commitment. n_vars_final = cfg.num_variables - sum( whir_folding_factor_at_round(i) for i in range(n_rounds + 1) ) final_coeffs = state.next_extension_scalars_vec(1 << n_vars_final) - # Final STIR challenges (against the last commitment) — uses final_round_config. - # In Rust: final.domain_size = round_params.last().domain_size >> rs_reduction_factor(n_rounds-1). - # `whir_domain_size_at(num_variables, log_inv_rate, n_rounds)` already applies all the - # reductions for rounds 0..n_rounds, so it equals final.domain_size directly. final_domain_size = whir_domain_size_at(cfg.num_variables, cfg.log_inv_rate, n_rounds) final_folding_factor = whir_folding_factor_at_round(n_rounds) - final_num_variables = ( - cfg.num_variables - sum(whir_folding_factor_at_round(i) for i in range(n_rounds + 1)) - ) folded_domain_size_final = final_domain_size >> final_folding_factor - folded_gen_final = two_adic_generator( - final_domain_size.bit_length() - 1 - final_folding_factor - ) + folded_gen_final = two_adic_generator(final_domain_size.bit_length() - 1 - final_folding_factor) + log_height_final = folded_domain_size_final.bit_length() - 1 state.check_pow_grinding(cfg.final_query_pow_bits) - indices_final = state.sample_in_range( - folded_domain_size_final.bit_length() - 1, cfg.final_queries - ) - log_height_final = folded_domain_size_final.bit_length() - 1 - answers_ef: list[list[EF]] = [] + indices_final = state.sample_in_range(log_height_final, cfg.final_queries) + final_stir: list[SparseStatement] = [] for idx in indices_final: op = state.next_merkle_opening() - if not merkle_verify_path( - prev_commitment.root, log_height_final, idx, op.leaf_data, op.path - ): + if not merkle_verify_path(prev_commitment.root, log_height_final, idx, op.leaf_data, op.path): raise ProofError("Final Merkle verification failed") if n_rounds == 0: - answers_ef.append([EF.from_base(f) for f in op.leaf_data]) + answers = [EF.from_base(f) for f in op.leaf_data] else: - ans: list[EF] = [] - for i in range(0, len(op.leaf_data), EF.DIMENSION): - ans.append(EF(op.leaf_data[i : i + EF.DIMENSION])) - answers_ef.append(ans) - folds_final = [eval_multilinear_evals(a, round_folding[-1]) for a in answers_ef] - final_stir: list[SparseStatement] = [] - for idx, fold in zip(indices_final, folds_final): - gen_pow = pow(int(folded_gen_final.value), idx, P) - ef_pt = EF.from_base(Fp(gen_pow)) - expanded = expand_from_univariate(ef_pt, final_num_variables) - final_stir.append(SparseStatement.dense(expanded, fold)) + answers = [EF(op.leaf_data[i : i + EF.DIMENSION]) for i in range(0, len(op.leaf_data), EF.DIMENSION)] + fold = eval_multilinear_evals(answers, round_folding[-1]) + ef_pt = EF.from_base(Fp(pow(int(folded_gen_final.value), idx, P))) + final_stir.append(SparseStatement.dense(expand_from_univariate(ef_pt, n_vars_final), fold)) # Verify STIR constraints directly on final polynomial coefficients. for c in final_stir: @@ -1251,34 +1172,6 @@ def whir_verify( # ─── Table metadata (mirror of lean_vm::tables::table_trait) ────────────────────────────────────────────────────────── -@dataclass(frozen=True) -class BusData: - """One field of a precompile bus message: either a column index or a - constant. Mirrors `lean_vm::BusData`. - """ - - kind: str # "Column" or "Constant" - value: int - - @classmethod - def from_json(cls, obj: dict) -> "BusData": - return cls(kind=obj["kind"], value=int(obj["value"])) - - -@dataclass(frozen=True) -class Bus: - """Per-table bus descriptor. `direction` is "Pull" or "Push".""" - - direction: str - selector: int - data: tuple[BusData, ...] - - @property - def direction_flag(self) -> Fp: - # Mirrors `BusDirection::to_field_flag`: Pull = -1, Push = +1. - return Fp(P - 1) if self.direction == "Pull" else Fp(1) - - @dataclass(frozen=True) class Lookup: """A single memory lookup descriptor (`LookupIntoMemory` in Rust).""" @@ -1289,36 +1182,27 @@ class Lookup: @dataclass(frozen=True) class TableMeta: - """Bundle of everything the verifier needs about one table.""" + """The bits of `lean_vm::Table` the verifier actually consumes.""" name: str n_columns: int - bus: Bus + bus_direction: str # "Pull" or "Push" lookups: tuple[Lookup, ...] def tables_from_json(obj: list[dict]) -> list[TableMeta]: - out: list[TableMeta] = [] - for t in obj: - bus_obj = t["bus"] - bus = Bus( - direction=bus_obj["direction"], - selector=int(bus_obj["selector"]), - data=tuple(BusData.from_json(d) for d in bus_obj["data"]), - ) - lookups = tuple( - Lookup(index=int(l["index"]), values=tuple(int(v) for v in l["values"])) - for l in t["lookups"] - ) - out.append( - TableMeta( - name=t["name"], - n_columns=int(t["n_columns"]), - bus=bus, - lookups=lookups, - ) + return [ + TableMeta( + name=t["name"], + n_columns=int(t["n_columns"]), + bus_direction=t["bus"]["direction"], + lookups=tuple( + Lookup(index=int(l["index"]), values=tuple(int(v) for v in l["values"])) + for l in t["lookups"] + ), ) - return out + for t in obj + ] @@ -1331,9 +1215,7 @@ def compute_stacked_n_vars( table_log_heights: dict[str, int], table_n_columns: dict[str, int], ) -> int: - """Mirror of `stacked_pcs::compute_stacked_n_vars`. - - The stacked polynomial concatenates: + """The stacked polynomial concatenates: - 2 copies of memory -> 2 * 2^log_memory - one bytecode accumulator padded -> 2^max(log_bytecode, max_table_log_n_rows) - per table: n_columns * 2^log_n_rows @@ -1357,9 +1239,7 @@ def stacked_pcs_global_statements( tables: dict[str, TableMeta], constants: dict, ) -> list[SparseStatement]: - """Port of `stacked_pcs::stacked_pcs_global_statements`. - - Stacks the per-table column claims into the global statement list passed to + """Stacks the per-table column claims into the global statement list passed to `WhirConfig::verify`. Tables are processed in descending-height order. """ assert len(table_log_heights) == len(committed_statements) @@ -1431,9 +1311,7 @@ def stacked_pcs_parse_commitment( table_n_columns: dict[str, int], execution_table_name: str = "execution", ) -> ParsedCommitment: - """Port of `stacked_pcs_parse_commitment`. - - - Memory must be at least as wide as the execution table. + """- Memory must be at least as wide as the execution table. - The execution table must be the tallest table. - The stacked-poly size must fit within the WHIR domain bound. The actual commitment parsing is then delegated to `parsed_commitment_parse`. @@ -1475,9 +1353,7 @@ def sumcheck_verify( expected_sum: EF, eq_alphas: Sequence[EF] | None, ) -> Evaluation: - """Mirror of `sumcheck::sumcheck_verify`. - - Reads `n_vars` round polynomials, each of `degree + 1` coefficients (so the + """Reads `n_vars` round polynomials, each of `degree + 1` coefficients (so the bare polynomial is degree-`degree`; in the `eq_alphas` path the verifier extracts the bare poly and re-expands with `eq(α_round, X)`). Returns the final point + claimed value. @@ -1507,8 +1383,7 @@ def verify_gkr_quotient( state: VerifierState, n_vars: int, ) -> tuple[EF, list[EF], EF, EF]: - """Mirror of `verify_gkr_quotient`. Returns - `(quotient, gkr_point, claims_num, claims_den)`. + """`(quotient, gkr_point, claims_num, claims_den)`. """ assert n_vars > N_VARS_TO_SEND_GKR_COEFFS send_len = 1 << N_VARS_TO_SEND_GKR_COEFFS @@ -1572,9 +1447,7 @@ def _quotient_sum(nums: Sequence[EF], dens: Sequence[EF]) -> EF: def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: - """Mirror of `poly::to_big_endian_in_field`. - - Returns the `bit_count` bits of `value` MSB-first, each as `EF::ZERO`/`EF::ONE`. + """Returns the `bit_count` bits of `value` MSB-first, each as `EF::ZERO`/`EF::ONE`. """ return [EF.one() if (value >> (bit_count - 1 - i)) & 1 else EF.zero() for i in range(bit_count)] @@ -1587,22 +1460,18 @@ def from_end(seq: Sequence, n: int) -> list: def mle_of_01234567_etc(point: Sequence[EF]) -> EF: - """Mirror of `utils::mle_of_01234567_etc`. - - Evaluates the multilinear polynomial whose evaluations on `{0,1}^n` are + """Evaluates the multilinear polynomial whose evaluations on `{0,1}^n` are `f(i) = i` (with `i` interpreted big-endian), at `point`. """ if not point: return EF.zero() e = mle_of_01234567_etc(point[1:]) - bit = EF(_from_int(1 << (len(point) - 1)).c) + bit = EF.from_base(Fp(1 << (len(point) - 1))) return (EF.one() - point[0]) * e + point[0] * (e + bit) def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: - """Mirror of `poly::mle_of_zeros_then_ones`. - - Evaluates the multilinear of `[0, ..., 0, 1, ..., 1]` (`n_zeros` zeros, then + """Evaluates the multilinear of `[0, ..., 0, 1, ..., 1]` (`n_zeros` zeros, then `2^len(point) - n_zeros` ones) at `point`. """ n_values = 1 << len(point) @@ -1618,9 +1487,7 @@ def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: def finger_print(table: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: - """Mirror of `utils::finger_print`. - - Computes `Σᵢ alphas_eq_poly[i] · data[i] + alphas_eq_poly[-1] · table`. + """Computes `Σᵢ alphas_eq_poly[i] · data[i] + alphas_eq_poly[-1] · table`. """ assert len(alphas_eq_poly) > len(data) acc = EF.zero() @@ -1630,15 +1497,10 @@ def finger_print(table: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> return acc -def _from_int(x: int) -> EF: - return EF.from_base(Fp(x % P)) - - def sort_tables_by_height( table_log_heights: dict[str, int], ) -> list[tuple[str, int]]: - """Mirror of `lean_vm::sort_tables_by_height` — descending by `log_n_rows`, - `BTreeMap` ordering (= alphabetical) breaks ties. + """`BTreeMap` ordering (= alphabetical) breaks ties. """ items = sorted(table_log_heights.items()) # alphabetical items.sort(key=lambda kv: -kv[1]) @@ -1665,7 +1527,6 @@ def eval_eq(point: Sequence[EF]) -> list[EF]: @dataclass class GenericLogupStatements: - """Mirror of `GenericLogupStatements` returned by `verify_generic_logup`.""" memory_and_acc_point: list[EF] value_memory: EF @@ -1687,7 +1548,6 @@ def _compute_total_active_len_logup( table_lookups: dict[str, list[Lookup]], execution_name: str, ) -> int: - """Mirror of `logup::compute_total_active_len`.""" max_table_height = 1 << tables_sorted[0][1] log_n_cycles = next(h for n, h in tables_sorted if n == execution_name) @@ -1715,9 +1575,7 @@ def verify_generic_logup( constants: dict, execution_name: str = "execution", ) -> GenericLogupStatements: - """Port of `verify_generic_logup`. - - `bytecode_multilinear` is the flat coefficient vector of length + """`bytecode_multilinear` is the flat coefficient vector of length `2^(log_bytecode + ceil(log2(N_INSTRUCTION_COLUMNS)))` — what the Rust verifier holds as `&bytecode.instructions_multilinear`. @@ -1732,9 +1590,8 @@ def verify_generic_logup( dom_byte = constants["logup_bytecode_domainsep"] tables_sorted = sort_tables_by_height(table_log_heights) - log_bytecode = log2_strict_usize( - len(bytecode_multilinear) // _next_pow_two(n_instr_cols) - ) + n_instr_padded = 1 << log2_ceil_usize(n_instr_cols) # next power of 2 + log_bytecode = log2_strict_usize(len(bytecode_multilinear) // n_instr_padded) table_lookups = {name: list(tables[name].lookups) for name in table_log_heights} total_active_len = _compute_total_active_len_logup( @@ -1897,22 +1754,13 @@ def pref_at(offset: int, log_height: int) -> EF: ) -def _next_pow_two(x: int) -> int: - if x <= 1: - return 1 - return 1 << (x - 1).bit_length() - - - # ─── AIR sumcheck helpers (port of sub_protocols/air_sumcheck.rs) ────────────────────────────────────────────────────────── def natural_ordering_point_for_session( sumcheck_air_point: Sequence[EF], log_n_rows: int ) -> list[EF]: - """Mirror of `air_sumcheck::natural_ordering_point_for_session`. - - Takes the last `log_n_rows` coordinates of the AIR sumcheck point and + """Takes the last `log_n_rows` coordinates of the AIR sumcheck point and reverses them. """ if log_n_rows == 0: @@ -1927,9 +1775,7 @@ def back_loaded_table_contribution( constraint_eval: EF, eta_power: EF, ) -> EF: - """Mirror of `verify_execution::back_loaded_table_contribution`. - - eta^t · (Π i∈[0, n_max - n_t) sumcheck_point[i]) · eq(bus_point, natural_point) · constraint_eval + """eta^t · (Π i∈[0, n_max - n_t) sumcheck_point[i]) · eq(bus_point, natural_point) · constraint_eval """ n_t = len(bus_point) n_max = len(sumcheck_air_point) @@ -1967,9 +1813,7 @@ def columns_evals_up_and_down( class ConstraintFolder: - """Mirror of `air::constraint_folder::ConstraintFolder` over EF. - - Each `assert_zero(x)` (or `assert_zero_ef`) contributes + """Each `assert_zero(x)` (or `assert_zero_ef`) contributes `alpha_powers[constraint_index] · x` to the accumulator. """ @@ -1996,23 +1840,8 @@ def assert_bool(self, x: EF) -> None: self.assert_zero(x * (EF.one() - x)) -# Registry of per-table AIR constraint evaluators. Each function takes -# `(folder, table_meta, extra_data)` and emits constraints via the folder. -_AIR_EVALUATORS: dict[str, "callable"] = {} - - -def register_air_evaluator(name: str): - def decorator(fn): - _AIR_EVALUATORS[name] = fn - return fn - return decorator - - def _eval_virtual_bus_column(extra_data: dict, flag: EF, data: Sequence[EF]) -> EF: - """Port of `tables::utils::eval_virtual_bus_column`. - - `(Σ alphas[i] · data[i] + alphas[-1] · LOGUP_PRECOMPILE_DOMAINSEP) · bus_beta + flag`. - """ + """`(Σ αᵢ · dataᵢ + α_last · LOGUP_PRECOMPILE_DOMAINSEP) · bus_beta + flag`.""" alphas: list[EF] = extra_data["logup_alphas_eq_poly"] bus_beta: EF = extra_data["bus_beta"] assert len(data) < len(alphas) @@ -2029,19 +1858,17 @@ def air_constraint_eval( alpha_powers: Sequence[EF], extra_data: dict, ) -> EF: - """Evaluate the table's AIR constraint polynomial at `col_evals`. + """Evaluate `table`'s AIR constraint polynomial at the given column evals. `col_evals[:n_columns]` is the `up` row, `col_evals[n_columns:]` is the - `down` row (only for tables with `down_column_indexes`). - `extra_data` carries `logup_alphas_eq_poly`, `bus_beta`, `c` (logup_c) — - used by the precompile bus constraints. + `down` row (only present for tables with `down_column_indexes`). """ - n_up = table.n_columns - folder = ConstraintFolder(col_evals[:n_up], col_evals[n_up:], alpha_powers) - impl = _AIR_EVALUATORS.get(table.name) - if impl is None: - raise NotImplementedError(f"AIR evaluator not yet ported for table {table.name!r}") - impl(folder, table, extra_data) + folder = ConstraintFolder(col_evals[:table.n_columns], col_evals[table.n_columns:], alpha_powers) + { + "execution": _eval_air_execution, + "extension_op": _eval_air_extension_op, + "poseidon16_compress": _eval_air_poseidon16, + }[table.name](folder, table, extra_data) return folder.accumulator @@ -2060,7 +1887,6 @@ def air_constraint_eval( } -@register_air_evaluator("execution") def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: up = folder.up down = folder.down @@ -2194,7 +2020,6 @@ def dot(av: Sequence[EF], bv: Sequence[EF]) -> EF: ] -@register_air_evaluator("extension_op") def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: up = folder.up down = folder.down @@ -2307,41 +2132,28 @@ def _ef_cube(x: EF) -> EF: return x * x * x -def _load_poseidon1_constants() -> dict: +@functools.cache +def _p1c() -> dict: + """Poseidon1 round constants + matrices, lifted from the Rust-dumped JSON.""" import json from pathlib import Path - - raw = json.loads( - (Path(__file__).with_name("poseidon1_constants.json")).read_text() - ) - - def to_fp_mat(m: list[list[int]]) -> list[list[Fp]]: - return [[Fp(v) for v in row] for row in m] - + raw = json.loads(Path(__file__).with_name("poseidon1_constants.json").read_text()) + mat = lambda m: [[Fp(v) for v in row] for row in m] + vec = lambda v: [Fp(x) for x in v] return { - "half_full_rounds": raw["half_full_rounds"], - "partial_rounds": raw["partial_rounds"], - "initial_constants": to_fp_mat(raw["initial_constants"]), - "final_constants": to_fp_mat(raw["final_constants"]), - "sparse_m_i": to_fp_mat(raw["sparse_m_i"]), - "sparse_first_row": to_fp_mat(raw["sparse_first_row"]), - "sparse_v": to_fp_mat(raw["sparse_v"]), - "sparse_first_rc": [Fp(v) for v in raw["sparse_first_round_constants"]], - "sparse_scalar_rc": [Fp(v) for v in raw["sparse_scalar_round_constants"]], - "mds_dense": to_fp_mat(raw["mds_dense"]), + "half_full_rounds": raw["half_full_rounds"], + "partial_rounds": raw["partial_rounds"], + "initial_constants": mat(raw["initial_constants"]), + "final_constants": mat(raw["final_constants"]), + "sparse_m_i": mat(raw["sparse_m_i"]), + "sparse_first_row": mat(raw["sparse_first_row"]), + "sparse_v": mat(raw["sparse_v"]), + "sparse_first_rc": vec(raw["sparse_first_round_constants"]), + "sparse_scalar_rc": vec(raw["sparse_scalar_round_constants"]), + "mds_dense": mat(raw["mds_dense"]), } -_POSEIDON1_CONSTS: dict | None = None - - -def _p1c() -> dict: - global _POSEIDON1_CONSTS - if _POSEIDON1_CONSTS is None: - _POSEIDON1_CONSTS = _load_poseidon1_constants() - return _POSEIDON1_CONSTS - - _POSEIDON_WIDTH = 16 _HALF_DIGEST_LEN = 4 _POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1 # = 2 @@ -2376,7 +2188,6 @@ def _eval_2_full_rounds( rc1: list[Fp], rc2: list[Fp], ) -> list[EF]: - """Mirror of `eval_2_full_rounds_16` (16 constraints emitted).""" state = _cube_vec(_add_kb_vec(state, rc1)) state = _mds_dense_apply(state) state = _cube_vec(_add_kb_vec(state, rc2)) @@ -2396,7 +2207,6 @@ def _eval_last_2_full_rounds( rc2: list[Fp], flag_half_output: EF, ) -> None: - """Mirror of `eval_last_2_full_rounds_16` (4 + 4 = 8 constraints).""" state = _cube_vec(_add_kb_vec(state, rc1)) state = _mds_dense_apply(state) state = _cube_vec(_add_kb_vec(state, rc2)) @@ -2412,7 +2222,6 @@ def _eval_last_2_full_rounds( def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) -> None: - """Mirror of `eval_poseidon1_16`. Emits 80 (non-bus) constraints.""" const = _p1c() state = list(cols["inputs"]) initial_state = list(cols["inputs"]) # used for compression at the end @@ -2480,7 +2289,6 @@ def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: return out -@register_air_evaluator("poseidon16_compress") def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: up = folder.up one = EF.one() @@ -2566,42 +2374,19 @@ def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: class AirStageResult: """Outputs of the AIR sumcheck stage, fed into the WHIR finale.""" - sumcheck_air_point: list[EF] - bus_beta: EF - air_alpha: EF - eta: EF committed_statements: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]] public_memory_random_point: list[EF] public_memory_eval: EF -def _max_air_constraints(tables: dict[str, TableMeta]) -> int: - # Hardcoded mirrors of `
::n_constraints` for each table. - NC = {"execution": 13, "extension_op": 16, "poseidon16_compress": 81} - return max(NC[t] for t in tables) - - -def _table_degree_air(table_name: str) -> int: - # Hardcoded mirrors of `
::degree_air`. - return {"execution": 5, "extension_op": 4, "poseidon16_compress": 10}[table_name] - - -def _table_down_column_indexes(table_name: str) -> list[int]: - """Hardcoded mirrors of `
::down_column_indexes`.""" - if table_name == "execution": - # COL_PC=0, COL_FP=1 - return [0, 1] - if table_name == "extension_op": - # COL_START, COL_IS_BE, COL_LEN, COL_FLAG_ADD, COL_FLAG_MUL, - # COL_FLAG_POLY_EQ, COL_IDX_A, COL_IDX_B, COL_COMP+0..5 - return [1, 0, 5, 2, 3, 4, 6, 7, 24, 25, 26, 27, 28] - if table_name == "poseidon16_compress": - return [] - raise KeyError(table_name) - - -def _table_n_down_columns(table_name: str) -> int: - return len(_table_down_column_indexes(table_name)) +# Per-table compile-time spec (Rust: `
::{degree_air, n_constraints, +# down_column_indexes}`). The down-column lists for `execution` and +# `extension_op` are exactly what their `Air::down_column_indexes` returns. +_TABLE_SPECS: dict[str, dict] = { + "execution": {"degree": 5, "n_constraints": 13, "down": [0, 1]}, + "extension_op": {"degree": 4, "n_constraints": 16, "down": [1, 0, 5, 2, 3, 4, 6, 7, 24, 25, 26, 27, 28]}, + "poseidon16_compress": {"degree": 10, "n_constraints": 81, "down": []}, +} def verify_air_stage( @@ -2614,18 +2399,16 @@ def verify_air_stage( public_input: Sequence[Fp], log_memory: int, ) -> AirStageResult: - """Port of the AIR-sumcheck block in `verify_execution.rs` (lines 100–179). - - Returns the per-table committed statements (point + eq_values + next_values) + """Returns the per-table committed statements (point + eq_values + next_values) and the public-memory random point + its evaluation. """ bus_beta = state.sample() air_alpha = state.sample() - max_air_constraints = _max_air_constraints(tables) + max_n_constraints = max(_TABLE_SPECS[name]["n_constraints"] for name in tables) alpha_powers: list[EF] = [] cur = EF.one() - for _ in range(max_air_constraints + 1): + for _ in range(max_n_constraints + 1): alpha_powers.append(cur) cur = cur * air_alpha @@ -2642,7 +2425,7 @@ def verify_air_stage( bus_den = logup.bus_denominators_values[name] flag = ( EF.zero() - EF.one() - if tables[name].bus.direction == "Pull" + if tables[name].bus_direction == "Pull" else EF.one() ) bus_final_value = bus_num * flag + bus_beta * (bus_den - logup_c) @@ -2650,7 +2433,7 @@ def verify_air_stage( eta_powers.append(cur) cur = cur * eta - max_full_degree = max(_table_degree_air(name) + 1 for name, _ in tables_sorted) + max_full_degree = max(_TABLE_SPECS[name]["degree"] + 1 for name, _ in tables_sorted) n_max = tables_sorted[0][1] sumcheck_result = sumcheck_verify(state, n_max, max_full_degree, initial_sum, None) @@ -2676,12 +2459,9 @@ def verify_air_stage( } for (name, log_n_rows), eta_pow in zip(tables_sorted, eta_powers): meta = tables[name] - n_down = _table_n_down_columns(name) - n_cols_total = meta.n_columns + n_down - col_evals = state.next_extension_scalars_vec(n_cols_total) - - alpha_powers_table = alpha_powers # same list — folder reads constraint_index - constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers_table, extra_data) + down_indexes = _TABLE_SPECS[name]["down"] + col_evals = state.next_extension_scalars_vec(meta.n_columns + len(down_indexes)) + constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) bus_point = from_end(logup.gkr_point, log_n_rows) natural_pt = natural_ordering_point_for_session(sumcheck_air_point, log_n_rows) @@ -2690,10 +2470,7 @@ def verify_air_stage( ) point, eq_values, next_values = columns_evals_up_and_down( - meta.n_columns, - _table_down_column_indexes(name), - col_evals, - natural_pt, + meta.n_columns, down_indexes, col_evals, natural_pt ) committed[name].append((point, eq_values, next_values)) @@ -2709,10 +2486,6 @@ def verify_air_stage( ) return AirStageResult( - sumcheck_air_point=list(sumcheck_air_point), - bus_beta=bus_beta, - air_alpha=air_alpha, - eta=eta, committed_statements=committed, public_memory_random_point=list(public_memory_random_point), public_memory_eval=public_memory_eval, @@ -2723,19 +2496,6 @@ def verify_air_stage( # ─── Top-level verifier ────────────────────────────────────────────────────────── -@dataclass(frozen=True) -class TableInfo: - """Table metadata (bus + lookups + n_columns). Built by `tables_from_json`.""" - - name: str - n_columns: int - bus: Bus - lookups: tuple[Lookup, ...] - - def to_meta(self) -> TableMeta: - return TableMeta(self.name, self.n_columns, self.bus, self.lookups) - - @dataclass class VerifyResult: """High-level outputs of `verify_execution` (mostly for diagnostics).""" @@ -2751,7 +2511,7 @@ def verify_execution( bytecode: Bytecode, public_input: Sequence[Fp], proof: Proof, - tables: Sequence[TableInfo], + tables: Sequence[TableMeta], constants: dict, bytecode_multilinear: list[Fp], ) -> VerifyResult: @@ -2801,7 +2561,7 @@ def verify_execution( table_log_heights = {t.name: log_n_rows for t, log_n_rows in zip(tables, table_log_n_rows)} table_n_columns = {t.name: t.n_columns for t in tables} - tables_by_name = {t.name: t.to_meta() for t in tables} + tables_by_name = {t.name: t for t in tables} parsed_commitment = stacked_pcs_parse_commitment( state, @@ -2902,9 +2662,7 @@ def verify_execution( def poseidon_compress_slice(data: Sequence[Fp], use_iv: bool) -> list[Fp]: - """Port of `utils::poseidon_compress_slice`. - - Hash a length-multiple-of-8 sequence into one 8-element digest using + """Hash a length-multiple-of-8 sequence into one 8-element digest using Poseidon16 in Davies-Meyer compression mode. If `use_iv` is false the first 16 elements seed the sponge directly; if true an all-zero IV is used. """ @@ -2923,56 +2681,10 @@ def poseidon_compress_slice(data: Sequence[Fp], use_iv: bool) -> list[Fp]: return h -def _load_test_vector(json_path): - """Load the Rust-dumped end-to-end test vector.""" - import array - import json - from pathlib import Path - - json_path = Path(json_path) - raw = json.loads(json_path.read_text()) - - # Bytecode multilinear (raw u32 LE sidecar). - mle_bin = (json_path.parent / raw["bytecode_multilinear_path"]).read_bytes() - arr = array.array("I") - arr.frombytes(mle_bin) - assert len(arr) == raw["bytecode_multilinear_len"] - bytecode_multilinear = [Fp(v) for v in arr] - - bytecode = Bytecode( - hash=[Fp(v) for v in raw["bytecode_hash"]], - log_size=raw["bytecode_log_size"], - ) - public_input = [Fp(v) for v in raw["public_input"]] - input_data = [Fp(v) for v in raw["input_data"]] - - transcript = [Fp(v) for v in raw["proof"]["transcript"]] - openings: list[MerkleOpening] = [] - for bucket in raw["proof"]["merkle_paths"]: - for path in restore_merkle_paths(prunedpaths_from_json(bucket)): - openings.append(MerkleOpening(leaf_data=path.leaf_data, path=path.sibling_hashes)) - proof = Proof(transcript=transcript, merkle_openings=openings) - - metas = tables_from_json(raw["tables"]) - tables = [ - TableInfo(name=m.name, n_columns=m.n_columns, bus=m.bus, lookups=m.lookups) - for m in metas - ] - - return { - "bytecode": bytecode, - "bytecode_multilinear": bytecode_multilinear, - "public_input": public_input, - "input_data": input_data, - "proof": proof, - "tables": tables, - "constants": raw["constants"], - } - - def main() -> int: """Load the end-to-end test vector and run `verify_execution`.""" - import sys + import array + import json from pathlib import Path vector_path = Path(__file__).resolve().parents[2] / "target" / "zkvm_test_vectors" / "proof.json" @@ -2984,22 +2696,34 @@ def main() -> int: return 1 print(f"Loading {vector_path.name}...") - v = _load_test_vector(vector_path) + raw = json.loads(vector_path.read_text()) + + # Bytecode multilinear (raw u32 LE sidecar). + mle_blob = (vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes() + arr = array.array("I"); arr.frombytes(mle_blob) + assert len(arr) == raw["bytecode_multilinear_len"] + bytecode_multilinear = [Fp(v) for v in arr] + + bytecode = Bytecode([Fp(v) for v in raw["bytecode_hash"]], raw["bytecode_log_size"]) + public_input = [Fp(v) for v in raw["public_input"]] + input_data = [Fp(v) for v in raw["input_data"]] + + openings = [ + MerkleOpening(leaf_data=p.leaf_data, path=p.sibling_hashes) + for bucket in raw["proof"]["merkle_paths"] + for p in restore_merkle_paths(prunedpaths_from_json(bucket)) + ] + proof = Proof(transcript=[Fp(v) for v in raw["proof"]["transcript"]], merkle_openings=openings) # Sanity: re-derive `public_input` from `input_data` to check the hash. - derived = poseidon_compress_slice(v["input_data"], use_iv=True) - if derived != v["public_input"]: + if poseidon_compress_slice(input_data, use_iv=True) != public_input: print("FAIL: poseidon_compress_slice(input_data) doesn't match dumped public_input") return 1 try: result = verify_execution( - v["bytecode"], - v["public_input"], - v["proof"], - v["tables"], - v["constants"], - v["bytecode_multilinear"], + bytecode, public_input, proof, + tables_from_json(raw["tables"]), raw["constants"], bytecode_multilinear, ) except ProofError as e: print(f"FAIL: {e}") From 2cc2b64bac5b3583754c1589b6c2e20728d1566c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 18 May 2026 00:28:41 +0200 Subject: [PATCH 08/69] wip --- crates/lean_prover/tests/dump_zkvm_vector.rs | 66 ++-- crates/lean_prover/verifier.py | 381 +++++-------------- 2 files changed, 112 insertions(+), 335 deletions(-) diff --git a/crates/lean_prover/tests/dump_zkvm_vector.rs b/crates/lean_prover/tests/dump_zkvm_vector.rs index 6a521ab83..be6e9c9f4 100644 --- a/crates/lean_prover/tests/dump_zkvm_vector.rs +++ b/crates/lean_prover/tests/dump_zkvm_vector.rs @@ -10,12 +10,11 @@ use std::fs; use std::path::PathBuf; -use backend::{Air, PrimeField32, PrunedMerklePaths}; +use backend::{Air, MerkleOpening, PrimeField32}; use lean_vm::*; use rec_aggregation::{aggregate_type_1, get_aggregation_bytecode, init_aggregation_bytecode, verify_type_1}; use serde::Serialize; use std::io::Write; -use utils::poseidon_compress_slice; use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; type F = lean_vm::F; @@ -27,24 +26,19 @@ fn f_to_u32(x: F) -> u32 { } #[derive(Serialize)] -struct PrunedPathJson { - leaf_index: usize, - siblings: Vec<[u32; DIGEST_ELEMS]>, +struct MerkleOpeningJson { + leaf_data: Vec, + path: Vec<[u32; DIGEST_ELEMS]>, } #[derive(Serialize)] -struct PrunedMerklePathsJson { - merkle_height: usize, - original_order: Vec, - leaf_data: Vec>, - paths: Vec, - n_trailing_zeros: usize, -} - -#[derive(Serialize)] -struct ProofJson { +struct RawProofJson { + /// Flat raw transcript: every absorbed group is padded to a multiple of 8 + /// (RATE) with zeros — the format the zkDSL recursion verifier reads. transcript: Vec, - merkle_paths: Vec, + /// Already-restored Merkle openings (no pruning) in the order the verifier + /// consumes them. + merkle_openings: Vec, } #[derive(Serialize)] @@ -108,27 +102,13 @@ struct OutJson { constants: ConstantsJson, snark_domain_sep: [u32; DIGEST_ELEMS], - proof: ProofJson, + proof: RawProofJson, } -fn convert_pruned(p: &PrunedMerklePaths) -> PrunedMerklePathsJson { - PrunedMerklePathsJson { - merkle_height: p.merkle_height, - original_order: p.original_order.clone(), - leaf_data: p - .leaf_data - .iter() - .map(|v| v.iter().map(|&f| f_to_u32(f)).collect()) - .collect(), - paths: p - .paths - .iter() - .map(|(idx, siblings)| PrunedPathJson { - leaf_index: *idx, - siblings: siblings.iter().map(|d| d.map(f_to_u32)).collect(), - }) - .collect(), - n_trailing_zeros: p.n_trailing_zeros, +fn convert_opening(o: &MerkleOpening) -> MerkleOpeningJson { + MerkleOpeningJson { + leaf_data: o.leaf_data.iter().map(|&f| f_to_u32(f)).collect(), + path: o.path.iter().map(|d| d.map(f_to_u32)).collect(), } } @@ -158,13 +138,13 @@ fn dump_zkvm_vector() { .expect("aggregate_type_1 failed") }; - // `verify_type_1` rebuilds `input_data` from the public info and runs the - // Rust verifier as a self-check. We grab `input_data` from the returned - // `InnerVerified` and reuse `sig.proof.proof` for the serialized proof. - let proof = sig.proof.proof.clone(); + // `verify_type_1` runs the Rust verifier (self-check) and returns the + // restored, padded raw transcript that the zkDSL recursion verifier + // expects — which is exactly what the Python verifier consumes. let verified = verify_type_1(&sig).expect("Rust verify_type_1 failed"); let input_data = verified.input_data; - let public_input = poseidon_compress_slice(&input_data, true); + let public_input = verified.input_data_hash; + let raw_proof = verified.raw_proof; let convert_bus = |bus: Bus| BusJson { direction: match bus.direction { @@ -226,9 +206,9 @@ fn dump_zkvm_vector() { ending_pc: ENDING_PC, }, snark_domain_sep: lean_prover::SNARK_DOMAIN_SEP.map(f_to_u32), - proof: ProofJson { - transcript: proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), - merkle_paths: proof.merkle_paths.iter().map(convert_pruned).collect(), + proof: RawProofJson { + transcript: raw_proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), + merkle_openings: raw_proof.merkle_openings.iter().map(convert_opening).collect(), }, }; diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 3a617d36a..42a54cfea 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -36,9 +36,10 @@ MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 -# Poseidon16 sponge parameters. This branch uses the older compression-with- -# domain-separator challenger (no `rate_fresh`/`duplex`); state is RATE-sized -# and sampling re-permutes with a per-call domain separator. +# Poseidon16 sponge parameters. The challenger uses the compression-with- +# domain-separator design: `state` is a RATE-sized buffer; `observe(chunk)` +# does `state ← permute(state || chunk)[:RATE]`; sampling re-permutes the +# state with a per-call domain separator. RATE = 8 WIDTH = 16 CAPACITY = WIDTH - RATE @@ -191,7 +192,7 @@ def pow(self, n: int) -> "EF": -# ─── Poseidon16-based Challenger (duplex sponge) ────────────────────────────────────────────────────────── +# ─── Poseidon16-based Challenger ────────────────────────────────────────────────────────── _POSEIDON16 = Poseidon1(PARAMS_16) @@ -302,56 +303,57 @@ class MerkleOpening: @dataclass class Proof: - """Verifier-side proof: matches backend::Proof. + """Verifier-side raw proof: matches backend::RawProof. - `merkle_openings` is the RESTORED list of openings (post-pruning), in the - order the verifier consumes them. + `transcript` is the flat RAW transcript (every absorbed group padded to a + multiple of RATE with zeros — the format the zkDSL recursion verifier reads). + `merkle_openings` is the list of already-restored openings in the order the + verifier consumes them (no pruning machinery on the Python side). """ transcript: list[Fp] merkle_openings: list[MerkleOpening] = field(default_factory=list) +def _next_multiple_of(n: int, m: int) -> int: + return ((n + m - 1) // m) * m + + class VerifierState: - """Mirrors fiat_shamir::verifier::VerifierState exactly.""" + """Drives the Fiat-Shamir transcript: reads scalars from `proof.transcript`, + samples challenges from the challenger, and yields restored Merkle openings. + + Every read pads to RATE (the zkDSL recursion format) — `n` real scalars are + consumed as `next_multiple_of(n, RATE)` raw scalars, the trailing positions + must be zero, and the full RATE-aligned chunk is what the challenger absorbs. + """ def __init__(self, proof: Proof) -> None: self.challenger = Challenger() - self.transcript: list[Fp] = list(proof.transcript) + self.transcript = list(proof.transcript) + self.merkle_openings = list(proof.merkle_openings) self.offset = 0 - self.merkle_openings: list[MerkleOpening] = list(proof.merkle_openings) self.merkle_opening_index = 0 - self.raw_transcript: list[Fp] = [] - - # ---- internal helpers ---------------------------------------------- - def _read(self, n: int) -> list[Fp]: - if self.offset + n > len(self.transcript): + def _read_padded(self, n: int) -> list[Fp]: + """Read `next_multiple_of(n, RATE)` raw scalars, assert the trailing + positions are zero, observe the full padded chunk, return all of it.""" + n_padded = _next_multiple_of(n, RATE) + if self.offset + n_padded > len(self.transcript): raise ProofError("ExceededTranscript") - out = self.transcript[self.offset : self.offset + n] - self.offset += n - return out - - def _absorb_and_record(self, scalars: list[Fp]) -> None: - self.challenger.observe_many(scalars) - self.raw_transcript.extend(scalars) - padded = (len(scalars) + RATE - 1) // RATE * RATE - self.raw_transcript.extend([Fp(0)] * (padded - len(scalars))) - - # ---- FSVerifier trait surface -------------------------------------- + chunk = self.transcript[self.offset : self.offset + n_padded] + self.offset += n_padded + for i in range(n, n_padded): + if int(chunk[i].value) != 0: + raise ProofError("InvalidTranscript: non-zero padding") + self.challenger.observe_many(chunk) + return chunk def observe_scalars(self, scalars: Sequence[Fp]) -> None: self.challenger.observe_many(list(scalars)) - def duplex(self) -> None: - """No-op on this branch — the older challenger has no rate-staleness - notion, so duplex calls in the WHIR verifier are simply skipped.""" - pass - def next_base_scalars_vec(self, n: int) -> list[Fp]: - scalars = self._read(n) - self._absorb_and_record(scalars) - return scalars + return self._read_padded(n)[:n] def next_extension_scalars_vec(self, n: int) -> list[EF]: flat = self.next_base_scalars_vec(n * EF.DIMENSION) @@ -372,114 +374,35 @@ def sample_in_range(self, bits: int, n_samples: int) -> list[int]: def next_merkle_opening(self) -> MerkleOpening: if self.merkle_opening_index >= len(self.merkle_openings): raise ProofError("ExceededTranscript: no more Merkle openings") - op = self.merkle_openings[self.merkle_opening_index] self.merkle_opening_index += 1 - return op + return self.merkle_openings[self.merkle_opening_index - 1] def check_pow_grinding(self, bits: int) -> None: + """Grinding witness is written as `[witness, 0, 0, 0, 0, 0, 0, 0]`.""" if bits == 0: return - witness = self._read(1) - self.challenger.observe_many(witness) - # OLD challenger: state is the RATE-sized output of the last permute; - # grinding checks state[0]. + chunk = self._read_padded(1) if int(self.challenger.state[0].value) & ((1 << bits) - 1) != 0: raise ProofError("InvalidGrindingWitness") - self.raw_transcript.append(witness[0]) - self.raw_transcript.extend([Fp(0)] * (RATE - 1)) - - def next_sumcheck_polynomial( - self, - n_coeffs: int, - claimed_sum: EF, - eq_alpha: EF | None, - ) -> list[EF]: - """Mirrors `verifier::next_sumcheck_polynomial`. - - With `eq_alpha=None`: prover sends h(X) of `n_coeffs` coeffs, omitting - `c0` (recovered from `claimed_sum = h(0) + h(1)`). We read - `(n_coeffs-1) * 5` base scalars, recover `c0`, and absorb the full - flattened polynomial. - - With `eq_alpha=Some(α)`: prover sends a "bare" polynomial of - `n_coeffs - 1` coefficients; the verifier recovers `h0` and expands to - the full degree polynomial via `expand_bare_to_full`. - """ - if eq_alpha is None: - rest = self.next_extension_scalars_vec_no_record(n_coeffs - 1) - c0 = _ef_halve(claimed_sum - _ef_sum(rest)) - full = [c0] + rest - self._absorb_and_record(_flatten_ef_list(full)) - return full - - # eq_alpha path - rest_scalars = self._read((n_coeffs - 2) * EF.DIMENSION) - rest_bare = [ - EF(rest_scalars[i : i + EF.DIMENSION]) - for i in range(0, len(rest_scalars), EF.DIMENSION) - ] - h0 = claimed_sum - eq_alpha * _ef_sum(rest_bare) - bare = [h0] + rest_bare - full_coeffs = _expand_bare_to_full(bare, eq_alpha) - self._absorb_and_record(_flatten_ef_list(full_coeffs)) - return full_coeffs - - def next_extension_scalars_vec_no_record(self, n: int) -> list[EF]: - """Read `n` extension scalars but DON'T record/absorb yet — caller does.""" - flat = self._read(n * EF.DIMENSION) - return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] -# ─── Bytecode (minimal placeholder) + helpers ────────────────────────────────────────────────────────── +# ─── Small helpers (Bytecode metadata, EF utilities, log2, padding) ────────────────────────────────────────────── @dataclass class Bytecode: - """The bytecode metadata `verify_execution` needs (hash + log size).""" - + """What `verify_execution` needs about a bytecode program.""" hash: list[Fp] log_size: int -# Multiplicative inverse of 2 mod P (KoalaBear). Used by halve operations. -_HALVE_FP = Fp(pow(2, P - 2, P)) - - -def _ef_halve(x: EF) -> EF: - return EF([c * _HALVE_FP for c in x.c]) - - -def _ef_sum(xs: Sequence[EF]) -> EF: - acc = EF.zero() - for x in xs: - acc = acc + x - return acc - - -def _flatten_ef_list(xs: Sequence[EF]) -> list[Fp]: - out: list[Fp] = [] - for x in xs: - out.extend(x.c) - return out - - -def _expand_bare_to_full(bare: list[EF], alpha: EF) -> list[EF]: - """utils::expand_bare_to_full: g(X) = eq(α, X) * h(X).""" - one_minus_alpha = EF.one() - alpha - two_alpha_minus_one = (alpha + alpha) - EF.one() - d = len(bare) - 1 - full: list[EF] = [one_minus_alpha * bare[0]] - for k in range(1, d + 1): - full.append(one_minus_alpha * bare[k] + two_alpha_minus_one * bare[k - 1]) - full.append(two_alpha_minus_one * bare[d]) - return full +# Multiplicative inverse of 2 in Fp (KoalaBear). Used to halve EF elements. +_INV_TWO = Fp(pow(2, P - 2, P)) def log2_ceil_usize(x: int) -> int: - if x <= 1: - return 0 - return (x - 1).bit_length() + return 0 if x <= 1 else (x - 1).bit_length() def log2_strict_usize(x: int) -> int: @@ -490,114 +413,11 @@ def log2_strict_usize(x: int) -> int: def padd_with_zero_to_next_power_of_two(values: Sequence[Fp]) -> list[Fp]: if not values: return [Fp(0)] - n = 1 - while n < len(values): - n <<= 1 + n = 1 << log2_ceil_usize(len(values)) return list(values) + [Fp(0)] * (n - len(values)) - -# Merkle: hashing primitives, pruned-paths restoration, Merkle verify. -# Mirrors symetric::merkle + fiat_shamir::merkle_pruning. - - - -@dataclass -class MerklePath: - - leaf_data: list[Fp] - sibling_hashes: list[list[Fp]] # each entry has DIGEST_ELEMS Fp - leaf_index: int - - -@dataclass -class PrunedMerklePaths: - - merkle_height: int - original_order: list[int] - leaf_data: list[list[Fp]] - paths: list[tuple[int, list[list[Fp]]]] # (leaf_index, siblings) with skips - n_trailing_zeros: int - - -def _lca_level(a: int, b: int) -> int: - """Number of bits needed to differ — i.e., ceiling-LCA level over the tree.""" - diff = a ^ b - return diff.bit_length() - - -def restore_merkle_paths(p: PrunedMerklePaths) -> list[MerklePath]: - """Reconstructs full sibling arrays from the pruned form using leaf hashing - and 2:1 compression (Poseidon16). Raises ProofError on malformed inputs. - """ - - h = p.merkle_height - if h >= 32: - raise ProofError("Merkle height too large") - if p.n_trailing_zeros > 1024: - raise ProofError("Merkle leaf trailing-zero count too large") - - leaf_data = [list(d) + [Fp(0)] * p.n_trailing_zeros for d in p.leaf_data] - n = len(p.paths) - - def levels(i: int) -> int: - if i == 0: - return h - return _lca_level(p.paths[i - 1][0], p.paths[i][0]) - - def skip(i: int) -> int | None: - if i + 1 < n: - return _lca_level(p.paths[i][0], p.paths[i + 1][0]) - 1 - return None - - # Backward pass: build subtree hashes. - subtree_hashes: list[list[list[Fp]]] = [[] for _ in range(n)] - for i in range(n - 1, -1, -1): - leaf_idx, stored = p.paths[i] - if leaf_idx >= (1 << h): - raise ProofError("Merkle leaf index out of range") - stored_iter = iter(stored) - cur = hash_slice(leaf_data[i]) - subtree_hashes[i].append(list(cur)) - for lvl in range(levels(i)): - if skip(i) == lvl: - try: - sibling = subtree_hashes[i + 1][lvl] - except IndexError as e: - raise ProofError("Merkle restore: missing sibling") from e - else: - try: - sibling = next(stored_iter) - except StopIteration as e: - raise ProofError("Merkle restore: stored siblings exhausted") from e - if ((leaf_idx >> lvl) & 1) == 0: - cur = poseidon16_compress(cur, sibling) - else: - cur = poseidon16_compress(sibling, cur) - subtree_hashes[i].append(list(cur)) - - # Forward pass: build the full sibling lists. - restored: list[MerklePath] = [] - for i in range(n): - leaf_idx, stored = p.paths[i] - stored_iter = iter(stored) - siblings: list[list[Fp]] = [] - for lvl in range(levels(i)): - if skip(i) == lvl: - sibling = subtree_hashes[i + 1][lvl] - else: - try: - sibling = next(stored_iter) - except StopIteration as e: - raise ProofError("Merkle restore: stored siblings exhausted (fwd)") from e - siblings.append(list(sibling)) - if restored: - # Reuse the previous restored path's siblings for the levels above. - siblings.extend(restored[-1].sibling_hashes[levels(i) :]) - restored.append(MerklePath(leaf_data=leaf_data[i], sibling_hashes=siblings, leaf_index=leaf_idx)) - - # Reorder by original_order. - return [restored[idx] for idx in p.original_order] +# ─── Merkle path verify ────────────────────────────────── def merkle_verify_path( @@ -607,31 +427,16 @@ def merkle_verify_path( opened_values: Sequence[Fp], opening_proof: Sequence[list[Fp]], ) -> bool: - + """Hash the leaf, walk up `log_height` siblings, compare to the commitment.""" if len(opening_proof) != log_height: return False cur = hash_slice(list(opened_values)) - idx = index for sibling in opening_proof: - if idx & 1 == 0: - cur = poseidon16_compress(cur, sibling) - else: - cur = poseidon16_compress(sibling, cur) - idx >>= 1 + cur = poseidon16_compress(cur, sibling) if index & 1 == 0 else poseidon16_compress(sibling, cur) + index >>= 1 return list(commit) == list(cur) -def prunedpaths_from_json(obj: dict) -> PrunedMerklePaths: - """Helper for test vectors: parse the JSON shape dumped by Rust.""" - return PrunedMerklePaths( - merkle_height=obj["merkle_height"], - original_order=list(obj["original_order"]), - leaf_data=[[Fp(v) for v in chunk] for chunk in obj["leaf_data"]], - paths=[(p["leaf_index"], [[Fp(v) for v in s] for s in p["siblings"]]) for p in obj["paths"]], - n_trailing_zeros=obj["n_trailing_zeros"], - ) - - # ─── WHIR polynomial primitives (poly + whir crates) ────────────────────────────────────────────────────────── @@ -731,22 +536,16 @@ def inner_num_variables(self) -> int: def selector_num_variables(self) -> int: return self.total_num_variables - self.inner_num_variables - @staticmethod - def new_(total: int, point: list[EF], values: list[SparseValue]) -> "SparseStatement": - assert total >= len(point) - return SparseStatement(total, point, values, is_next=False) - @staticmethod def dense(point: list[EF], value: EF) -> "SparseStatement": - return SparseStatement(len(point), point, [SparseValue(0, value)], is_next=False) + return SparseStatement(len(point), point, [SparseValue(0, value)]) @staticmethod def unique_value(total: int, index: int, value: EF) -> "SparseStatement": - return SparseStatement(total, [], [SparseValue(index, value)], is_next=False) + return SparseStatement(total, [], [SparseValue(index, value)]) @staticmethod def new_next(total: int, point: list[EF], values: list[SparseValue]) -> "SparseStatement": - assert total >= len(point) return SparseStatement(total, point, values, is_next=True) @@ -808,14 +607,9 @@ def whir_domain_size_at(num_variables: int, starting_log_inv_rate: int, round_in return 1 << domain_log -# --------------------------------------------------------------------------- -# WHIR config table — float-derived numbers only, dumped by the Rust test. -# -# Everything not in the JSON (n_rounds, per-round num_variables/log_inv_rate/ -# domain_size/folding_factor/folded_domain_gen, final_sumcheck_rounds, -# final_log_inv_rate, ...) is integer arithmetic and should be derived on the -# Python side when it's actually needed. -# --------------------------------------------------------------------------- +# The Rust-dumped JSON only carries the float-derived numbers (query counts, +# OOD samples, grinding bits); every other parameter is integer arithmetic +# we recompute on the fly via the helpers above. @dataclass(frozen=True) @@ -897,34 +691,42 @@ def parsed_commitment_parse(state: VerifierState, num_variables: int, ood_sample ) +def _check_sumcheck_identity(coeffs: list[EF], target: EF) -> None: + """`h(0) + h(1) = target`, i.e. `coeffs[0] + sum(coeffs) == target`.""" + s = coeffs[0] + for c in coeffs: + s = s + c + if s != target: + raise ProofError("Sumcheck identity failed: h(0) + h(1) != target") + + +def _eval_univariate(coeffs: list[EF], x: EF) -> EF: + """Horner: c[0] + c[1]*x + c[2]*x^2 + ...""" + acc = EF.zero() + for c in reversed(coeffs): + acc = acc * x + c + return acc + + def verify_sumcheck_rounds( state: VerifierState, claimed_sum_ref: list[EF], # 1-element box, mutated in-place rounds: int, pow_bits: int, ) -> list[EF]: - """Returns the folding randomness for these rounds. Mutates `claimed_sum_ref[0]`. - """ + """Degree-2 sumcheck (3 coeffs per round). Returns the folding randomness + and mutates `claimed_sum_ref[0]` to the final claim.""" randomness: list[EF] = [] for _ in range(rounds): - coeffs = state.next_sumcheck_polynomial(3, claimed_sum_ref[0], None) + coeffs = state.next_extension_scalars_vec(3) + _check_sumcheck_identity(coeffs, claimed_sum_ref[0]) state.check_pow_grinding(pow_bits) r = state.sample() - # Evaluate cubic poly (length-3 coeffs in standard univariate basis). - # DensePolynomial::evaluate uses Horner-style on coeffs[0..n]. claimed_sum_ref[0] = _eval_univariate(coeffs, r) randomness.append(r) return randomness -def _eval_univariate(coeffs: list[EF], x: EF) -> EF: - """Standard univariate evaluation: c[0] + c[1]*x + c[2]*x^2 + ...""" - acc = EF.zero() - for c in reversed(coeffs): - acc = acc * x + c - return acc - - def combine_constraints( state: VerifierState, claimed_sum_ref: list[EF], @@ -1064,7 +866,6 @@ def whir_verify( prev_commitment = parsed_commitment # OODS + initial statement combine. - state.duplex() initial_constraints = prev_commitment.oods_constraints() + statement combo = combine_constraints(state, claimed_sum_ref, initial_constraints) round_constraints.append((combo, initial_constraints)) @@ -1102,8 +903,6 @@ def whir_verify( folding_randomness=round_folding[-1], ) constraints_r = new_commitment.oods_constraints() + stir_constraints - - state.duplex() combo_r = combine_constraints(state, claimed_sum_ref, constraints_r) round_constraints.append((combo_r, constraints_r)) @@ -1351,18 +1150,15 @@ def sumcheck_verify( n_vars: int, degree: int, expected_sum: EF, - eq_alphas: Sequence[EF] | None, ) -> Evaluation: - """Reads `n_vars` round polynomials, each of `degree + 1` coefficients (so the - bare polynomial is degree-`degree`; in the `eq_alphas` path the verifier - extracts the bare poly and re-expands with `eq(α_round, X)`). - Returns the final point + claimed value. - """ + """Reads `n_vars` round polynomials in standard univariate basis, each with + `degree + 1` coefficients. Checks `h(0) + h(1) = target` each round, then + folds in the challenge. Returns the final point + claimed value.""" target = expected_sum challenges: list[EF] = [] - for round_idx in range(n_vars): - eq_alpha = eq_alphas[round_idx] if eq_alphas is not None else None - coeffs = state.next_sumcheck_polynomial(degree + 1, target, eq_alpha) + for _ in range(n_vars): + coeffs = state.next_extension_scalars_vec(degree + 1) + _check_sumcheck_identity(coeffs, target) r = state.sample() challenges.append(r) target = _eval_univariate(coeffs, r) @@ -1412,8 +1208,7 @@ def _verify_gkr_quotient_step( ) -> tuple[list[EF], EF, EF]: alpha = state.sample() expected_sum = claims_num + alpha * claims_den - eq_alphas_rev = list(reversed(point)) - postponed = sumcheck_verify(state, n_vars, 3, expected_sum, eq_alphas_rev) + postponed = sumcheck_verify(state, n_vars, 3, expected_sum) # Rust: postponed.point.0.reverse(); postponed_point = list(reversed(postponed.point)) inner_evals = state.next_extension_scalars_vec(4) @@ -1944,7 +1739,7 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: nu_a_minus_one = nu_a - one add = aux * EF.from_base(Fp(2)) - aux * aux - deref = _ef_halve(aux * (aux - one)) + deref = aux * (aux - one) * EF.from_base(_INV_TWO) is_precompile = -(add + mul + deref + jump - one) # Constraint 1: bus column (assert_zero_ef) @@ -2436,7 +2231,7 @@ def verify_air_stage( max_full_degree = max(_TABLE_SPECS[name]["degree"] + 1 for name, _ in tables_sorted) n_max = tables_sorted[0][1] - sumcheck_result = sumcheck_verify(state, n_max, max_full_degree, initial_sum, None) + sumcheck_result = sumcheck_verify(state, n_max, max_full_degree, initial_sum) sumcheck_air_point = sumcheck_result.point claimed_air_final_value = sumcheck_result.value @@ -2709,9 +2504,11 @@ def main() -> int: input_data = [Fp(v) for v in raw["input_data"]] openings = [ - MerkleOpening(leaf_data=p.leaf_data, path=p.sibling_hashes) - for bucket in raw["proof"]["merkle_paths"] - for p in restore_merkle_paths(prunedpaths_from_json(bucket)) + MerkleOpening( + leaf_data=[Fp(v) for v in o["leaf_data"]], + path=[[Fp(v) for v in d] for d in o["path"]], + ) + for o in raw["proof"]["merkle_openings"] ] proof = Proof(transcript=[Fp(v) for v in raw["proof"]["transcript"]], merkle_openings=openings) From 9fdebb88bbcec6456a10605cf9ae9022f55126d4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 18 May 2026 08:46:45 +0200 Subject: [PATCH 09/69] wip --- crates/lean_prover/verifier.py | 792 +++++++++++---------------------- 1 file changed, 256 insertions(+), 536 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 42a54cfea..9d8837253 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -24,7 +24,7 @@ from __future__ import annotations import functools -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Sequence from lean_spec.subspecs.koalabear import Fp, P @@ -303,16 +303,13 @@ class MerkleOpening: @dataclass class Proof: - """Verifier-side raw proof: matches backend::RawProof. - - `transcript` is the flat RAW transcript (every absorbed group padded to a - multiple of RATE with zeros — the format the zkDSL recursion verifier reads). - `merkle_openings` is the list of already-restored openings in the order the - verifier consumes them (no pruning machinery on the Python side). - """ + """Mirrors `backend::RawProof`. `transcript` is the flat raw transcript + (every absorbed group padded to a multiple of RATE with zeros — the format + the zkDSL recursion verifier reads). `merkle_openings` is the list of + already-restored openings in consumption order.""" transcript: list[Fp] - merkle_openings: list[MerkleOpening] = field(default_factory=list) + merkle_openings: list[MerkleOpening] def _next_multiple_of(n: int, m: int) -> int: @@ -442,9 +439,8 @@ def merkle_verify_path( def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: - """[x, x^2, x^4, ..., x^{2^{n-1}}] — matches MultilinearPoint::expand_from_univariate.""" - out: list[EF] = [] - cur = x + """`[x, x², x⁴, …, x^(2^(n−1))]` — `MultilinearPoint::expand_from_univariate`.""" + out, cur = [], x for _ in range(num_variables): out.append(cur) cur = cur * cur @@ -452,12 +448,12 @@ def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: - """Π (1 + 2 a_i b_i − a_i − b_i) (eq polynomial).""" + """`Π (1 − a_i − b_i + 2·a_i·b_i)` — multilinear `eq` polynomial.""" assert len(a) == len(b) - one = EF.one() - acc = one + one, acc = EF.one(), EF.one() for x, y in zip(a, b): - acc = acc * (one + (x * y) + (x * y) - x - y) + xy = x * y + acc = acc * (one + xy + xy - x - y) return acc @@ -499,6 +495,51 @@ def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: return cur[0] +def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: + """Same fold as `eval_multilinear_evals`, specialized for base-field evals. + + Uses numpy to skip per-scalar Python/`Fp` wrapper overhead — the bytecode + multilinear is a 2²²-entry fold, far too big for the generic path. + """ + import numpy as np + assert len(base_evals) == 1 << len(point) + pt = [tuple(int(ci.value) for ci in p.c) for p in point] + cur = np.asarray(base_evals, dtype=np.int64) % P + # First round: base → EF. a + (b-a)*r, with r ∈ EF, a,b ∈ base. + a, b = cur[0::2], cur[1::2] + d = (b - a) % P + r = pt[-1] + cur = np.stack( + [(a + d * r[0]) % P, *[(d * r[k]) % P for k in range(1, 5)]], + axis=1, + ) + # Remaining rounds: EF × EF. Schoolbook product reduced mod X^5+X^2-1 + # (using X^5≡1-X², X^6≡X-X³, X^7≡X²-X⁴, X^8≡X³+X²-1). + # Reduce every multiply before summing to stay inside int64. + for r0, r1, r2, r3, r4 in (pt[i] for i in range(len(pt) - 2, -1, -1)): + a, b = cur[0::2], cur[1::2] + d = (b - a) % P + d0, d1, d2, d3, d4 = d[:, 0], d[:, 1], d[:, 2], d[:, 3], d[:, 4] + m = lambda x, y: (x * y) % P + p0 = m(d0, r0) + p1 = (m(d0, r1) + m(d1, r0)) % P + p2 = (m(d0, r2) + m(d1, r1) + m(d2, r0)) % P + p3 = (m(d0, r3) + m(d1, r2) + m(d2, r1) + m(d3, r0)) % P + p4 = (m(d0, r4) + m(d1, r3) + m(d2, r2) + m(d3, r1) + m(d4, r0)) % P + p5 = (m(d1, r4) + m(d2, r3) + m(d3, r2) + m(d4, r1)) % P + p6 = (m(d2, r4) + m(d3, r3) + m(d4, r2)) % P + p7 = (m(d3, r4) + m(d4, r3)) % P + p8 = m(d4, r4) + cur = np.stack([ + (a[:, 0] + p0 + p5 - p8) % P, + (a[:, 1] + p1 + p6) % P, + (a[:, 2] + p2 - p5 + p7 + p8) % P, + (a[:, 3] + p3 - p6 + p8) % P, + (a[:, 4] + p4 - p7) % P, + ], axis=1) + return EF([Fp(int(v)) for v in cur[0]]) + + def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: """poly::eval_multilinear_coeffs: split coeffs in half, recurse. @@ -661,43 +702,30 @@ def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: @dataclass class ParsedCommitment: - num_variables: int root: list[Fp] # length DIGEST_ELEMS ood_points: list[EF] ood_answers: list[EF] def oods_constraints(self) -> list[SparseStatement]: - """One dense SparseStatement per (point, eval) pair.""" - out: list[SparseStatement] = [] - for p, ev in zip(self.ood_points, self.ood_answers): - point = expand_from_univariate(p, self.num_variables) - out.append(SparseStatement.dense(point, ev)) - return out + return [ + SparseStatement.dense(expand_from_univariate(p, self.num_variables), ev) + for p, ev in zip(self.ood_points, self.ood_answers) + ] def parsed_commitment_parse(state: VerifierState, num_variables: int, ood_samples: int) -> ParsedCommitment: root = state.next_base_scalars_vec(DIGEST_ELEMS) - ood_points: list[EF] = [] - ood_answers: list[EF] = [] - if ood_samples > 0: - ood_points = state.sample_vec(ood_samples) - ood_answers = state.next_extension_scalars_vec(ood_samples) - return ParsedCommitment( - num_variables=num_variables, - root=root, - ood_points=ood_points, - ood_answers=ood_answers, - ) + ood_points = state.sample_vec(ood_samples) if ood_samples else [] + ood_answers = state.next_extension_scalars_vec(ood_samples) if ood_samples else [] + return ParsedCommitment(num_variables, root, ood_points, ood_answers) -def _check_sumcheck_identity(coeffs: list[EF], target: EF) -> None: - """`h(0) + h(1) = target`, i.e. `coeffs[0] + sum(coeffs) == target`.""" - s = coeffs[0] - for c in coeffs: - s = s + c - if s != target: - raise ProofError("Sumcheck identity failed: h(0) + h(1) != target") +@dataclass +class Evaluation: + """Claim that a multilinear evaluates to `value` at `point`.""" + point: list[EF] + value: EF def _eval_univariate(coeffs: list[EF], x: EF) -> EF: @@ -708,39 +736,47 @@ def _eval_univariate(coeffs: list[EF], x: EF) -> EF: return acc -def verify_sumcheck_rounds( +def verify_sumcheck( state: VerifierState, - claimed_sum_ref: list[EF], # 1-element box, mutated in-place - rounds: int, - pow_bits: int, -) -> list[EF]: - """Degree-2 sumcheck (3 coeffs per round). Returns the folding randomness - and mutates `claimed_sum_ref[0]` to the final claim.""" - randomness: list[EF] = [] - for _ in range(rounds): - coeffs = state.next_extension_scalars_vec(3) - _check_sumcheck_identity(coeffs, claimed_sum_ref[0]) + target: EF, + n_vars: int, + degree: int, + pow_bits: int = 0, +) -> "Evaluation": + """Read `n_vars` round polynomials (degree-`degree`, sent as `degree + 1` + coefficients). Each round: check `h(0) + h(1) == target`, optional PoW + grinding, sample a challenge, fold the target. Returns (point, value).""" + point: list[EF] = [] + for _ in range(n_vars): + coeffs = state.next_extension_scalars_vec(degree + 1) + # h(0) + h(1) = coeffs[0] + sum(coeffs). + s = coeffs[0] + for c in coeffs: + s = s + c + if s != target: + raise ProofError("Sumcheck identity failed: h(0) + h(1) != target") state.check_pow_grinding(pow_bits) r = state.sample() - claimed_sum_ref[0] = _eval_univariate(coeffs, r) - randomness.append(r) - return randomness + point.append(r) + target = _eval_univariate(coeffs, r) + return Evaluation(point=point, value=target) def combine_constraints( state: VerifierState, - claimed_sum_ref: list[EF], + target: EF, constraints: list[SparseStatement], -) -> list[EF]: +) -> tuple[EF, list[EF]]: + """Linear combination of constraint values by random `γ` powers. Returns + the updated target and the per-value combination weights `[1, γ, γ², ...]`.""" gamma: EF = state.sample() - combination = [EF.one()] + combo = [EF.one()] for smt in constraints: for v in smt.values: - pow_prev = combination[-1] - claimed_sum_ref[0] = claimed_sum_ref[0] + pow_prev * v.value - combination.append(pow_prev * gamma) - combination.pop() - return combination + target = target + combo[-1] * v.value + combo.append(combo[-1] * gamma) + combo.pop() + return target, combo def verify_stir_challenges( @@ -859,39 +895,26 @@ def whir_verify( assert s.total_num_variables == parsed_commitment.num_variables n_rounds, final_sumcheck_rounds = whir_n_rounds_and_final_sumcheck(cfg.num_variables) - round_constraints: list[tuple[list[EF], list[SparseStatement]]] = [] round_folding: list[list[EF]] = [] - claimed_sum_ref: list[EF] = [EF.zero()] + target = EF.zero() prev_commitment = parsed_commitment - # OODS + initial statement combine. + # Initial: combine OODS + statement, then run the first folding sumcheck. initial_constraints = prev_commitment.oods_constraints() + statement - combo = combine_constraints(state, claimed_sum_ref, initial_constraints) + target, combo = combine_constraints(state, target, initial_constraints) round_constraints.append((combo, initial_constraints)) + init_sc = verify_sumcheck(state, target, whir_folding_factor_at_round(0), 2, cfg.starting_folding_pow_bits) + round_folding.append(init_sc.point) + target = init_sc.value - # Initial sumcheck. - folding_rand = verify_sumcheck_rounds( - state, - claimed_sum_ref, - whir_folding_factor_at_round(0), - cfg.starting_folding_pow_bits, - ) - round_folding.append(folding_rand) - - # Round loop. + # Per-round loop: new commitment → STIR → combine → sumcheck. for r in range(n_rounds): rp = cfg.rounds[r] - # New num_variables = num_variables_at_round(after this round's first absorb) - # In Rust: round_state.num_variables = num_variables - folding_factor.total_number(r+1) - nvars_round = cfg.num_variables - sum( - whir_folding_factor_at_round(i) for i in range(r + 1) - ) + nvars_round = cfg.num_variables - sum(whir_folding_factor_at_round(i) for i in range(r + 1)) new_commitment = parsed_commitment_parse(state, nvars_round, rp.ood_samples) - stir_constraints = verify_stir_challenges( - state, - cfg, + state, cfg, round_index=r, num_variables=nvars_round, log_inv_rate=whir_log_inv_rate_at(cfg.log_inv_rate, r), @@ -903,35 +926,25 @@ def whir_verify( folding_randomness=round_folding[-1], ) constraints_r = new_commitment.oods_constraints() + stir_constraints - combo_r = combine_constraints(state, claimed_sum_ref, constraints_r) + target, combo_r = combine_constraints(state, target, constraints_r) round_constraints.append((combo_r, constraints_r)) - - folding_rand_r = verify_sumcheck_rounds( - state, - claimed_sum_ref, - whir_folding_factor_at_round(r + 1), - rp.folding_pow_bits, - ) - round_folding.append(folding_rand_r) + sc = verify_sumcheck(state, target, whir_folding_factor_at_round(r + 1), 2, rp.folding_pow_bits) + round_folding.append(sc.point) + target = sc.value prev_commitment = new_commitment # Final round: read the final polynomial in coefficient form, then run a # last batch of STIR queries against the last commitment. - n_vars_final = cfg.num_variables - sum( - whir_folding_factor_at_round(i) for i in range(n_rounds + 1) - ) + n_vars_final = cfg.num_variables - sum(whir_folding_factor_at_round(i) for i in range(n_rounds + 1)) final_coeffs = state.next_extension_scalars_vec(1 << n_vars_final) final_domain_size = whir_domain_size_at(cfg.num_variables, cfg.log_inv_rate, n_rounds) final_folding_factor = whir_folding_factor_at_round(n_rounds) - folded_domain_size_final = final_domain_size >> final_folding_factor folded_gen_final = two_adic_generator(final_domain_size.bit_length() - 1 - final_folding_factor) - log_height_final = folded_domain_size_final.bit_length() - 1 + log_height_final = (final_domain_size >> final_folding_factor).bit_length() - 1 state.check_pow_grinding(cfg.final_query_pow_bits) - indices_final = state.sample_in_range(log_height_final, cfg.final_queries) - final_stir: list[SparseStatement] = [] - for idx in indices_final: + for idx in state.sample_in_range(log_height_final, cfg.final_queries): op = state.next_merkle_opening() if not merkle_verify_path(prev_commitment.root, log_height_final, idx, op.leaf_data, op.path): raise ProofError("Final Merkle verification failed") @@ -941,27 +954,19 @@ def whir_verify( answers = [EF(op.leaf_data[i : i + EF.DIMENSION]) for i in range(0, len(op.leaf_data), EF.DIMENSION)] fold = eval_multilinear_evals(answers, round_folding[-1]) ef_pt = EF.from_base(Fp(pow(int(folded_gen_final.value), idx, P))) - final_stir.append(SparseStatement.dense(expand_from_univariate(ef_pt, n_vars_final), fold)) - - # Verify STIR constraints directly on final polynomial coefficients. - for c in final_stir: - if not verify_constraint_coeffs(c, final_coeffs): + smt = SparseStatement.dense(expand_from_univariate(ef_pt, n_vars_final), fold) + if not verify_constraint_coeffs(smt, final_coeffs): raise ProofError("Final STIR constraint mismatch") - # Final sumcheck. - final_sumcheck_rand = verify_sumcheck_rounds( - state, claimed_sum_ref, final_sumcheck_rounds, 0 - ) - round_folding.append(final_sumcheck_rand) + # Final sumcheck — closes the protocol against the constraint-weights MLE. + final_sc = verify_sumcheck(state, target, final_sumcheck_rounds, 2) + round_folding.append(final_sc.point) + target = final_sc.value - # Flatten all folding randomness; evaluate the constraint weights polynomial. folding_randomness_flat = [r for chunk in round_folding for r in chunk] eval_weights = eval_constraints_poly(round_constraints, folding_randomness_flat) - - # Final coeffs are evaluated at REVERSED final_sumcheck_rand. - reversed_point = list(reversed(final_sumcheck_rand)) - final_value = eval_multilinear_coeffs(final_coeffs, reversed_point) - if claimed_sum_ref[0] != eval_weights * final_value: + final_value = eval_multilinear_coeffs(final_coeffs, list(reversed(final_sc.point))) + if target != eval_weights * final_value: raise ProofError("WHIR final sumcheck check failed") return folding_randomness_flat @@ -971,22 +976,15 @@ def whir_verify( # ─── Table metadata (mirror of lean_vm::tables::table_trait) ────────────────────────────────────────────────────────── -@dataclass(frozen=True) -class Lookup: - """A single memory lookup descriptor (`LookupIntoMemory` in Rust).""" - - index: int - values: tuple[int, ...] - - @dataclass(frozen=True) class TableMeta: - """The bits of `lean_vm::Table` the verifier actually consumes.""" + """The bits of `lean_vm::Table` the verifier consumes. Each lookup is a + `(index_column, value_columns)` pair (mirrors `LookupIntoMemory`).""" name: str n_columns: int bus_direction: str # "Pull" or "Push" - lookups: tuple[Lookup, ...] + lookups: tuple[tuple[int, tuple[int, ...]], ...] def tables_from_json(obj: list[dict]) -> list[TableMeta]: @@ -995,10 +993,7 @@ def tables_from_json(obj: list[dict]) -> list[TableMeta]: name=t["name"], n_columns=int(t["n_columns"]), bus_direction=t["bus"]["direction"], - lookups=tuple( - Lookup(index=int(l["index"]), values=tuple(int(v) for v in l["values"])) - for l in t["lookups"] - ), + lookups=tuple((int(l["index"]), tuple(int(v) for v in l["values"])) for l in t["lookups"]), ) for t in obj ] @@ -1045,56 +1040,26 @@ def stacked_pcs_global_statements( tables_sorted = sort_tables_by_height(table_log_heights) out = list(previous_statements) - offset = 2 << memory_n_vars # memory + memory_acc - max_table_n_vars = tables_sorted[0][1] - offset += 1 << max(bytecode_n_vars, max_table_n_vars) # bytecode acc - + offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, tables_sorted[0][1])) col_pc = constants["col_pc"] - starting_pc = constants["starting_pc"] - ending_pc = constants["ending_pc"] + + def values_at(d: dict[int, EF], n_vars: int) -> list[SparseValue]: + # Rust uses BTreeMap → ascending-key iteration; Python dicts are + # insertion-ordered, so we sort explicitly here. + return [SparseValue((offset >> n_vars) + i, v) for i, v in sorted(d.items())] for name, n_vars in tables_sorted: if name == "execution": - out.append( - SparseStatement.unique_value( - stacked_n_vars, - offset + (col_pc << n_vars), - EF.from_base(Fp(starting_pc)), - ) - ) - out.append( - SparseStatement.unique_value( - stacked_n_vars, - offset + ((col_pc + 1) << n_vars) - 1, - EF.from_base(Fp(ending_pc)), - ) - ) + # PC column: first row pinned to `starting_pc`, last row to `ending_pc`. + for idx_in_col, pc_value in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: + out.append(SparseStatement.unique_value( + stacked_n_vars, offset + (col_pc << n_vars) + idx_in_col, EF.from_base(Fp(pc_value)), + )) for (point, eq_values, next_values) in committed_statements[name]: - # Rust uses BTreeMap → ascending-key iteration. Python dicts are - # insertion-ordered, so we have to sort explicitly here. if next_values: - out.append( - SparseStatement.new_next( - stacked_n_vars, - list(point), - [ - SparseValue((offset >> n_vars) + col_index, value) - for col_index, value in sorted(next_values.items()) - ], - ) - ) - out.append( - SparseStatement( - total_num_variables=stacked_n_vars, - point=list(point), - values=[ - SparseValue((offset >> n_vars) + col_index, value) - for col_index, value in sorted(eq_values.items()) - ], - is_next=False, - ) - ) + out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values, n_vars))) + out.append(SparseStatement(stacked_n_vars, list(point), values_at(eq_values, n_vars))) offset += tables[name].n_columns << n_vars @@ -1132,109 +1097,45 @@ def stacked_pcs_parse_commitment( -# ─── Generic sumcheck verifier (port of `backend/sumcheck/src/verify.rs`) ────────────────────────────────────────────────────────── - - -@dataclass -class Evaluation: - """Pair (point, value): claim that a multilinear evaluates to `value` at - `point`. Mirrors `poly::Evaluation`. - """ - - point: list[EF] - value: EF - - -def sumcheck_verify( - state: VerifierState, - n_vars: int, - degree: int, - expected_sum: EF, -) -> Evaluation: - """Reads `n_vars` round polynomials in standard univariate basis, each with - `degree + 1` coefficients. Checks `h(0) + h(1) = target` each round, then - folds in the challenge. Returns the final point + claimed value.""" - target = expected_sum - challenges: list[EF] = [] - for _ in range(n_vars): - coeffs = state.next_extension_scalars_vec(degree + 1) - _check_sumcheck_identity(coeffs, target) - r = state.sample() - challenges.append(r) - target = _eval_univariate(coeffs, r) - return Evaluation(point=challenges, value=target) - - -# --------------------------------------------------------------------------- -# GKR-quotient verifier (port of `sub_protocols::quotient_gkr`) -# -# Verifies the protocol `Σ nᵢ/dᵢ` via a layered sumcheck. -# --------------------------------------------------------------------------- +# ─── GKR-quotient verifier (port of `sub_protocols::quotient_gkr`) ────────────────────────────────────────────────────────── +# Verifies `Σ nᵢ/dᵢ` via a layered sumcheck. N_VARS_TO_SEND_GKR_COEFFS = 5 -def verify_gkr_quotient( - state: VerifierState, - n_vars: int, -) -> tuple[EF, list[EF], EF, EF]: - """`(quotient, gkr_point, claims_num, claims_den)`. - """ +def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF], EF, EF]: + """Returns `(quotient, point, claim_num, claim_den)`. Reads the top-level + coefficients, then collapses one variable per layer with a degree-3 + sumcheck on `n·d_r + n_r·d` plus the next (num, den) random combination.""" assert n_vars > N_VARS_TO_SEND_GKR_COEFFS - send_len = 1 << N_VARS_TO_SEND_GKR_COEFFS - - last_nums = state.next_extension_scalars_vec(send_len) - last_dens = state.next_extension_scalars_vec(send_len) - quotient = _quotient_sum(last_nums, last_dens) - - point: list[EF] = state.sample_vec(N_VARS_TO_SEND_GKR_COEFFS) - claims_num = eval_multilinear_evals(last_nums, point) - claims_den = eval_multilinear_evals(last_dens, point) - - for i in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): - point, claims_num, claims_den = _verify_gkr_quotient_step( - state, i, point, claims_num, claims_den - ) - return quotient, point, claims_num, claims_den + nums = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) + dens = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) + quotient = EF.zero() + for n, d in zip(nums, dens): + quotient = quotient + n * d.inv() -def _verify_gkr_quotient_step( - state: VerifierState, - n_vars: int, - point: list[EF], - claims_num: EF, - claims_den: EF, -) -> tuple[list[EF], EF, EF]: - alpha = state.sample() - expected_sum = claims_num + alpha * claims_den - postponed = sumcheck_verify(state, n_vars, 3, expected_sum) - # Rust: postponed.point.0.reverse(); - postponed_point = list(reversed(postponed.point)) - inner_evals = state.next_extension_scalars_vec(4) - - # constraints_eval = α · n_r · d_r + (n_l · d_r + n_r · d_l) - constraints_eval = ( - alpha * inner_evals[2] * inner_evals[3] - + (inner_evals[0] * inner_evals[3] + inner_evals[1] * inner_evals[2]) - ) - - if postponed.value != eq_poly_outside(point, postponed_point) * constraints_eval: - raise ProofError("GKR step: postponed value mismatch") - - beta = state.sample() - one_minus_beta = EF.one() - beta - next_num = one_minus_beta * inner_evals[0] + beta * inner_evals[1] - next_den = one_minus_beta * inner_evals[2] + beta * inner_evals[3] - next_point = postponed_point + [beta] - return next_point, next_num, next_den + point = state.sample_vec(N_VARS_TO_SEND_GKR_COEFFS) + claim_num = eval_multilinear_evals(nums, point) + claim_den = eval_multilinear_evals(dens, point) + for layer_n_vars in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): + alpha = state.sample() + sc = verify_sumcheck(state, claim_num + alpha * claim_den, layer_n_vars, 3) + sc_point = list(reversed(sc.point)) + # Inner evaluations: (n_left, n_right, d_left, d_right) at sc.point. + nl, nr, dl, dr = state.next_extension_scalars_vec(4) + # Sumcheck identity: eq(point, sc_point) · (α·dl·dr + (nl·dr + nr·dl)). + if sc.value != eq_poly_outside(point, sc_point) * (alpha * dl * dr + nl * dr + nr * dl): + raise ProofError("GKR step: postponed value mismatch") + beta = state.sample() + one_minus = EF.one() - beta + claim_num = one_minus * nl + beta * nr + claim_den = one_minus * dl + beta * dr + point = sc_point + [beta] -def _quotient_sum(nums: Sequence[EF], dens: Sequence[EF]) -> EF: - acc = EF.zero() - for n, d in zip(nums, dens): - acc = acc + n * d.inv() - return acc + return quotient, point, claim_num, claim_den @@ -1336,35 +1237,13 @@ class GenericLogupStatements: bytecode_evaluation: Evaluation -def _compute_total_active_len_logup( - log_memory: int, - log_bytecode: int, - tables_sorted: list[tuple[str, int]], - table_lookups: dict[str, list[Lookup]], - execution_name: str, -) -> int: - max_table_height = 1 << tables_sorted[0][1] - log_n_cycles = next(h for n, h in tables_sorted if n == execution_name) - - def offset_for_table(name: str, log_n_rows: int) -> int: - num_cols = sum(len(l.values) for l in table_lookups[name]) + 1 # +1 for the bus - return num_cols << log_n_rows - - return ( - (1 << log_memory) - + max(1 << log_bytecode, max_table_height) - + (1 << log_n_cycles) - + sum(offset_for_table(name, h) for name, h in tables_sorted) - ) - - def verify_generic_logup( state: VerifierState, c: EF, alphas: list[EF], alphas_eq_poly: list[EF], log_memory: int, - bytecode_multilinear: list[Fp], + bytecode_multilinear: list[int], table_log_heights: dict[str, int], tables: dict[str, TableMeta], constants: dict, @@ -1388,9 +1267,16 @@ def verify_generic_logup( n_instr_padded = 1 << log2_ceil_usize(n_instr_cols) # next power of 2 log_bytecode = log2_strict_usize(len(bytecode_multilinear) // n_instr_padded) - table_lookups = {name: list(tables[name].lookups) for name in table_log_heights} - total_active_len = _compute_total_active_len_logup( - log_memory, log_bytecode, tables_sorted, table_lookups, execution_name + # Total active length = memory + bytecode + execution + per-table footprints, + # where each footprint is (sum of lookup arities + 1 bus column) × 2^log_n_rows. + max_table_height = 1 << tables_sorted[0][1] + log_n_cycles = next(h for n, h in tables_sorted if n == execution_name) + table_cols = lambda n: sum(len(vs) for _, vs in tables[n].lookups) + 1 + total_active_len = ( + (1 << log_memory) + + max(1 << log_bytecode, max_table_height) + + (1 << log_n_cycles) + + sum((table_cols(n) << h) for n, h in tables_sorted) ) total_gkr_n_vars = log2_ceil_usize(total_active_len) @@ -1435,9 +1321,7 @@ def pref_at(offset: int, log_height: int) -> EF: bytecode_index_value = mle_of_01234567_etc(bytecode_and_acc_point) log_instr = log2_ceil_usize(n_instr_cols) bytecode_point = list(bytecode_and_acc_point) + list(from_end(alphas, log_instr)) - bytecode_value = eval_multilinear_evals( - [EF.from_base(x) for x in bytecode_multilinear], bytecode_point - ) + bytecode_value = eval_mle_base_at_ef(bytecode_multilinear, bytecode_point) # Correction: `(1 - alpha[0]) * (1 - alpha[1]) * ... * (1 - alpha[k-1])` # over the alphas BEFORE the last `log_instr` (= the bus-data slot bits). correction = EF.one() @@ -1502,12 +1386,12 @@ def pref_at(offset: int, log_height: int) -> EF: offset += 1 << log_n_rows # II] Lookups into memory - for lookup in meta.lookups: + for index_col, value_cols in meta.lookups: index_eval = state.next_extension_scalar() - assert lookup.index not in table_values - table_values[lookup.index] = index_eval + assert index_col not in table_values + table_values[index_col] = index_eval - for i, col_index in enumerate(lookup.values): + for i, col_index in enumerate(value_cols): value_eval = state.next_extension_scalar() assert col_index not in table_values table_values[col_index] = value_eval @@ -1549,58 +1433,6 @@ def pref_at(offset: int, log_height: int) -> EF: ) -# ─── AIR sumcheck helpers (port of sub_protocols/air_sumcheck.rs) ────────────────────────────────────────────────────────── - - -def natural_ordering_point_for_session( - sumcheck_air_point: Sequence[EF], log_n_rows: int -) -> list[EF]: - """Takes the last `log_n_rows` coordinates of the AIR sumcheck point and - reverses them. - """ - if log_n_rows == 0: - return [] - return list(reversed(sumcheck_air_point[-log_n_rows:])) - - -def back_loaded_table_contribution( - bus_point: Sequence[EF], - sumcheck_air_point: Sequence[EF], - natural_ordering_point: Sequence[EF], - constraint_eval: EF, - eta_power: EF, -) -> EF: - """eta^t · (Π i∈[0, n_max - n_t) sumcheck_point[i]) · eq(bus_point, natural_point) · constraint_eval - """ - n_t = len(bus_point) - n_max = len(sumcheck_air_point) - suffix_start = n_max - n_t - assert len(natural_ordering_point) == n_t - eq_val = eq_poly_outside(bus_point, natural_ordering_point) - k_t = EF.one() - for x in sumcheck_air_point[:suffix_start]: - k_t = k_t * x - return eta_power * k_t * eq_val * constraint_eval - - -def columns_evals_up_and_down( - n_columns: int, - down_column_indexes: Sequence[int], - col_evals: Sequence[EF], - natural_ordering_point: Sequence[EF], -) -> tuple[list[EF], dict[int, EF], dict[int, EF]]: - """Mirror of `air_sumcheck::columns_evals_up_and_down`. - - Splits `col_evals` into the per-column evaluation map plus the "next" - (= `down`) columns. Returns `(point, eq_values, next_values)`. - """ - n_up = n_columns - assert len(col_evals) == n_up + len(down_column_indexes) - eq_values = {i: col_evals[i] for i in range(n_up)} - next_values = { - idx: col_evals[n_up + j] for j, idx in enumerate(down_column_indexes) - } - return list(natural_ordering_point), eq_values, next_values @@ -1608,30 +1440,23 @@ def columns_evals_up_and_down( class ConstraintFolder: - """Each `assert_zero(x)` (or `assert_zero_ef`) contributes - `alpha_powers[constraint_index] · x` to the accumulator. - """ + """Each `assert_zero(x)` contributes `alpha_powers[i] · x` to the + running accumulator. `assert_eq` and `assert_bool` are sugar.""" def __init__(self, up: Sequence[EF], down: Sequence[EF], alpha_powers: Sequence[EF]) -> None: - self.up = list(up) - self.down = list(down) - self.alpha_powers = list(alpha_powers) + self.up, self.down, self.alpha_powers = list(up), list(down), list(alpha_powers) self.accumulator: EF = EF.zero() - self.constraint_index = 0 + self.i = 0 def assert_zero(self, x: EF) -> None: - a = self.alpha_powers[self.constraint_index] - self.accumulator = self.accumulator + a * x - self.constraint_index += 1 - - # `assert_zero_ef` is identical in EF. - assert_zero_ef = assert_zero + self.accumulator = self.accumulator + self.alpha_powers[self.i] * x + self.i += 1 def assert_eq(self, x: EF, y: EF) -> None: self.assert_zero(x - y) def assert_bool(self, x: EF) -> None: - # bool_check(x) = x * (1 - x). Zero iff x is 0 or 1. + # bool_check(x) = x · (1 − x), zero iff x ∈ {0, 1}. self.assert_zero(x * (EF.one() - x)) @@ -1671,47 +1496,16 @@ def air_constraint_eval( # ─── Execution-table AIR (lean_vm/src/tables/execution/air.rs) ────────────────────────────────────────────────────────── -# Column indexes for the execution table (mirrors execution/air.rs). -_EXEC = { - "PC": 0, "FP": 1, - "MEM_ADDRESS_A": 2, "MEM_ADDRESS_B": 3, "MEM_ADDRESS_C": 4, - "MEM_VALUE_A": 5, "MEM_VALUE_B": 6, "MEM_VALUE_C": 7, - "OPERAND_A": 8, "OPERAND_B": 9, "OPERAND_C": 10, - "FLAG_A": 11, "FLAG_B": 12, "FLAG_C": 13, "FLAG_C_FP": 14, - "FLAG_AB_FP": 15, "MUL": 16, "JUMP": 17, "AUX": 18, "PRECOMPILE_DATA": 19, -} - - def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: - up = folder.up - down = folder.down + # Column layout (execution/air.rs): pc, fp, addr_{a,b,c}, value_{a,b,c}, + # operand_{a,b,c}, flag_{a,b,c}, flag_c_fp, flag_ab_fp, mul, jump, aux, + # precompile_data. `down[0..2]` is the next row's (pc, fp). + (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, + operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, + flag_ab_fp, mul, jump, aux, precompile_data) = folder.up[:20] + next_pc, next_fp = folder.down[0], folder.down[1] one = EF.one() - next_pc = down[0] - next_fp = down[1] - - operand_a = up[_EXEC["OPERAND_A"]] - operand_b = up[_EXEC["OPERAND_B"]] - operand_c = up[_EXEC["OPERAND_C"]] - flag_a = up[_EXEC["FLAG_A"]] - flag_b = up[_EXEC["FLAG_B"]] - flag_c = up[_EXEC["FLAG_C"]] - flag_c_fp = up[_EXEC["FLAG_C_FP"]] - flag_ab_fp = up[_EXEC["FLAG_AB_FP"]] - mul = up[_EXEC["MUL"]] - jump = up[_EXEC["JUMP"]] - aux = up[_EXEC["AUX"]] - precompile_data = up[_EXEC["PRECOMPILE_DATA"]] - - value_a = up[_EXEC["MEM_VALUE_A"]] - value_b = up[_EXEC["MEM_VALUE_B"]] - value_c = up[_EXEC["MEM_VALUE_C"]] - pc = up[_EXEC["PC"]] - fp = up[_EXEC["FP"]] - addr_a = up[_EXEC["MEM_ADDRESS_A"]] - addr_b = up[_EXEC["MEM_ADDRESS_B"]] - addr_c = up[_EXEC["MEM_ADDRESS_C"]] - one_minus_flag_a_and_flag_ab_fp = -(flag_a + flag_ab_fp - one) one_minus_flag_b_and_flag_ab_fp = -(flag_b + flag_ab_fp - one) one_minus_flag_c_and_flag_c_fp = -(flag_c + flag_c_fp - one) @@ -1743,7 +1537,7 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: is_precompile = -(add + mul + deref + jump - one) # Constraint 1: bus column (assert_zero_ef) - folder.assert_zero_ef( + folder.assert_zero( _eval_virtual_bus_column( extra_data, is_precompile, [precompile_data, nu_a, nu_b, nu_c] ) @@ -1776,12 +1570,7 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: # ─── Extension-op-table AIR (lean_vm/src/tables/extension_op/air.rs) ────────────────────────────────────────────────────────── -_EXT_OP_COL = { - "IS_BE": 0, "START": 1, "FLAG_ADD": 2, "FLAG_MUL": 3, "FLAG_POLY_EQ": 4, - "LEN": 5, "IDX_A": 6, "IDX_B": 7, "IDX_RES": 8, - "VA": 9, "VB": 14, "VRES": 19, "COMP": 24, -} - +# Bus-data magic numbers from extension_op/air.rs (precompile-data layout). _EXT_OP_FLAG_IS_BE = 4 _EXT_OP_FLAG_ADD = 8 _EXT_OP_FLAG_MUL = 16 @@ -1816,35 +1605,16 @@ def dot(av: Sequence[EF], bv: Sequence[EF]) -> EF: def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: - up = folder.up - down = folder.down + # `up` layout (extension_op/air.rs): is_be, start, flag_{add,mul,poly_eq}, + # len, idx_{a,b,res}, then four 5-element EF blocks va, vb, vres, comp. + is_be, start, flag_add, flag_mul, flag_poly_eq, len_col, idx_a, idx_b, idx_res = folder.up[:9] + va, vb, vres, comp = (folder.up[9 + 5 * k : 9 + 5 * (k + 1)] for k in range(4)) + # `down` layout: start, is_be, len, flag_{add,mul,poly_eq}, idx_{a,b}, comp[0..5]. + (start_down, is_be_down, len_down, flag_add_down, flag_mul_down, + flag_poly_eq_down, idx_a_down, idx_b_down) = folder.down[:8] + comp_down = folder.down[8:13] one = EF.one() - is_be = up[_EXT_OP_COL["IS_BE"]] - start = up[_EXT_OP_COL["START"]] - flag_add = up[_EXT_OP_COL["FLAG_ADD"]] - flag_mul = up[_EXT_OP_COL["FLAG_MUL"]] - flag_poly_eq = up[_EXT_OP_COL["FLAG_POLY_EQ"]] - len_col = up[_EXT_OP_COL["LEN"]] - idx_a = up[_EXT_OP_COL["IDX_A"]] - idx_b = up[_EXT_OP_COL["IDX_B"]] - idx_res = up[_EXT_OP_COL["IDX_RES"]] - - va = [up[_EXT_OP_COL["VA"] + k] for k in range(5)] - vb = [up[_EXT_OP_COL["VB"] + k] for k in range(5)] - vres = [up[_EXT_OP_COL["VRES"] + k] for k in range(5)] - comp = [up[_EXT_OP_COL["COMP"] + k] for k in range(5)] - - start_down = down[0] - is_be_down = down[1] - len_down = down[2] - flag_add_down = down[3] - flag_mul_down = down[4] - flag_poly_eq_down = down[5] - idx_a_down = down[6] - idx_b_down = down[7] - comp_down = [down[8 + k] for k in range(5)] - active = flag_add + flag_mul + flag_poly_eq activation_flag = start * active @@ -1857,7 +1627,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat ) # Constraint 1: bus - folder.assert_zero_ef( + folder.assert_zero( _eval_virtual_bus_column(extra_data, activation_flag, [aux, idx_a, idx_b, idx_res]) ) @@ -2128,7 +1898,7 @@ def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: ) # Constraint 1: bus - folder.assert_zero_ef( + folder.assert_zero( _eval_virtual_bus_column( extra_data, flag_active, [precompile_data_reconstructed, index_a, index_b, index_res] ) @@ -2165,15 +1935,6 @@ def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: # ─── AIR-stage orchestration in verify_execution ────────────────────────────────────────────────────────── -@dataclass -class AirStageResult: - """Outputs of the AIR sumcheck stage, fed into the WHIR finale.""" - - committed_statements: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]] - public_memory_random_point: list[EF] - public_memory_eval: EF - - # Per-table compile-time spec (Rust: `
::{degree_air, n_constraints, # down_column_indexes}`). The down-column lists for `execution` and # `extension_op` are exactly what their `Air::down_column_indexes` returns. @@ -2193,9 +1954,11 @@ def verify_air_stage( tables: dict[str, TableMeta], public_input: Sequence[Fp], log_memory: int, -) -> AirStageResult: - """Returns the per-table committed statements (point + eq_values + next_values) - and the public-memory random point + its evaluation. +) -> tuple[dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]], list[EF], EF]: + """Returns `(committed_statements, public_memory_random_point, public_memory_eval)`. + + `committed_statements[name]` is a list of (point, eq_values, next_values) + triples — one per session for that table. """ bus_beta = state.sample() air_alpha = state.sample() @@ -2231,7 +1994,7 @@ def verify_air_stage( max_full_degree = max(_TABLE_SPECS[name]["degree"] + 1 for name, _ in tables_sorted) n_max = tables_sorted[0][1] - sumcheck_result = sumcheck_verify(state, n_max, max_full_degree, initial_sum) + sumcheck_result = verify_sumcheck(state, initial_sum, n_max, max_full_degree) sumcheck_air_point = sumcheck_result.point claimed_air_final_value = sumcheck_result.value @@ -2258,16 +2021,20 @@ def verify_air_stage( col_evals = state.next_extension_scalars_vec(meta.n_columns + len(down_indexes)) constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) + # Per-table contribution = η^t · (Π unused-prefix coords) · eq · C(col_evals). bus_point = from_end(logup.gkr_point, log_n_rows) - natural_pt = natural_ordering_point_for_session(sumcheck_air_point, log_n_rows) - my_air_final_value = my_air_final_value + back_loaded_table_contribution( - bus_point, sumcheck_air_point, natural_pt, constraint_eval, eta_pow + natural_pt = list(reversed(sumcheck_air_point[-log_n_rows:])) if log_n_rows else [] + k_t = EF.one() + for x in sumcheck_air_point[: n_max - log_n_rows]: + k_t = k_t * x + my_air_final_value = my_air_final_value + ( + eta_pow * k_t * eq_poly_outside(bus_point, natural_pt) * constraint_eval ) - point, eq_values, next_values = columns_evals_up_and_down( - meta.n_columns, down_indexes, col_evals, natural_pt - ) - committed[name].append((point, eq_values, next_values)) + # Split col_evals into the per-column eq-values + the next-row evals. + eq_values = {i: col_evals[i] for i in range(meta.n_columns)} + next_values = {idx: col_evals[meta.n_columns + j] for j, idx in enumerate(down_indexes)} + committed[name].append((natural_pt, eq_values, next_values)) if my_air_final_value != claimed_air_final_value: raise ProofError("AIR sumcheck: my_air_final_value != claimed_air_final_value") @@ -2280,36 +2047,21 @@ def verify_air_stage( [EF.from_base(f) for f in public_memory], public_memory_random_point ) - return AirStageResult( - committed_statements=committed, - public_memory_random_point=list(public_memory_random_point), - public_memory_eval=public_memory_eval, - ) + return committed, list(public_memory_random_point), public_memory_eval # ─── Top-level verifier ────────────────────────────────────────────────────────── -@dataclass -class VerifyResult: - """High-level outputs of `verify_execution` (mostly for diagnostics).""" - - log_inv_rate: int - log_memory: int - stacked_n_vars: int - bytecode_evaluation_point: list[EF] - bytecode_evaluation_value: EF - - def verify_execution( bytecode: Bytecode, public_input: Sequence[Fp], proof: Proof, tables: Sequence[TableMeta], constants: dict, - bytecode_multilinear: list[Fp], -) -> VerifyResult: + bytecode_multilinear: list[int], +) -> dict: """Verify a leanVM execution proof. Port of `verify_execution.rs`. The flow is: @@ -2387,69 +2139,41 @@ def verify_execution( constants, ) - air_stage = verify_air_stage( - state, - logup_statements, - logup_c, - logup_alphas_eq_poly, - table_log_heights, - tables_by_name, - public_input, - log_memory, + committed, pm_point, pm_eval = verify_air_stage( + state, logup_statements, logup_c, logup_alphas_eq_poly, + table_log_heights, tables_by_name, public_input, log_memory, ) - # Build the global WHIR statement list and run the final WHIR verify. + # Build the global WHIR statement list. Three "previous" statements seed it + # (memory + memory_acc, public memory, bytecode acc), then `stacked_pcs_…` + # appends the per-table committed claims. stacked_n_vars = parsed_commitment.num_variables + mk = lambda point, values: SparseStatement(stacked_n_vars, list(point), values) previous_statements = [ - SparseStatement( - total_num_variables=stacked_n_vars, - point=list(logup_statements.memory_and_acc_point), - values=[ - SparseValue(0, logup_statements.value_memory), - SparseValue(1, logup_statements.value_memory_acc), - ], - is_next=False, - ), - SparseStatement( - total_num_variables=stacked_n_vars, - point=list(air_stage.public_memory_random_point), - values=[SparseValue(0, air_stage.public_memory_eval)], - is_next=False, - ), - SparseStatement( - total_num_variables=stacked_n_vars, - point=list(logup_statements.bytecode_and_acc_point), - values=[ - SparseValue( - (2 << log_memory) >> bytecode.log_size, - logup_statements.value_bytecode_acc, - ), - ], - is_next=False, - ), + mk(logup_statements.memory_and_acc_point, [ + SparseValue(0, logup_statements.value_memory), + SparseValue(1, logup_statements.value_memory_acc), + ]), + mk(pm_point, [SparseValue(0, pm_eval)]), + mk(logup_statements.bytecode_and_acc_point, [SparseValue( + (2 << log_memory) >> bytecode.log_size, + logup_statements.value_bytecode_acc, + )]), ] global_statements = stacked_pcs_global_statements( - stacked_n_vars, - log_memory, - bytecode.log_size, - previous_statements, - table_log_heights, - air_stage.committed_statements, - tables_by_name, - constants, + stacked_n_vars, log_memory, bytecode.log_size, previous_statements, + table_log_heights, committed, tables_by_name, constants, ) whir_cfg = whir_config(log_inv_rate, stacked_n_vars) whir_verify(state, whir_cfg, parsed_commitment, global_statements) - return VerifyResult( - log_inv_rate=log_inv_rate, - log_memory=log_memory, - stacked_n_vars=stacked_n_vars, - bytecode_evaluation_point=list(logup_statements.bytecode_evaluation.point), - bytecode_evaluation_value=logup_statements.bytecode_evaluation.value, - ) + return { + "log_inv_rate": log_inv_rate, + "log_memory": log_memory, + "stacked_n_vars": stacked_n_vars, + } @@ -2497,7 +2221,7 @@ def main() -> int: mle_blob = (vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes() arr = array.array("I"); arr.frombytes(mle_blob) assert len(arr) == raw["bytecode_multilinear_len"] - bytecode_multilinear = [Fp(v) for v in arr] + bytecode_multilinear: list[int] = list(arr) bytecode = Bytecode([Fp(v) for v in raw["bytecode_hash"]], raw["bytecode_log_size"]) public_input = [Fp(v) for v in raw["public_input"]] @@ -2526,11 +2250,7 @@ def main() -> int: print(f"FAIL: {e}") return 1 - print( - f"OK: rec-aggregation proof verified " - f"(log_inv_rate={result.log_inv_rate}, log_memory={result.log_memory}, " - f"stacked_n_vars={result.stacked_n_vars})" - ) + print(f"OK: rec-aggregation proof verified ({result})") return 0 From 35adcfc941b46bc12c56c2a8ada2ebf03f8e9bd1 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 18 May 2026 09:46:53 +0200 Subject: [PATCH 10/69] wip --- crates/lean_prover/verifier.py | 1623 +++++++++++--------------------- 1 file changed, 566 insertions(+), 1057 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 9d8837253..6f198a042 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -66,7 +66,7 @@ -# ─── Error type ────────────────────────────────────────────────────────── +# ─── Error type + quintic extension field ────────────────────────────────────────────────────────── class ProofError(Exception): @@ -80,11 +80,8 @@ class ProofError(Exception): class EF: - """Element of the degree-5 extension of KoalaBear. - - Stored as 5 base coefficients `c[0..5]` representing `c0 + c1 X + ... + c4 X^4`. - Irreducible polynomial: X^5 + X^2 - 1, i.e. X^5 ≡ 1 - X^2. - """ + """Quintic extension `Fp[X] / (X⁵ + X² − 1)`. Stored as 5 base coefficients; + multiplication reduces with `X⁵ ≡ 1 − X²`.""" __slots__ = ("c",) DIMENSION = 5 @@ -93,99 +90,46 @@ def __init__(self, coeffs: Sequence[Fp]): assert len(coeffs) == 5 self.c = tuple(coeffs) - # --- constructors ----------------------------------------------------- - - @staticmethod - def zero() -> "EF": - return EF([Fp(0)] * 5) - @staticmethod - def one() -> "EF": - return EF([Fp(1), Fp(0), Fp(0), Fp(0), Fp(0)]) - + def zero() -> "EF": return EF([Fp(0)] * 5) @staticmethod - def from_base(x: Fp) -> "EF": - return EF([x, Fp(0), Fp(0), Fp(0), Fp(0)]) - + def one() -> "EF": return EF([Fp(1)] + [Fp(0)] * 4) @staticmethod - def from_basis_coefficients(coeffs: Sequence[Fp]) -> "EF": - return EF(coeffs) - - # --- arithmetic ------------------------------------------------------- - - def __add__(self, other) -> "EF": - if isinstance(other, Fp): - return EF([self.c[0] + other, *self.c[1:]]) - return EF([a + b for a, b in zip(self.c, other.c)]) - + def from_base(x: Fp) -> "EF": return EF([x] + [Fp(0)] * 4) + + def __add__(self, o): + if isinstance(o, Fp): return EF([self.c[0] + o, *self.c[1:]]) + return EF([a + b for a, b in zip(self.c, o.c)]) + def __sub__(self, o): + if isinstance(o, Fp): return EF([self.c[0] - o, *self.c[1:]]) + return EF([a - b for a, b in zip(self.c, o.c)]) + def __neg__(self): return EF([-a for a in self.c]) __radd__ = __add__ - def __sub__(self, other) -> "EF": - if isinstance(other, Fp): - return EF([self.c[0] - other, *self.c[1:]]) - return EF([a - b for a, b in zip(self.c, other.c)]) - - def __rsub__(self, other) -> "EF": - # other - self, where other is Fp - return EF([other - self.c[0], *[-c for c in self.c[1:]]]) - - def __neg__(self) -> "EF": - return EF([-a for a in self.c]) - - def __mul__(self, other: "EF | Fp | int") -> "EF": - if isinstance(other, Fp): - return EF([a * other for a in self.c]) - if isinstance(other, int): - f = Fp(other % P) - return EF([a * f for a in self.c]) - # Schoolbook poly mul mod (X^5 + X^2 - 1). - a, b = self.c, other.c - prod = [Fp(0)] * 9 # degree up to 8 + def __mul__(self, o): + if isinstance(o, Fp): return EF([a * o for a in self.c]) + # Schoolbook degree-8 product, reduced with X^k = X^(k-5)·(1 − X²) for k ≥ 5. + a, b = self.c, o.c + prod = [Fp(0)] * 9 for i in range(5): for j in range(5): prod[i + j] = prod[i + j] + a[i] * b[j] - # Reduce: X^5 = 1 - X^2, X^k for k >= 5 reduced repeatedly. - for k in range(8, 4, -1): # 8,7,6,5 + for k in range(8, 4, -1): coef = prod[k] - if int(coef.value) == 0: - continue - # X^k = X^(k-5) * X^5 = X^(k-5) * (1 - X^2) - prod[k] = Fp(0) prod[k - 5] = prod[k - 5] + coef prod[k - 3] = prod[k - 3] - coef return EF(prod[:5]) - __rmul__ = __mul__ - def __eq__(self, other: object) -> bool: - if not isinstance(other, EF): - return NotImplemented - return self.c == other.c - - def __hash__(self) -> int: - return hash(self.c) - - def __repr__(self) -> str: - return f"EF({[int(x.value) for x in self.c]})" - - def is_zero(self) -> bool: - return all(int(x.value) == 0 for x in self.c) + def __eq__(self, o): return isinstance(o, EF) and self.c == o.c + def __hash__(self): return hash(self.c) + def __repr__(self): return f"EF({[int(x.value) for x in self.c]})" def inv(self) -> "EF": - """Inverse via Fermat: a^(q-2) where q = P^5. Slow but simple.""" - if self.is_zero(): - raise ZeroDivisionError("EF inverse of zero") - exp = P ** 5 - 2 - return self.pow(exp) - - def pow(self, n: int) -> "EF": - if n < 0: - return self.inv().pow(-n) - result = EF.one() - base = self + """Fermat: `a^(P⁵ − 2)`.""" + result, base, n = EF.one(), self, P ** 5 - 2 while n > 0: - if n & 1: - result = result * base + if n & 1: result = result * base base = base * base n >>= 1 return result @@ -205,88 +149,59 @@ def poseidon16_compress_in_place(state: list[Fp]) -> list[Fp]: def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: - """2:1 Merkle compression: top DIGEST_ELEMS of `compress_in_place(left || right)`.""" - assert len(left) == DIGEST_ELEMS and len(right) == DIGEST_ELEMS + """2:1 Merkle compression: top `DIGEST_ELEMS` of `permute(L || R) + (L || R)`.""" return poseidon16_compress_in_place(list(left) + list(right))[:DIGEST_ELEMS] def hash_slice(data: Sequence[Fp]) -> list[Fp]: - """`symetric::hash_slice` with WIDTH=16, RATE=OUT=8 (right-to-left absorbing).""" - assert len(data) % RATE == 0 - n_chunks = len(data) // RATE - assert n_chunks >= 2 - state = list(data[len(data) - WIDTH :]) - state = poseidon16_compress_in_place(state) - for chunk_idx in range(n_chunks - 3, -1, -1): - offset = chunk_idx * RATE - state = state[:CAPACITY] + list(data[offset : offset + RATE]) - state = poseidon16_compress_in_place(state) + """`symetric::hash_slice` with `WIDTH=16, RATE=OUT=8` (right-to-left absorb).""" + assert len(data) % RATE == 0 and len(data) >= WIDTH + state = poseidon16_compress_in_place(list(data[-WIDTH:])) + for k in range(len(data) // RATE - 3, -1, -1): + state = poseidon16_compress_in_place(state[:CAPACITY] + list(data[k * RATE : (k + 1) * RATE])) return state[:DIGEST_ELEMS] class Challenger: - """Poseidon16 challenger (old "compression + domain-separator" design). + """Compression-with-domain-separator Fiat-Shamir state. - Mirrors `fiat_shamir::challenger` on this branch: - - `state` is a length-RATE buffer (8 elements). - - `observe(value)`: `state = permute(state || value)[:RATE]`. - - `sample_many(n)`: hash `(domain_sep_i || state)` for `i ∈ 0..=n`; - return the first `n`, advance `state` to the last one. - """ + `state` is a length-`RATE` buffer; `observe(chunk)` does + `state ← permute(state || chunk)[:RATE]`; sampling re-permutes the + state with a per-call domain separator `[i, 0, …, 0]`.""" def __init__(self) -> None: self.state: list[Fp] = [Fp(0)] * RATE - def observe(self, value: Sequence[Fp]) -> None: - assert len(value) == RATE - out = poseidon16_compress_in_place(list(self.state) + list(value)) - self.state = out[:RATE] - def observe_many(self, scalars: Sequence[Fp]) -> None: for i in range(0, len(scalars), RATE): chunk = list(scalars[i : i + RATE]) - if len(chunk) < RATE: - chunk = chunk + [Fp(0)] * (RATE - len(chunk)) - self.observe(chunk) - - # Alias matching `Challenger::observe_scalars` on this branch. - observe_scalars = observe_many - - def sample_many(self, n: int) -> list[list[Fp]]: - sampled: list[list[Fp]] = [] - last: list[Fp] | None = None - for i in range(n + 1): - domain_sep = [Fp(i)] + [Fp(0)] * (RATE - 1) - hashed = poseidon16_compress_in_place(domain_sep + list(self.state))[:RATE] - if i < n: - sampled.append(hashed) - else: - last = hashed - if last is not None: - self.state = last - return sampled + chunk += [Fp(0)] * (RATE - len(chunk)) + self.state = poseidon16_compress_in_place(self.state + chunk)[:RATE] - def sample_ef_vec(self, n: int) -> list[EF]: - """Mirrors utils::sample_vec — pulls ceil(n*5/8) blocks, takes first n*5.""" - n_blocks = (n * EF.DIMENSION + RATE - 1) // RATE + def _sample_blocks(self, n_blocks: int) -> list[Fp]: + """Run `n_blocks + 1` permutations with domain separators, advance the + state to the last, return the first `n_blocks * RATE` scalars flat.""" flat: list[Fp] = [] - for block in self.sample_many(n_blocks): - flat.extend(block) - flat = flat[: n * EF.DIMENSION] + for i in range(n_blocks + 1): + ds = [Fp(i)] + [Fp(0)] * (RATE - 1) + hashed = poseidon16_compress_in_place(ds + self.state)[:RATE] + if i < n_blocks: flat.extend(hashed) + else: self.state = hashed + return flat + + def sample_ef_vec(self, n: int) -> list[EF]: + flat = self._sample_blocks((n * EF.DIMENSION + RATE - 1) // RATE)[: n * EF.DIMENSION] return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] def sample_ef(self) -> EF: return self.sample_ef_vec(1)[0] def sample_in_range(self, bits: int, n_samples: int) -> list[int]: - """Mirrors challenger::sample_in_range — not perfectly uniform.""" + """Truncate the low `bits` bits of `n_samples` field samples — matches + `challenger::sample_in_range` (not perfectly uniform).""" assert bits < 31 - n_blocks = (n_samples + RATE - 1) // RATE - flat: list[Fp] = [] - for block in self.sample_many(n_blocks): - flat.extend(block) - mask = (1 << bits) - 1 - return [int(x.value) & mask for x in flat[:n_samples]] + flat = self._sample_blocks((n_samples + RATE - 1) // RATE)[:n_samples] + return [int(x.value) & ((1 << bits) - 1) for x in flat] @@ -312,37 +227,29 @@ class Proof: merkle_openings: list[MerkleOpening] -def _next_multiple_of(n: int, m: int) -> int: - return ((n + m - 1) // m) * m - - class VerifierState: """Drives the Fiat-Shamir transcript: reads scalars from `proof.transcript`, - samples challenges from the challenger, and yields restored Merkle openings. + samples challenges from the challenger, yields restored Merkle openings. - Every read pads to RATE (the zkDSL recursion format) — `n` real scalars are - consumed as `next_multiple_of(n, RATE)` raw scalars, the trailing positions - must be zero, and the full RATE-aligned chunk is what the challenger absorbs. - """ + Every read pads to RATE — `n` real scalars are consumed as + `next_multiple_of(n, RATE)` raw scalars (trailing positions must be zero), + and the full RATE-aligned chunk is what the challenger absorbs.""" def __init__(self, proof: Proof) -> None: self.challenger = Challenger() self.transcript = list(proof.transcript) - self.merkle_openings = list(proof.merkle_openings) + self.openings = list(proof.merkle_openings) self.offset = 0 - self.merkle_opening_index = 0 + self.open_idx = 0 def _read_padded(self, n: int) -> list[Fp]: - """Read `next_multiple_of(n, RATE)` raw scalars, assert the trailing - positions are zero, observe the full padded chunk, return all of it.""" - n_padded = _next_multiple_of(n, RATE) - if self.offset + n_padded > len(self.transcript): + n_pad = -(-n // RATE) * RATE # next multiple of RATE + if self.offset + n_pad > len(self.transcript): raise ProofError("ExceededTranscript") - chunk = self.transcript[self.offset : self.offset + n_padded] - self.offset += n_padded - for i in range(n, n_padded): - if int(chunk[i].value) != 0: - raise ProofError("InvalidTranscript: non-zero padding") + chunk = self.transcript[self.offset : self.offset + n_pad] + self.offset += n_pad + if any(int(chunk[i].value) for i in range(n, n_pad)): + raise ProofError("InvalidTranscript: non-zero padding") self.challenger.observe_many(chunk) return chunk @@ -356,29 +263,21 @@ def next_extension_scalars_vec(self, n: int) -> list[EF]: flat = self.next_base_scalars_vec(n * EF.DIMENSION) return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] - def next_extension_scalar(self) -> EF: - return self.next_extension_scalars_vec(1)[0] - - def sample(self) -> EF: - return self.challenger.sample_ef() - - def sample_vec(self, n: int) -> list[EF]: - return self.challenger.sample_ef_vec(n) - - def sample_in_range(self, bits: int, n_samples: int) -> list[int]: - return self.challenger.sample_in_range(bits, n_samples) + def next_extension_scalar(self) -> EF: return self.next_extension_scalars_vec(1)[0] + def sample(self) -> EF: return self.challenger.sample_ef() + def sample_vec(self, n: int) -> list[EF]: return self.challenger.sample_ef_vec(n) + def sample_in_range(self, b: int, n: int) -> list[int]: return self.challenger.sample_in_range(b, n) def next_merkle_opening(self) -> MerkleOpening: - if self.merkle_opening_index >= len(self.merkle_openings): + if self.open_idx >= len(self.openings): raise ProofError("ExceededTranscript: no more Merkle openings") - self.merkle_opening_index += 1 - return self.merkle_openings[self.merkle_opening_index - 1] + self.open_idx += 1 + return self.openings[self.open_idx - 1] def check_pow_grinding(self, bits: int) -> None: - """Grinding witness is written as `[witness, 0, 0, 0, 0, 0, 0, 0]`.""" - if bits == 0: - return - chunk = self._read_padded(1) + """Grinding witness is `[witness, 0, …, 0]` (RATE-padded).""" + if bits == 0: return + self._read_padded(1) if int(self.challenger.state[0].value) & ((1 << bits) - 1) != 0: raise ProofError("InvalidGrindingWitness") @@ -458,20 +357,22 @@ def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: + """The "next-row" weight `ν(x, y)`: multilinear extension of `y = x + 1` + (big-endian, mod `2^n`). Sums one term per "carry boundary" position plus + the all-ones wraparound `Π x_i · y_i`.""" assert len(x) == len(y) - n = len(x) one = EF.one() - eq_prefix: list[EF] = [one] + # Prefix of `eq(x_i, y_i)` and suffix of `Π x_i · (1 − y_i)` (the low-bits term). + n = len(x) + eq_prefix = [one] for i in range(n): - eq_i = x[i] * y[i] + (one - x[i]) * (one - y[i]) - eq_prefix.append(eq_prefix[i] * eq_i) - low_suffix: list[EF] = [one] * (n + 1) + eq_prefix.append(eq_prefix[i] * (x[i] * y[i] + (one - x[i]) * (one - y[i]))) + low_suffix = [one] * (n + 1) for i in range(n - 1, -1, -1): low_suffix[i] = low_suffix[i + 1] * x[i] * (one - y[i]) s = EF.zero() - for arr in range(n): - carry = (one - x[arr]) * y[arr] - s = s + eq_prefix[arr] * carry * low_suffix[arr + 1] + for i in range(n): + s = s + eq_prefix[i] * (one - x[i]) * y[i] * low_suffix[i + 1] prod = one for v in list(x) + list(y): prod = prod * v @@ -479,19 +380,12 @@ def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: - """Evaluate a multilinear in *evaluation* form (length 2^n) at point ∈ EF^n. - - Big-endian indexing: index `i` corresponds to the bits `(b_0, ..., b_{n-1})` - where `b_0` is the *most significant* bit, matching `poly::eval_multilinear`. - Fold variables from the last to the first. - """ + """Evaluate a multilinear in evaluation form (length `2^n`) at `point ∈ EF^n`. + Big-endian indexing — fold variables last-to-first.""" assert len(evals) == 1 << len(point) cur = list(evals) for r in reversed(point): - nxt: list[EF] = [] - for j in range(0, len(cur), 2): - nxt.append(cur[j] + (cur[j + 1] - cur[j]) * r) - cur = nxt + cur = [cur[j] + (cur[j + 1] - cur[j]) * r for j in range(0, len(cur), 2)] return cur[0] @@ -541,18 +435,14 @@ def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: - """poly::eval_multilinear_coeffs: split coeffs in half, recurse. - - `coeffs` represents `Σ_b c_b · x_0^{b_0} · ... · x_{n-1}^{b_{n-1}}` - in the standard multilinear coefficient basis. - """ + """`Σ_b c_b · Π_i x_i^(b_i)` in the standard multilinear coefficient basis.""" assert len(coeffs) == 1 << len(point) if not point: return coeffs[0] - x = point[0] - tail = point[1:] half = len(coeffs) // 2 - return eval_multilinear_coeffs(coeffs[:half], tail) + eval_multilinear_coeffs(coeffs[half:], tail) * x + lo = eval_multilinear_coeffs(coeffs[:half], point[1:]) + hi = eval_multilinear_coeffs(coeffs[half:], point[1:]) + return lo + hi * point[0] @dataclass @@ -563,19 +453,18 @@ class SparseValue: @dataclass class SparseStatement: + """A claim with a multilinear `point` over the last `len(point)` variables + and one or more `(selector, value)` pairs indexing the leading selector + bits. `is_next` swaps the eq-weight for a "next-row" weight (`next_mle`).""" total_num_variables: int - point: list[EF] # the "inner" point, length = inner_num_variables + point: list[EF] values: list[SparseValue] is_next: bool = False - @property - def inner_num_variables(self) -> int: - return len(self.point) - @property def selector_num_variables(self) -> int: - return self.total_num_variables - self.inner_num_variables + return self.total_num_variables - len(self.point) @staticmethod def dense(point: list[EF], value: EF) -> "SparseStatement": @@ -594,30 +483,31 @@ def new_next(total: int, point: list[EF], values: list[SparseValue]) -> "SparseS # ─── WHIR config helpers: derive integer-only parameters from the trimmed JSON ────────────────────────────────────────────────────────── -def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: - """FoldingFactor::compute_number_of_rounds with default (7, 5, max_send=8).""" - nv_except_first = num_variables - WHIR_INITIAL_FOLDING_FACTOR - max_send = MAX_NUM_VARIABLES_TO_SEND_COEFFS - if nv_except_first < max_send: - return 0, nv_except_first - n_rounds = -(-(nv_except_first - max_send) // WHIR_SUBSEQUENT_FOLDING_FACTOR) - final_sumcheck_rounds = nv_except_first - n_rounds * WHIR_SUBSEQUENT_FOLDING_FACTOR - return n_rounds, final_sumcheck_rounds - - def whir_folding_factor_at_round(r: int) -> int: return WHIR_INITIAL_FOLDING_FACTOR if r == 0 else WHIR_SUBSEQUENT_FOLDING_FACTOR -def whir_rs_reduction_factor(r: int) -> int: - return RS_DOMAIN_INITIAL_REDUCTION_FACTOR if r == 0 else 1 +def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: + """FoldingFactor::compute_number_of_rounds with default (7, 5, max_send=8). + Returns `(n_rounds, final_sumcheck_rounds)`.""" + nv = num_variables - WHIR_INITIAL_FOLDING_FACTOR + if nv < MAX_NUM_VARIABLES_TO_SEND_COEFFS: + return 0, nv + n = -(-(nv - MAX_NUM_VARIABLES_TO_SEND_COEFFS) // WHIR_SUBSEQUENT_FOLDING_FACTOR) + return n, nv - n * WHIR_SUBSEQUENT_FOLDING_FACTOR + + +def whir_log_inv_rate_at(start_rate: int, r: int) -> int: + """Initial rate, then each round adds `folding_factor − rs_reduction`.""" + return start_rate + r * (WHIR_SUBSEQUENT_FOLDING_FACTOR - 1) + ( + WHIR_INITIAL_FOLDING_FACTOR - RS_DOMAIN_INITIAL_REDUCTION_FACTOR if r >= 1 else 0 + ) -def whir_log_inv_rate_at(starting_log_inv_rate: int, round_index: int) -> int: - rate = starting_log_inv_rate - for r in range(round_index): - rate += whir_folding_factor_at_round(r) - whir_rs_reduction_factor(r) - return rate +def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: + """`log₂(domain_size)` going into round `r`: starts at `num_vars + rate` + and shrinks by the per-round RS reduction (`5` at round 0, `1` thereafter).""" + return num_variables + start_rate - (RS_DOMAIN_INITIAL_REDUCTION_FACTOR + r - 1 if r >= 1 else 0) # KoalaBear two-adic generators: index `bits` is the primitive 2^bits-th root @@ -631,28 +521,9 @@ def whir_log_inv_rate_at(starting_log_inv_rate: int, round_index: int) -> int: def two_adic_generator(bits: int) -> Fp: - assert 0 <= bits <= BASE_TWO_ADICITY return Fp(KB_TWO_ADIC_GENERATORS[bits]) -def whir_domain_size_at(num_variables: int, starting_log_inv_rate: int, round_index: int) -> int: - """domain_size that goes into `round_parameters[round_index]`. - - The Rust code seeds `domain_size = 1 << (num_variables + log_inv_rate)` and - halves by `rs_reduction_factor(round)` BEFORE moving to the next round, so - the value stored in round r is the *current* domain_size pre-reduction. - """ - domain_log = num_variables + starting_log_inv_rate - for r in range(round_index): - domain_log -= whir_rs_reduction_factor(r) - return 1 << domain_log - - -# The Rust-dumped JSON only carries the float-derived numbers (query counts, -# OOD samples, grinding bits); every other parameter is integer arithmetic -# we recompute on the fly via the helpers above. - - @dataclass(frozen=True) class WhirRoundConfig: num_queries: int @@ -673,27 +544,22 @@ class WhirConfig: @functools.cache -def _whir_configs() -> dict[tuple[int, int], WhirConfig]: +def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: + """Loads the Rust-dumped JSON (float-derived query/OOD/grinding numbers). + Everything else is recomputed on the fly via the helpers above.""" import json from pathlib import Path raw = json.loads(Path(__file__).with_name(WHIR_CONFIGS_PATH).read_text()) - return { - (c["log_inv_rate"], c["num_variables"]): WhirConfig( - **{k: c[k] for k in WhirConfig.__annotations__ if k != "rounds"}, - rounds=tuple(WhirRoundConfig(**r) for r in c["rounds"]), - ) - for c in raw - } - - -def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: - try: - return _whir_configs()[(log_inv_rate, num_variables)] - except KeyError: - raise KeyError( - f"No WHIR config for (log_inv_rate={log_inv_rate}, num_variables={num_variables}). " - "Regenerate with: cargo test -p lean_prover --test dump_whir_configs" - ) from None + for c in raw: + if (c["log_inv_rate"], c["num_variables"]) == (log_inv_rate, num_variables): + return WhirConfig( + **{k: c[k] for k in WhirConfig.__annotations__ if k != "rounds"}, + rounds=tuple(WhirRoundConfig(**r) for r in c["rounds"]), + ) + raise KeyError( + f"No WHIR config for (log_inv_rate={log_inv_rate}, num_variables={num_variables}). " + "Regenerate with: cargo test -p lean_prover --test dump_whir_configs" + ) @@ -743,14 +609,13 @@ def verify_sumcheck( degree: int, pow_bits: int = 0, ) -> "Evaluation": - """Read `n_vars` round polynomials (degree-`degree`, sent as `degree + 1` - coefficients). Each round: check `h(0) + h(1) == target`, optional PoW - grinding, sample a challenge, fold the target. Returns (point, value).""" + """Read `n_vars` round polynomials in univariate basis (`degree + 1` coeffs + each). Per round: check `h(0) + h(1) == target`, optional PoW grinding, + sample a challenge, fold the target into `h(challenge)`.""" point: list[EF] = [] for _ in range(n_vars): coeffs = state.next_extension_scalars_vec(degree + 1) - # h(0) + h(1) = coeffs[0] + sum(coeffs). - s = coeffs[0] + s = coeffs[0] # h(0) + h(1) = coeffs[0] + Σ coeffs. for c in coeffs: s = s + c if s != target: @@ -784,71 +649,46 @@ def verify_stir_challenges( cfg: WhirConfig, round_index: int, num_variables: int, - log_inv_rate: int, folding_factor: int, - next_folding_factor: int, num_queries: int, query_pow_bits: int, commitment: ParsedCommitment, folding_randomness: list[EF], ) -> list[SparseStatement]: - """`folding_factor` is the folding factor applied AT this round (i.e. how the - leaves are arranged). `next_folding_factor` is the AIR sumcheck folding for - the *next* hop; for the final pseudo-round it equals `folding_factor`. - - Returns STIR constraints (SparseStatements) for the next claim-combining. - """ - # Domain size at this round (pre-RS-reduction for round `round_index`). - domain_size = whir_domain_size_at(cfg.num_variables, cfg.log_inv_rate, round_index) - folded_domain_size = domain_size >> folding_factor - folded_domain_gen = two_adic_generator(domain_size.bit_length() - 1 - folding_factor) + """Read `num_queries` Merkle openings, fold each answer at + `folding_randomness`, and emit a dense STIR constraint per query.""" + log_domain = whir_log_domain_size_at(cfg.num_variables, cfg.log_inv_rate, round_index) + log_height = log_domain - folding_factor + gen = two_adic_generator(log_height) state.check_pow_grinding(query_pow_bits) - indices = state.sample_in_range(folded_domain_size.bit_length() - 1, num_queries) + indices = state.sample_in_range(log_height, num_queries) - leafs_base_field = round_index == 0 - log_height = folded_domain_size.bit_length() - 1 - answers_ef: list[list[EF]] = [] + def pack_answers(leaf: list[Fp]) -> list[EF]: + # Round 0 leaves are base-field; later rounds carry packed EF (5 base → 1 EF). + if round_index == 0: + return [EF.from_base(f) for f in leaf] + return [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] + + constraints: list[SparseStatement] = [] for idx in indices: op = state.next_merkle_opening() if not merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path): raise ProofError("Merkle verification failed") - # leaf_data is base; if leafs encode EF, pack 5 base → 1 EF. - if leafs_base_field: - answers_ef.append([EF.from_base(f) for f in op.leaf_data]) - else: - ans: list[EF] = [] - for i in range(0, len(op.leaf_data), EF.DIMENSION): - ans.append(EF(op.leaf_data[i : i + EF.DIMENSION])) - answers_ef.append(ans) - - # Each answer is a length-(2^folding_factor) eval-form multilinear; fold at folding_randomness. - folds: list[EF] = [eval_multilinear_evals(a, folding_randomness) for a in answers_ef] - - stir_constraints: list[SparseStatement] = [] - for idx, fold in zip(indices, folds): - point = folded_domain_gen.value - # point = folded_domain_gen ^ idx, as a base-field element wrapped into EF. - gen_pow = pow(int(folded_domain_gen.value), idx, P) - ef_pt = EF.from_base(Fp(gen_pow)) - expanded = expand_from_univariate(ef_pt, num_variables) - stir_constraints.append(SparseStatement.dense(expanded, fold)) - return stir_constraints + fold = eval_multilinear_evals(pack_answers(op.leaf_data), folding_randomness) + ef_pt = EF.from_base(Fp(pow(int(gen.value), idx, P))) + constraints.append(SparseStatement.dense(expand_from_univariate(ef_pt, num_variables), fold)) + return constraints def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> bool: - """Checks that the constraint's point is `[α, α^2, α^4, ...]` and that - the univariate polynomial (Horner) evaluates to each claimed value. - """ + """Checks `constraint.point == [α, α², α⁴, …]` and that the univariate + `Σ coeffs[i] · α^i` matches every claimed value.""" assert constraint.selector_num_variables == 0 alpha = constraint.point[0] - for a, b in zip(constraint.point, constraint.point[1:]): - if a * a != b: - return False - # Horner from highest-degree coefficient (last in `coeffs`) downward. - univ_eval = EF.zero() - for c in reversed(coeffs): - univ_eval = univ_eval * alpha + c + if any(a * a != b for a, b in zip(constraint.point, constraint.point[1:])): + return False + univ_eval = _eval_univariate(coeffs, alpha) return all(univ_eval == v.value for v in constraint.values) @@ -856,30 +696,26 @@ def eval_constraints_poly( constraints: list[tuple[list[EF], list[SparseStatement]]], point: list[EF], ) -> EF: - """`constraints` is a list of (combination_randomness, sparse_statements) per - round. `point` is the global folding randomness; it is sliced down by the - folding factor of each preceding round before use. - """ + """Per-round `(combination_weights, statements)`: at each round we slice + `point` by the previous round's folding factor, then sum + `lagrange(selector) · common_weight(smt.point, inner_pt) · γ^i` over all + `(smt, value)` pairs.""" value = EF.zero() pt = list(point) for round_idx, (randomness, smts) in enumerate(constraints): if round_idx > 0: - k = whir_folding_factor_at_round(round_idx - 1) - pt = pt[k:] + pt = pt[whir_folding_factor_at_round(round_idx - 1):] i = 0 for smt in smts: - inner_pt = pt[len(pt) - smt.inner_num_variables :] - if smt.is_next: - common_weight = next_mle(smt.point, inner_pt) - else: - common_weight = eq_poly_outside(smt.point, inner_pt) + inner_pt = pt[len(pt) - len(smt.point):] + common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly_outside(smt.point, inner_pt) + sel_n = smt.selector_num_variables for v in smt.values: - # Per-selector lagrange weight on bits NOT covered by the inner point. lagrange = EF.one() - for j in range(smt.selector_num_variables): - bit = (v.selector >> (smt.selector_num_variables - 1 - j)) & 1 + for j in range(sel_n): + bit = (v.selector >> (sel_n - 1 - j)) & 1 lagrange = lagrange * (pt[j] if bit else (EF.one() - pt[j])) - value = value + lagrange * common_weight * randomness[i] + value = value + lagrange * common * randomness[i] i += 1 assert i == len(randomness) return value @@ -898,78 +734,58 @@ def whir_verify( round_constraints: list[tuple[list[EF], list[SparseStatement]]] = [] round_folding: list[list[EF]] = [] target = EF.zero() - prev_commitment = parsed_commitment - # Initial: combine OODS + statement, then run the first folding sumcheck. - initial_constraints = prev_commitment.oods_constraints() + statement - target, combo = combine_constraints(state, target, initial_constraints) - round_constraints.append((combo, initial_constraints)) - init_sc = verify_sumcheck(state, target, whir_folding_factor_at_round(0), 2, cfg.starting_folding_pow_bits) - round_folding.append(init_sc.point) - target = init_sc.value + def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None: + nonlocal target + new_target, combo = combine_constraints(state, target, constraints) + round_constraints.append((combo, constraints)) + sc = verify_sumcheck(state, new_target, n_fold, 2, pow_bits) + round_folding.append(sc.point) + target = sc.value + + # Initial: OODS + caller statement, then run the first folding sumcheck. + step(parsed_commitment.oods_constraints() + statement, + whir_folding_factor_at_round(0), cfg.starting_folding_pow_bits) - # Per-round loop: new commitment → STIR → combine → sumcheck. + # Per-round loop: new commitment → STIR → combine → fold sumcheck. + prev_commitment = parsed_commitment + nvars_round = cfg.num_variables for r in range(n_rounds): rp = cfg.rounds[r] - nvars_round = cfg.num_variables - sum(whir_folding_factor_at_round(i) for i in range(r + 1)) + nvars_round -= whir_folding_factor_at_round(r) new_commitment = parsed_commitment_parse(state, nvars_round, rp.ood_samples) - stir_constraints = verify_stir_challenges( - state, cfg, - round_index=r, - num_variables=nvars_round, - log_inv_rate=whir_log_inv_rate_at(cfg.log_inv_rate, r), - folding_factor=whir_folding_factor_at_round(r), - next_folding_factor=whir_folding_factor_at_round(r + 1), - num_queries=rp.num_queries, - query_pow_bits=rp.query_pow_bits, - commitment=prev_commitment, - folding_randomness=round_folding[-1], + stir = verify_stir_challenges( + state, cfg, r, nvars_round, + whir_folding_factor_at_round(r), rp.num_queries, rp.query_pow_bits, + prev_commitment, round_folding[-1], ) - constraints_r = new_commitment.oods_constraints() + stir_constraints - target, combo_r = combine_constraints(state, target, constraints_r) - round_constraints.append((combo_r, constraints_r)) - sc = verify_sumcheck(state, target, whir_folding_factor_at_round(r + 1), 2, rp.folding_pow_bits) - round_folding.append(sc.point) - target = sc.value + step(new_commitment.oods_constraints() + stir, + whir_folding_factor_at_round(r + 1), rp.folding_pow_bits) prev_commitment = new_commitment - # Final round: read the final polynomial in coefficient form, then run a - # last batch of STIR queries against the last commitment. - n_vars_final = cfg.num_variables - sum(whir_folding_factor_at_round(i) for i in range(n_rounds + 1)) + # Final round: send poly in coefficient form, verify STIR queries against it. + n_vars_final = nvars_round - whir_folding_factor_at_round(n_rounds) final_coeffs = state.next_extension_scalars_vec(1 << n_vars_final) - - final_domain_size = whir_domain_size_at(cfg.num_variables, cfg.log_inv_rate, n_rounds) - final_folding_factor = whir_folding_factor_at_round(n_rounds) - folded_gen_final = two_adic_generator(final_domain_size.bit_length() - 1 - final_folding_factor) - log_height_final = (final_domain_size >> final_folding_factor).bit_length() - 1 - - state.check_pow_grinding(cfg.final_query_pow_bits) - for idx in state.sample_in_range(log_height_final, cfg.final_queries): - op = state.next_merkle_opening() - if not merkle_verify_path(prev_commitment.root, log_height_final, idx, op.leaf_data, op.path): - raise ProofError("Final Merkle verification failed") - if n_rounds == 0: - answers = [EF.from_base(f) for f in op.leaf_data] - else: - answers = [EF(op.leaf_data[i : i + EF.DIMENSION]) for i in range(0, len(op.leaf_data), EF.DIMENSION)] - fold = eval_multilinear_evals(answers, round_folding[-1]) - ef_pt = EF.from_base(Fp(pow(int(folded_gen_final.value), idx, P))) - smt = SparseStatement.dense(expand_from_univariate(ef_pt, n_vars_final), fold) + final_stir = verify_stir_challenges( + state, cfg, n_rounds, n_vars_final, + whir_folding_factor_at_round(n_rounds), cfg.final_queries, cfg.final_query_pow_bits, + prev_commitment, round_folding[-1], + ) + for smt in final_stir: if not verify_constraint_coeffs(smt, final_coeffs): raise ProofError("Final STIR constraint mismatch") # Final sumcheck — closes the protocol against the constraint-weights MLE. final_sc = verify_sumcheck(state, target, final_sumcheck_rounds, 2) round_folding.append(final_sc.point) - target = final_sc.value - folding_randomness_flat = [r for chunk in round_folding for r in chunk] - eval_weights = eval_constraints_poly(round_constraints, folding_randomness_flat) + folding_flat = [r for chunk in round_folding for r in chunk] + eval_weights = eval_constraints_poly(round_constraints, folding_flat) final_value = eval_multilinear_coeffs(final_coeffs, list(reversed(final_sc.point))) - if target != eval_weights * final_value: + if final_sc.value != eval_weights * final_value: raise ProofError("WHIR final sumcheck check failed") - return folding_randomness_flat + return folding_flat @@ -1009,18 +825,12 @@ def compute_stacked_n_vars( table_log_heights: dict[str, int], table_n_columns: dict[str, int], ) -> int: - """The stacked polynomial concatenates: - - 2 copies of memory -> 2 * 2^log_memory - - one bytecode accumulator padded -> 2^max(log_bytecode, max_table_log_n_rows) - - per table: n_columns * 2^log_n_rows - """ - max_table_log_n_rows = max(table_log_heights.values()) - total_len = (2 << log_memory) + ( - 1 << max(log_bytecode, max_table_log_n_rows) - ) - for name, log_n_rows in table_log_heights.items(): - total_len += table_n_columns[name] << log_n_rows - return log2_ceil_usize(total_len) + """`log₂` of the stacked polynomial length: 2·memory + bytecode-acc (padded + to the tallest table) + Σ per-table `n_columns × 2^log_n_rows`.""" + max_h = max(table_log_heights.values()) + total = (2 << log_memory) + (1 << max(log_bytecode, max_h)) + total += sum(table_n_columns[n] << h for n, h in table_log_heights.items()) + return log2_ceil_usize(total) def stacked_pcs_global_statements( @@ -1073,25 +883,15 @@ def stacked_pcs_parse_commitment( log_bytecode: int, table_log_heights: dict[str, int], table_n_columns: dict[str, int], - execution_table_name: str = "execution", ) -> ParsedCommitment: - """- Memory must be at least as wide as the execution table. - - The execution table must be the tallest table. - - The stacked-poly size must fit within the WHIR domain bound. - The actual commitment parsing is then delegated to `parsed_commitment_parse`. - """ - exec_log = table_log_heights[execution_table_name] + """Validate sizing invariants (memory ≥ execution ≥ all other tables, + stacked-poly fits the WHIR domain bound), then parse the commitment.""" + exec_log = table_log_heights["execution"] if log_memory < exec_log or exec_log < max(table_log_heights.values()): raise ProofError("InvalidProof: memory or execution table size invariants broken") - - stacked_n_vars = compute_stacked_n_vars( - log_memory, log_bytecode, table_log_heights, table_n_columns - ) - # `WhirConfig::new` asserts stacked_n_vars + log_inv_rate - first_round <= F::TWO_ADICITY. - max_nv = BASE_TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate - if stacked_n_vars > max_nv: + stacked_n_vars = compute_stacked_n_vars(log_memory, log_bytecode, table_log_heights, table_n_columns) + if stacked_n_vars > BASE_TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") - cfg = whir_config(log_inv_rate, stacked_n_vars) return parsed_commitment_parse(state, stacked_n_vars, cfg.commitment_ood_samples) @@ -1143,77 +943,55 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: - """Returns the `bit_count` bits of `value` MSB-first, each as `EF::ZERO`/`EF::ONE`. - """ + """`bit_count` bits of `value` MSB-first, each as `EF::ZERO`/`EF::ONE`.""" return [EF.one() if (value >> (bit_count - 1 - i)) & 1 else EF.zero() for i in range(bit_count)] def from_end(seq: Sequence, n: int) -> list: - """`utils::from_end` — the last `n` elements.""" - if n == 0: - return [] - return list(seq[len(seq) - n :]) + """Last `n` elements of `seq`.""" + return list(seq[len(seq) - n :]) if n else [] def mle_of_01234567_etc(point: Sequence[EF]) -> EF: - """Evaluates the multilinear polynomial whose evaluations on `{0,1}^n` are - `f(i) = i` (with `i` interpreted big-endian), at `point`. - """ + """MLE of `f(i) = i` (big-endian) at `point`.""" if not point: return EF.zero() e = mle_of_01234567_etc(point[1:]) - bit = EF.from_base(Fp(1 << (len(point) - 1))) - return (EF.one() - point[0]) * e + point[0] * (e + bit) + return e + point[0] * EF.from_base(Fp(1 << (len(point) - 1))) def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: - """Evaluates the multilinear of `[0, ..., 0, 1, ..., 1]` (`n_zeros` zeros, then - `2^len(point) - n_zeros` ones) at `point`. - """ + """MLE of `[0]*n_zeros ++ [1]*(2^len(point) − n_zeros)` at `point`.""" n_values = 1 << len(point) assert n_zeros <= n_values - if n_zeros == 0: - return EF.one() - if n_zeros == n_values: - return EF.zero() - half = n_values >> 1 + if n_zeros == 0: return EF.one() + if n_zeros == n_values: return EF.zero() + half, tail = n_values >> 1, point[1:] if n_zeros < half: - return (EF.one() - point[0]) * mle_of_zeros_then_ones(n_zeros, point[1:]) + point[0] - return point[0] * mle_of_zeros_then_ones(n_zeros - half, point[1:]) + return (EF.one() - point[0]) * mle_of_zeros_then_ones(n_zeros, tail) + point[0] + return point[0] * mle_of_zeros_then_ones(n_zeros - half, tail) def finger_print(table: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: - """Computes `Σᵢ alphas_eq_poly[i] · data[i] + alphas_eq_poly[-1] · table`. - """ + """`Σᵢ αᵢ · dataᵢ + α_last · table` — Reed-Solomon-style fingerprint.""" assert len(alphas_eq_poly) > len(data) acc = EF.zero() for a, d in zip(alphas_eq_poly, data): acc = acc + a * d - acc = acc + alphas_eq_poly[-1] * EF.from_base(table) - return acc + return acc + alphas_eq_poly[-1] * EF.from_base(table) -def sort_tables_by_height( - table_log_heights: dict[str, int], -) -> list[tuple[str, int]]: - """`BTreeMap` ordering (= alphabetical) breaks ties. - """ - items = sorted(table_log_heights.items()) # alphabetical - items.sort(key=lambda kv: -kv[1]) - return items +def sort_tables_by_height(table_log_heights: dict[str, int]) -> list[tuple[str, int]]: + """Descending by height, alphabetical on ties (matches Rust `BTreeMap`).""" + return sorted(sorted(table_log_heights.items()), key=lambda kv: -kv[1]) def eval_eq(point: Sequence[EF]) -> list[EF]: - """Evaluation table of `eq(point, ·)`: the length-`2^n` vector with - `eq[i] = Πⱼ (point[j] if bitⱼ(i) else 1 - point[j])` for big-endian `i`. - """ + """Length-`2^n` evaluation table of `eq(point, ·)`: big-endian-bit-indexed + `Πⱼ (point[j] if bitⱼ(i) else 1 − point[j])`.""" out = [EF.one()] for p in point: - nxt: list[EF] = [] - for v in out: - nxt.append(v * (EF.one() - p)) - nxt.append(v * p) - out = nxt + out = [w for v in out for w in (v * (EF.one() - p), v * p)] return out @@ -1223,7 +1001,6 @@ def eval_eq(point: Sequence[EF]) -> list[EF]: @dataclass class GenericLogupStatements: - memory_and_acc_point: list[EF] value_memory: EF value_memory_acc: EF @@ -1233,8 +1010,6 @@ class GenericLogupStatements: bus_denominators_values: dict[str, EF] gkr_point: list[EF] columns_values: dict[str, dict[int, EF]] - total_gkr_n_vars: int - bytecode_evaluation: Evaluation def verify_generic_logup( @@ -1247,189 +1022,136 @@ def verify_generic_logup( table_log_heights: dict[str, int], tables: dict[str, TableMeta], constants: dict, - execution_name: str = "execution", ) -> GenericLogupStatements: - """`bytecode_multilinear` is the flat coefficient vector of length - `2^(log_bytecode + ceil(log2(N_INSTRUCTION_COLUMNS)))` — what the Rust - verifier holds as `&bytecode.instructions_multilinear`. - - `alphas` and `alphas_eq_poly` come from sampling `c` and `log2_ceil(max_bus_width)` - extension-field elements (per the leanVM `verify_execution`). - """ - - n_instr_cols = constants["n_instruction_columns"] + """Run the GKR-quotient protocol and reconstruct numerator/denominator + sums section by section (memory / bytecode / per-table). Each section + contributes a `pref · (num_term, den_term)` pair to the running totals.""" + n_instr_cols = constants["n_instruction_columns"] n_runtime_cols = constants["n_runtime_columns"] - col_pc = constants["col_pc"] - dom_mem = constants["logup_memory_domainsep"] - dom_byte = constants["logup_bytecode_domainsep"] + col_pc = constants["col_pc"] + dom_mem = Fp(constants["logup_memory_domainsep"]) + dom_byte = Fp(constants["logup_bytecode_domainsep"]) tables_sorted = sort_tables_by_height(table_log_heights) - n_instr_padded = 1 << log2_ceil_usize(n_instr_cols) # next power of 2 - log_bytecode = log2_strict_usize(len(bytecode_multilinear) // n_instr_padded) + log_bytecode = log2_strict_usize(len(bytecode_multilinear) // (1 << log2_ceil_usize(n_instr_cols))) + log_instr = log2_ceil_usize(n_instr_cols) + log_n_cycles = table_log_heights["execution"] - # Total active length = memory + bytecode + execution + per-table footprints, - # where each footprint is (sum of lookup arities + 1 bus column) × 2^log_n_rows. - max_table_height = 1 << tables_sorted[0][1] - log_n_cycles = next(h for n, h in tables_sorted if n == execution_name) + # Total active length: memory + max(bytecode, tallest table) + execution + # cycles + Σ per-table (lookup arity sum + 1 bus column) × 2^log_n_rows. table_cols = lambda n: sum(len(vs) for _, vs in tables[n].lookups) + 1 total_active_len = ( - (1 << log_memory) - + max(1 << log_bytecode, max_table_height) - + (1 << log_n_cycles) - + sum((table_cols(n) << h) for n, h in tables_sorted) + (1 << log_memory) + max(1 << log_bytecode, 1 << tables_sorted[0][1]) + (1 << log_n_cycles) + + sum(table_cols(n) << h for n, h in tables_sorted) ) total_gkr_n_vars = log2_ceil_usize(total_active_len) - quotient, point_gkr, numerators_value, denominators_value = verify_gkr_quotient( - state, total_gkr_n_vars - ) - + quotient, point_gkr, claim_num, claim_den = verify_gkr_quotient(state, total_gkr_n_vars) if quotient != EF.zero(): raise ProofError("logup: GKR sum != 0") - retrieved_num = EF.zero() - retrieved_den = EF.zero() - + num, den = EF.zero(), EF.zero() def pref_at(offset: int, log_height: int) -> EF: n_missing = total_gkr_n_vars - log_height bits = to_big_endian_in_field(offset >> log_height, n_missing) return eq_poly_outside(bits, point_gkr[:n_missing]) - # ---- Memory section -------------------------------------------------- - memory_and_acc_point = from_end(point_gkr, log_memory) - pref = pref_at(0, log_memory) - + # Memory section: subtracts the accumulator from `num`, adds (c − fp) to `den`. + mem_pt = from_end(point_gkr, log_memory) + pref = pref_at(0, log_memory) value_memory_acc = state.next_extension_scalar() - retrieved_num = retrieved_num - pref * value_memory_acc - - value_memory = state.next_extension_scalar() - value_index = mle_of_01234567_etc(memory_and_acc_point) - retrieved_den = retrieved_den + pref * ( - c - finger_print(Fp(dom_mem), [value_memory, value_index], alphas_eq_poly) - ) + value_memory = state.next_extension_scalar() + fp_mem = finger_print(dom_mem, [value_memory, mle_of_01234567_etc(mem_pt)], alphas_eq_poly) + num = num - pref * value_memory_acc + den = den + pref * (c - fp_mem) offset = 1 << log_memory - # ---- Bytecode section ------------------------------------------------ - log_bytecode_padded = max(log_bytecode, tables_sorted[0][1]) - bytecode_and_acc_point = from_end(point_gkr, log_bytecode) - pref = pref_at(offset, log_bytecode) - pref_padded = pref_at(offset, log_bytecode_padded) - + # Bytecode section: same shape; the bytecode MLE is evaluated at the + # `bytecode_and_acc_point + last log_instr coords of alphas` point and + # corrected by `Π (1 − alpha_i)` over the bus-data prefix. + log_byte_pad = max(log_bytecode, tables_sorted[0][1]) + byte_pt = from_end(point_gkr, log_bytecode) + pref = pref_at(offset, log_bytecode) + pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() - retrieved_num = retrieved_num - pref * value_bytecode_acc - - bytecode_index_value = mle_of_01234567_etc(bytecode_and_acc_point) - log_instr = log2_ceil_usize(n_instr_cols) - bytecode_point = list(bytecode_and_acc_point) + list(from_end(alphas, log_instr)) - bytecode_value = eval_mle_base_at_ef(bytecode_multilinear, bytecode_point) - # Correction: `(1 - alpha[0]) * (1 - alpha[1]) * ... * (1 - alpha[k-1])` - # over the alphas BEFORE the last `log_instr` (= the bus-data slot bits). + bytecode_value = eval_mle_base_at_ef( + bytecode_multilinear, list(byte_pt) + list(from_end(alphas, log_instr)) + ) correction = EF.one() for a in alphas[: len(alphas) - log_instr]: correction = correction * (EF.one() - a) - bytecode_value_corrected = bytecode_value * correction - retrieved_den = retrieved_den + pref * ( - c - - ( - bytecode_value_corrected - + bytecode_index_value * alphas_eq_poly[n_instr_cols] - + alphas_eq_poly[-1] * EF.from_base(Fp(dom_byte)) - ) - ) - - # Padding for bytecode (bytecode_acc shorter than max_table_height). - retrieved_den = retrieved_den + pref_padded * mle_of_zeros_then_ones( - 1 << log_bytecode, from_end(point_gkr, log_bytecode_padded) + fp_byte = ( + bytecode_value * correction + + mle_of_01234567_etc(byte_pt) * alphas_eq_poly[n_instr_cols] + + alphas_eq_poly[-1] * EF.from_base(dom_byte) ) - offset += 1 << log_bytecode_padded + num = num - pref * value_bytecode_acc + den = den + pref * (c - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, from_end(point_gkr, log_byte_pad)) + offset += 1 << log_byte_pad - # ---- Per-table sections ---------------------------------------------- + # Per-table sections: execution-only bytecode lookup, bus column, then + # one (index, value-array) lookup per `meta.lookups`. Each contributes + # `pref` to `num` and `pref · (c − fingerprint)` to `den`. bus_num_vals: dict[str, EF] = {} bus_den_vals: dict[str, EF] = {} columns_values: dict[str, dict[int, EF]] = {} - for name, log_n_rows in tables_sorted: meta = tables[name] table_values: dict[int, EF] = {} - if name == execution_name: - # 0] Bytecode lookup for the execution table. + if name == "execution": eval_on_pc = state.next_extension_scalar() - table_values[col_pc] = eval_on_pc - instr_evals = state.next_extension_scalars_vec(n_instr_cols) - for i, e in enumerate(instr_evals): - table_values[n_runtime_cols + i] = e - + table_values[col_pc] = eval_on_pc + table_values.update({n_runtime_cols + i: e for i, e in enumerate(instr_evals)}) pref = pref_at(offset, log_n_rows) - retrieved_num = retrieved_num + pref # numerator is 1 - retrieved_den = retrieved_den + pref * ( - c - - finger_print( - Fp(dom_byte), - list(instr_evals) + [eval_on_pc], - alphas_eq_poly, - ) - ) + fp = finger_print(dom_byte, list(instr_evals) + [eval_on_pc], alphas_eq_poly) + num = num + pref + den = den + pref * (c - fp) offset += 1 << log_n_rows - # I] Bus (data flow between tables) + # Bus column (data flow between tables). eval_on_selector = state.next_extension_scalar() + eval_on_data = state.next_extension_scalar() pref = pref_at(offset, log_n_rows) - retrieved_num = retrieved_num + pref * eval_on_selector - - eval_on_data = state.next_extension_scalar() - retrieved_den = retrieved_den + pref * eval_on_data - + num = num + pref * eval_on_selector + den = den + pref * eval_on_data bus_num_vals[name] = eval_on_selector bus_den_vals[name] = eval_on_data offset += 1 << log_n_rows - # II] Lookups into memory + # Lookups into memory. for index_col, value_cols in meta.lookups: index_eval = state.next_extension_scalar() assert index_col not in table_values table_values[index_col] = index_eval - for i, col_index in enumerate(value_cols): value_eval = state.next_extension_scalar() assert col_index not in table_values table_values[col_index] = value_eval - pref = pref_at(offset, log_n_rows) - retrieved_num = retrieved_num + pref - retrieved_den = retrieved_den + pref * ( - c - - finger_print( - Fp(dom_mem), - [value_eval, index_eval + EF.from_base(Fp(i))], - alphas_eq_poly, - ) - ) + fp = finger_print(dom_mem, [value_eval, index_eval + EF.from_base(Fp(i))], alphas_eq_poly) + num = num + pref + den = den + pref * (c - fp) offset += 1 << log_n_rows columns_values[name] = table_values - # Padding tail (xxx..xxx111...1 region beyond `offset`). - retrieved_den = retrieved_den + mle_of_zeros_then_ones(offset, point_gkr) - - if retrieved_num != numerators_value: - raise ProofError("logup: numerators value mismatch") - if retrieved_den != denominators_value: - raise ProofError("logup: denominators value mismatch") + # Padding tail (zeros over the [offset .. 2^total_gkr_n_vars) region). + den = den + mle_of_zeros_then_ones(offset, point_gkr) + if num != claim_num: raise ProofError("logup: numerators value mismatch") + if den != claim_den: raise ProofError("logup: denominators value mismatch") return GenericLogupStatements( - memory_and_acc_point=list(memory_and_acc_point), + memory_and_acc_point=list(mem_pt), value_memory=value_memory, value_memory_acc=value_memory_acc, - bytecode_and_acc_point=list(bytecode_and_acc_point), + bytecode_and_acc_point=list(byte_pt), value_bytecode_acc=value_bytecode_acc, bus_numerators_values=bus_num_vals, bus_denominators_values=bus_den_vals, gkr_point=list(point_gkr), columns_values=columns_values, - total_gkr_n_vars=total_gkr_n_vars, - bytecode_evaluation=Evaluation(point=bytecode_point, value=bytecode_value), ) @@ -1499,71 +1221,49 @@ def air_constraint_eval( def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: # Column layout (execution/air.rs): pc, fp, addr_{a,b,c}, value_{a,b,c}, # operand_{a,b,c}, flag_{a,b,c}, flag_c_fp, flag_ab_fp, mul, jump, aux, - # precompile_data. `down[0..2]` is the next row's (pc, fp). + # precompile_data. `down[0..2]` carries the next row's (pc, fp). (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, flag_ab_fp, mul, jump, aux, precompile_data) = folder.up[:20] next_pc, next_fp = folder.down[0], folder.down[1] one = EF.one() - one_minus_flag_a_and_flag_ab_fp = -(flag_a + flag_ab_fp - one) - one_minus_flag_b_and_flag_ab_fp = -(flag_b + flag_ab_fp - one) - one_minus_flag_c_and_flag_c_fp = -(flag_c + flag_c_fp - one) - - nu_a = ( - flag_a * operand_a - + one_minus_flag_a_and_flag_ab_fp * value_a - + flag_ab_fp * (fp + operand_a) - ) - nu_b = ( - flag_b * operand_b - + one_minus_flag_b_and_flag_ab_fp * value_b - + flag_ab_fp * (fp + operand_b) - ) - nu_c = ( - flag_c * operand_c - + one_minus_flag_c_and_flag_c_fp * value_c - + flag_c_fp * (fp + operand_c) - ) - - fp_plus_op_a = fp + operand_a - fp_plus_op_b = fp + operand_b - fp_plus_op_c = fp + operand_c - pc_plus_one = pc + one - nu_a_minus_one = nu_a - one - - add = aux * EF.from_base(Fp(2)) - aux * aux + # `nu_x = flag · operand + (1 − flag − flag_ab_fp) · value + flag_ab_fp · (fp + operand)`. + nfa = -(flag_a + flag_ab_fp - one) + nfb = -(flag_b + flag_ab_fp - one) + nfc = -(flag_c + flag_c_fp - one) + nu_a = flag_a * operand_a + nfa * value_a + flag_ab_fp * (fp + operand_a) + nu_b = flag_b * operand_b + nfb * value_b + flag_ab_fp * (fp + operand_b) + nu_c = flag_c * operand_c + nfc * value_c + flag_c_fp * (fp + operand_c) + + # `aux` is a 2-bit gadget: aux=0→nothing, aux=1→add, aux=2→deref. From it + # we derive boolean flags (`add` / `deref`) and the precompile catch-all. + add = aux * EF.from_base(Fp(2)) - aux * aux deref = aux * (aux - one) * EF.from_base(_INV_TWO) is_precompile = -(add + mul + deref + jump - one) - # Constraint 1: bus column (assert_zero_ef) - folder.assert_zero( - _eval_virtual_bus_column( - extra_data, is_precompile, [precompile_data, nu_a, nu_b, nu_c] - ) - ) - - # Constraints 2-4: address consistency - folder.assert_zero(one_minus_flag_a_and_flag_ab_fp * (addr_a - fp_plus_op_a)) - folder.assert_zero(one_minus_flag_b_and_flag_ab_fp * (addr_b - fp_plus_op_b)) - folder.assert_zero(one_minus_flag_c_and_flag_c_fp * (addr_c - fp_plus_op_c)) - - # Constraints 5-6: add/mul + # Constraint 1: precompile bus column. + folder.assert_zero(_eval_virtual_bus_column( + extra_data, is_precompile, [precompile_data, nu_a, nu_b, nu_c], + )) + # Constraints 2-4: address consistency on memory operands. + folder.assert_zero(nfa * (addr_a - (fp + operand_a))) + folder.assert_zero(nfb * (addr_b - (fp + operand_b))) + folder.assert_zero(nfc * (addr_c - (fp + operand_c))) + # Constraints 5-6: add / mul gates. folder.assert_zero(add * (nu_b - (nu_a + nu_c))) folder.assert_zero(mul * (nu_b - nu_a * nu_c)) - - # Constraints 7-8: deref + # Constraints 7-8: deref — `addr_b == value_a + operand_b` and `value_b == nu_c`. folder.assert_zero(deref * (addr_b - (value_a + operand_b))) folder.assert_zero(deref * (value_b - nu_c)) - - # Constraints 9-13: jump - jump_and_condition = jump * nu_a - folder.assert_zero(jump_and_condition * nu_a_minus_one) - folder.assert_zero(jump_and_condition * (next_pc - nu_b)) - folder.assert_zero(jump_and_condition * (next_fp - nu_c)) - not_jump_and_condition = -(jump_and_condition - one) - folder.assert_zero(not_jump_and_condition * (next_pc - pc_plus_one)) - folder.assert_zero(not_jump_and_condition * (next_fp - fp)) + # Constraints 9-13: jump control flow. + jc = jump * nu_a + folder.assert_zero(jc * (nu_a - one)) + folder.assert_zero(jc * (next_pc - nu_b)) + folder.assert_zero(jc * (next_fp - nu_c)) + not_jc = -(jc - one) + folder.assert_zero(not_jc * (next_pc - (pc + one))) + folder.assert_zero(not_jc * (next_fp - fp)) @@ -1579,97 +1279,73 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: - """Quintic-extension multiplication on 5-element EF arrays. - - Direct port of `quintic_mul` from koalabear/quintic_extension/extension.rs - using EF-level arithmetic — the dot-product becomes `Σ a[i]·b'[i]`. - """ + """Port of `quintic_mul` from `koalabear/quintic_extension/extension.rs` — + multiplication of 5-tuples of EF as quintic-extension elements.""" assert len(a) == 5 and len(b) == 5 - b0m3 = b[0] - b[3] - b1m4 = b[1] - b[4] - b4m2 = b[4] - b[2] - - def dot(av: Sequence[EF], bv: Sequence[EF]) -> EF: - acc = EF.zero() - for x, y in zip(av, bv): - acc = acc + x * y - return acc - - return [ - dot(a, [b[0], b[4], b[3], b[2], b1m4]), - dot(a, [b[1], b[0], b[4], b[3], b[2]]), - dot(a, [b[2], b1m4, b0m3, b4m2, b[3] - b1m4]), - dot(a, [b[3], b[2], b1m4, b0m3, b4m2]), - dot(a, [b[4], b[3], b[2], b1m4, b0m3]), + b0m3, b1m4, b4m2 = b[0] - b[3], b[1] - b[4], b[4] - b[2] + rows = [ + [b[0], b[4], b[3], b[2], b1m4], + [b[1], b[0], b[4], b[3], b[2]], + [b[2], b1m4, b0m3, b4m2, b[3] - b1m4], + [b[3], b[2], b1m4, b0m3, b4m2], + [b[4], b[3], b[2], b1m4, b0m3], ] + def dot(row: list[EF]) -> EF: + acc = a[0] * row[0] + for i in range(1, 5): + acc = acc + a[i] * row[i] + return acc + return [dot(row) for row in rows] def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: - # `up` layout (extension_op/air.rs): is_be, start, flag_{add,mul,poly_eq}, - # len, idx_{a,b,res}, then four 5-element EF blocks va, vb, vres, comp. + # `up`: is_be, start, flag_{add,mul,poly_eq}, len, idx_{a,b,res}, then four + # 5-EF blocks va, vb, vres, comp. `down`: start, is_be, len, flag_*, idx_*, + # comp[0..5] (we only use the next-row `start` and `comp` here). is_be, start, flag_add, flag_mul, flag_poly_eq, len_col, idx_a, idx_b, idx_res = folder.up[:9] va, vb, vres, comp = (folder.up[9 + 5 * k : 9 + 5 * (k + 1)] for k in range(4)) - # `down` layout: start, is_be, len, flag_{add,mul,poly_eq}, idx_{a,b}, comp[0..5]. + # `down`: start, is_be, len, flag_{add,mul,poly_eq}, idx_{a,b}, comp[0..5]. (start_down, is_be_down, len_down, flag_add_down, flag_mul_down, flag_poly_eq_down, idx_a_down, idx_b_down) = folder.down[:8] comp_down = folder.down[8:13] one = EF.one() - active = flag_add + flag_mul + flag_poly_eq - activation_flag = start * active - - aux = ( - is_be * EF.from_base(Fp(_EXT_OP_FLAG_IS_BE)) - + flag_add * EF.from_base(Fp(_EXT_OP_FLAG_ADD)) - + flag_mul * EF.from_base(Fp(_EXT_OP_FLAG_MUL)) - + flag_poly_eq * EF.from_base(Fp(_EXT_OP_FLAG_POLY_EQ)) - + len_col * EF.from_base(Fp(_EXT_OP_LEN_MULTIPLIER)) - ) - - # Constraint 1: bus - folder.assert_zero( - _eval_virtual_bus_column(extra_data, activation_flag, [aux, idx_a, idx_b, idx_res]) - ) - - is_ee = -(is_be - one) - not_start_down = -(start_down - one) - - va_f_or_ef = [va[0]] + [va[k] * is_ee for k in range(1, 5)] - comp_tail = [comp_down[k] * not_start_down for k in range(5)] - - # Constraints 2-6: bool flags - folder.assert_bool(is_be) - folder.assert_bool(start) - folder.assert_bool(flag_add) - folder.assert_bool(flag_mul) - folder.assert_bool(flag_poly_eq) + # Constraint 1: precompile bus. + aux = (is_be * EF.from_base(Fp(_EXT_OP_FLAG_IS_BE)) + + flag_add * EF.from_base(Fp(_EXT_OP_FLAG_ADD)) + + flag_mul * EF.from_base(Fp(_EXT_OP_FLAG_MUL)) + + flag_poly_eq * EF.from_base(Fp(_EXT_OP_FLAG_POLY_EQ)) + + len_col * EF.from_base(Fp(_EXT_OP_LEN_MULTIPLIER))) + folder.assert_zero(_eval_virtual_bus_column( + extra_data, start * (flag_add + flag_mul + flag_poly_eq), [aux, idx_a, idx_b, idx_res], + )) + + # Constraints 2-6: bool flags. + for f in (is_be, start, flag_add, flag_mul, flag_poly_eq): + folder.assert_bool(f) + + # `va` is `Fp` when in base-extension mode, full EF otherwise; `comp_tail` + # carries the next chunk's comp when this row isn't a `start`. + is_ee, not_start_down = -(is_be - one), -(start_down - one) + va_f_or_ef = [va[0]] + [va[k] * is_ee for k in range(1, 5)] + comp_tail = [comp_down[k] * not_start_down for k in range(5)] + va_times_vb = _quintic_mul_ef(va_f_or_ef, vb) - # Constraints 7-11: add + # Constraints 7-11: add. for k in range(5): folder.assert_zero((comp[k] - (va_f_or_ef[k] + vb[k] + comp_tail[k])) * flag_add) - - va_times_vb = _quintic_mul_ef(va_f_or_ef, vb) - - # Constraints 12-16: mul + # Constraints 12-16: mul. for k in range(5): folder.assert_zero((comp[k] - (va_times_vb[k] + comp_tail[k])) * flag_mul) - # Constraints 17-21: poly_eq - poly_eq_val = [] - for k in range(5): - base = va_times_vb[k] + va_times_vb[k] - va_f_or_ef[k] - vb[k] - poly_eq_val.append(base + one if k == 0 else base) - comp_down_or_one = [] - for k in range(5): - if k == 0: - comp_down_or_one.append(comp_down[0] * not_start_down + start_down) - else: - comp_down_or_one.append(comp_down[k] * not_start_down) + # Constraints 17-21: poly_eq gate — `comp ← (2·va·vb − va − vb + 1) · comp_down_or_one`. + poly_eq_val = [va_times_vb[k] + va_times_vb[k] - va_f_or_ef[k] - vb[k] + (one if k == 0 else EF.zero()) for k in range(5)] + comp_down_or_one = [comp_down[0] * not_start_down + start_down] + [comp_down[k] * not_start_down for k in range(1, 5)] poly_eq_result = _quintic_mul_ef(poly_eq_val, comp_down_or_one) for k in range(5): folder.assert_zero((comp[k] - poly_eq_result[k]) * flag_poly_eq) - # Constraints 22-26: result matches comp when start + # Constraints 22-26: result matches `comp` when `start`. for k in range(5): folder.assert_zero((comp[k] - vres[k]) * start) @@ -1693,242 +1369,163 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat # ─── Poseidon16-compress AIR (lean_vm/src/tables/poseidon_16/mod.rs) ────────────────────────────────────────────────────────── -def _ef_cube(x: EF) -> EF: - return x * x * x - - @functools.cache def _p1c() -> dict: """Poseidon1 round constants + matrices, lifted from the Rust-dumped JSON.""" import json from pathlib import Path raw = json.loads(Path(__file__).with_name("poseidon1_constants.json").read_text()) - mat = lambda m: [[Fp(v) for v in row] for row in m] - vec = lambda v: [Fp(x) for x in v] + fp_mat = lambda m: [[Fp(v) for v in row] for row in m] + fp_vec = lambda v: [Fp(x) for x in v] return { "half_full_rounds": raw["half_full_rounds"], "partial_rounds": raw["partial_rounds"], - "initial_constants": mat(raw["initial_constants"]), - "final_constants": mat(raw["final_constants"]), - "sparse_m_i": mat(raw["sparse_m_i"]), - "sparse_first_row": mat(raw["sparse_first_row"]), - "sparse_v": mat(raw["sparse_v"]), - "sparse_first_rc": vec(raw["sparse_first_round_constants"]), - "sparse_scalar_rc": vec(raw["sparse_scalar_round_constants"]), - "mds_dense": mat(raw["mds_dense"]), + "initial_constants": fp_mat(raw["initial_constants"]), + "final_constants": fp_mat(raw["final_constants"]), + "sparse_m_i": fp_mat(raw["sparse_m_i"]), + "sparse_first_row": fp_mat(raw["sparse_first_row"]), + "sparse_v": fp_mat(raw["sparse_v"]), + "sparse_first_rc": fp_vec(raw["sparse_first_round_constants"]), + "sparse_scalar_rc": fp_vec(raw["sparse_scalar_round_constants"]), + "mds_dense": fp_mat(raw["mds_dense"]), } _POSEIDON_WIDTH = 16 _HALF_DIGEST_LEN = 4 -_POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1 # = 2 -_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT = 1 << 2 # = 4 -_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = 1 << 3 # = 8 +_POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1 +_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT = 1 << 2 +_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = 1 << 3 -def _mds_dense_apply(state: list[EF]) -> list[EF]: - """state := mds_dense × state (dense MDS matrix multiplication).""" - mds = _p1c()["mds_dense"] +def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: + """`mat × state` — base-field matrix times EF-vector.""" out: list[EF] = [] - for i in range(_POSEIDON_WIDTH): + for row in mat: acc = EF.zero() - for j in range(_POSEIDON_WIDTH): - acc = acc + state[j] * mds[i][j] + for s, m in zip(state, row): + acc = acc + s * m out.append(acc) return out -def _add_kb_vec(state: list[EF], rc: list[Fp]) -> list[EF]: - return [s + EF.from_base(r) for s, r in zip(state, rc)] - - -def _cube_vec(state: list[EF]) -> list[EF]: - return [_ef_cube(s) for s in state] - - -def _eval_2_full_rounds( - folder: ConstraintFolder, - state: list[EF], - post_full_round: Sequence[EF], - rc1: list[Fp], - rc2: list[Fp], -) -> list[EF]: - state = _cube_vec(_add_kb_vec(state, rc1)) - state = _mds_dense_apply(state) - state = _cube_vec(_add_kb_vec(state, rc2)) - state = _mds_dense_apply(state) - for i in range(_POSEIDON_WIDTH): - folder.assert_eq(state[i], post_full_round[i]) - state[i] = post_full_round[i] +def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: + """Two MDS-sandwiched cubic S-boxes — one "full round" pair.""" + for rc in (rc1, rc2): + state = _matvec_kb(_p1c()["mds_dense"], [(s + EF.from_base(c)) * (s + EF.from_base(c)) * (s + EF.from_base(c)) for s, c in zip(state, rc)]) return state -def _eval_last_2_full_rounds( - folder: ConstraintFolder, - initial_state: Sequence[EF], - state: list[EF], - outputs: Sequence[EF], - rc1: list[Fp], - rc2: list[Fp], - flag_half_output: EF, -) -> None: - state = _cube_vec(_add_kb_vec(state, rc1)) - state = _mds_dense_apply(state) - state = _cube_vec(_add_kb_vec(state, rc2)) - state = _mds_dense_apply(state) - # Davies-Meyer: state += initial_state. - state = [s + init for s, init in zip(state, initial_state)] - one_minus_half = EF.one() - flag_half_output - for idx in range(_POSEIDON_WIDTH // 2): - if idx < _HALF_DIGEST_LEN: - folder.assert_eq(state[idx], outputs[idx]) - else: - folder.assert_zero(one_minus_half * (state[idx] - outputs[idx])) - - def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) -> None: + """Evaluate the Poseidon1-16 permutation as AIR constraints. Each "post" + column commits an intermediate state; we assert local computation matches, + then *adopt* the committed value to bound polynomial degree.""" const = _p1c() state = list(cols["inputs"]) - initial_state = list(cols["inputs"]) # used for compression at the end + initial = list(cols["inputs"]) # used for Davies-Meyer at the end - # --- initial full rounds (HALF_INITIAL_FULL_ROUNDS = 2) --- + # Initial full rounds (HALF_INITIAL_FULL_ROUNDS = 2 pairs each). half_initial = const["half_full_rounds"] // 2 - initial_consts = const["initial_constants"] for r in range(half_initial): - state = _eval_2_full_rounds( - folder, state, cols["beginning_full_rounds"][r], - initial_consts[2 * r], initial_consts[2 * r + 1], - ) + state = _full_round(state, const["initial_constants"][2 * r], const["initial_constants"][2 * r + 1]) + for i, post in enumerate(cols["beginning_full_rounds"][r]): + folder.assert_eq(state[i], post) + state[i] = post - # --- transition into partial rounds (no constraints emitted here) --- - # Rust uses the sparse `m_i` matrix, NOT the dense MDS. - state = _add_kb_vec(state, const["sparse_first_rc"]) + # Transition into partial rounds (uses sparse `m_i`, no constraints). + state = [s + EF.from_base(c) for s, c in zip(state, const["sparse_first_rc"])] state = _matvec_kb(const["sparse_m_i"], state) - first_rows = const["sparse_first_row"] - v_vecs = const["sparse_v"] - scalar_rc = const["sparse_scalar_rc"] + # Partial rounds: S-box on `state[0]` only, then sparse linear layer. n_partial = const["partial_rounds"] for r in range(n_partial): - # S-box on state[0]; the cubed value is committed in `partial_rounds[r]`. - cubed = _ef_cube(state[0]) - folder.assert_eq(cubed, cols["partial_rounds"][r]) # assert_eq_low ≡ assert_eq + cubed = state[0] * state[0] * state[0] + folder.assert_eq(cubed, cols["partial_rounds"][r]) state[0] = cols["partial_rounds"][r] if r < n_partial - 1: - state[0] = state[0] + scalar_rc[r] - # Sparse mat: new_s0 = first_row[r] · state; state[i] += old_s0 * v[r][i-1]. + state[0] = state[0] + const["sparse_scalar_rc"][r] old_s0 = state[0] + # new_s0 = ⟨first_row[r], state⟩ ; state[i] += old_s0 · v[r][i-1]. new_s0 = EF.zero() for j in range(_POSEIDON_WIDTH): - new_s0 = new_s0 + state[j] * first_rows[r][j] + new_s0 = new_s0 + state[j] * const["sparse_first_row"][r][j] state[0] = new_s0 for i in range(1, _POSEIDON_WIDTH): - state[i] = state[i] + old_s0 * v_vecs[r][i - 1] + state[i] = state[i] + old_s0 * const["sparse_v"][r][i - 1] - # --- ending full rounds (HALF_FINAL_FULL_ROUNDS - 1 = 1) --- + # Ending full rounds except the very last pair. half_final = const["half_full_rounds"] // 2 - final_consts = const["final_constants"] for r in range(half_final - 1): - state = _eval_2_full_rounds( - folder, state, cols["ending_full_rounds"][r], - final_consts[2 * r], final_consts[2 * r + 1], - ) - - # --- last 2 full rounds (8 constraints) --- - last_idx = 2 * (half_final - 1) - _eval_last_2_full_rounds( - folder, initial_state, state, cols["outputs"], - final_consts[last_idx], final_consts[last_idx + 1], - cols["flag_half_output"], - ) - - -def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: - """16x16 base-field matrix · EF-vector.""" - out = [] - for i in range(_POSEIDON_WIDTH): - acc = EF.zero() - for j in range(_POSEIDON_WIDTH): - acc = acc + state[j] * mat[i][j] - out.append(acc) - return out + state = _full_round(state, const["final_constants"][2 * r], const["final_constants"][2 * r + 1]) + for i, post in enumerate(cols["ending_full_rounds"][r]): + folder.assert_eq(state[i], post) + state[i] = post + + # Last full round + Davies-Meyer: state += initial_state, then compare to + # outputs (always for the first half; gated by `flag_half_output` after). + last = 2 * (half_final - 1) + state = _full_round(state, const["final_constants"][last], const["final_constants"][last + 1]) + state = [s + init for s, init in zip(state, initial)] + one_minus_half = EF.one() - cols["flag_half_output"] + for idx in range(_POSEIDON_WIDTH // 2): + if idx < _HALF_DIGEST_LEN: + folder.assert_eq(state[idx], cols["outputs"][idx]) + else: + folder.assert_zero(one_minus_half * (state[idx] - cols["outputs"][idx])) def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: + const = _p1c() up = folder.up one = EF.one() - const = _p1c() - - # Decode the Poseidon1Cols16 layout. - o = 0 - flag_active = up[o]; o += 1 - index_b = up[o]; o += 1 - index_res = up[o]; o += 1 - flag_half_output = up[o]; o += 1 - flag_hardcoded_left = up[o]; o += 1 - offset_hardcoded_left = up[o]; o += 1 - effective_index_left_first = up[o]; o += 1 - effective_index_left_second = up[o]; o += 1 - inputs = up[o : o + _POSEIDON_WIDTH]; o += _POSEIDON_WIDTH + W = _POSEIDON_WIDTH half_initial = const["half_full_rounds"] // 2 - beginning_full_rounds = [] - for _ in range(half_initial): - beginning_full_rounds.append(up[o : o + _POSEIDON_WIDTH]) - o += _POSEIDON_WIDTH - partial_cols = up[o : o + const["partial_rounds"]]; o += const["partial_rounds"] - half_final = const["half_full_rounds"] // 2 - ending_full_rounds = [] - for _ in range(half_final - 1): - ending_full_rounds.append(up[o : o + _POSEIDON_WIDTH]) - o += _POSEIDON_WIDTH - outputs = up[o : o + _POSEIDON_WIDTH // 2]; o += _POSEIDON_WIDTH // 2 + half_final = const["half_full_rounds"] // 2 - precompile_data_reconstructed = ( + # Decode the Poseidon1Cols16 layout by sequential slicing. + o = 0 + def take(n: int) -> list[EF]: + nonlocal o + chunk, o = up[o : o + n], o + n + return list(chunk) + + [flag_active, index_b, index_res, flag_half_output, flag_hardcoded_left, + offset_hardcoded_left, effective_index_left_first, effective_index_left_second] = take(8) + inputs = take(W) + beginning_full_rounds = [take(W) for _ in range(half_initial)] + partial_cols = take(const["partial_rounds"]) + ending_full_rounds = [take(W) for _ in range(half_final - 1)] + outputs = take(W // 2) + + # Reconstruct `precompile_data` from the flags + offset. + precompile_data = ( one - + flag_half_output * EF.from_base(Fp(_POSEIDON_HALF_OUTPUT_SHIFT)) - + flag_hardcoded_left * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT)) - + flag_hardcoded_left - * offset_hardcoded_left - * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT)) - ) - - one_minus_flag_hardcoded_left = one - flag_hardcoded_left - index_a = effective_index_left_second - one_minus_flag_hardcoded_left * EF.from_base( - Fp(_HALF_DIGEST_LEN) - ) - - # Constraint 1: bus - folder.assert_zero( - _eval_virtual_bus_column( - extra_data, flag_active, [precompile_data_reconstructed, index_a, index_b, index_res] - ) - ) - - # Constraints 2-4: bool flags - folder.assert_bool(flag_active) - folder.assert_bool(flag_half_output) - folder.assert_bool(flag_hardcoded_left) - - # Constraints 5-6: hardcoded-left consistency - folder.assert_zero( - flag_hardcoded_left * (offset_hardcoded_left - effective_index_left_first) - ) - folder.assert_zero( - one_minus_flag_hardcoded_left * (index_a - effective_index_left_first) - ) - - _eval_poseidon1_16( - folder, - { - "inputs": list(inputs), - "beginning_full_rounds": [list(r) for r in beginning_full_rounds], - "partial_rounds": list(partial_cols), - "ending_full_rounds": [list(r) for r in ending_full_rounds], - "outputs": list(outputs), - "flag_half_output": flag_half_output, - }, - extra_data, + + flag_half_output * EF.from_base(Fp(_POSEIDON_HALF_OUTPUT_SHIFT)) + + flag_hardcoded_left * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT)) + + flag_hardcoded_left * offset_hardcoded_left * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT)) ) + not_hcl = one - flag_hardcoded_left + index_a = effective_index_left_second - not_hcl * EF.from_base(Fp(_HALF_DIGEST_LEN)) + + # Constraint 1: bus column. + folder.assert_zero(_eval_virtual_bus_column( + extra_data, flag_active, [precompile_data, index_a, index_b, index_res], + )) + # Constraints 2-4: bool flags. + for f in (flag_active, flag_half_output, flag_hardcoded_left): + folder.assert_bool(f) + # Constraints 5-6: hardcoded-left consistency. + folder.assert_zero(flag_hardcoded_left * (offset_hardcoded_left - effective_index_left_first)) + folder.assert_zero(not_hcl * (index_a - effective_index_left_first)) + + _eval_poseidon1_16(folder, { + "inputs": inputs, + "beginning_full_rounds": beginning_full_rounds, + "partial_rounds": partial_cols, + "ending_full_rounds": ending_full_rounds, + "outputs": outputs, + "flag_half_output": flag_half_output, + }, extra_data) @@ -1945,6 +1542,15 @@ def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: } +def _powers(x: EF, n: int) -> list[EF]: + """`[1, x, x², …, x^(n−1)]`.""" + out, cur = [], EF.one() + for _ in range(n): + out.append(cur) + cur = cur * x + return out + + def verify_air_stage( state: VerifierState, logup: GenericLogupStatements, @@ -1956,98 +1562,58 @@ def verify_air_stage( log_memory: int, ) -> tuple[dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]], list[EF], EF]: """Returns `(committed_statements, public_memory_random_point, public_memory_eval)`. - - `committed_statements[name]` is a list of (point, eq_values, next_values) - triples — one per session for that table. - """ - bus_beta = state.sample() - air_alpha = state.sample() - - max_n_constraints = max(_TABLE_SPECS[name]["n_constraints"] for name in tables) - alpha_powers: list[EF] = [] - cur = EF.one() - for _ in range(max_n_constraints + 1): - alpha_powers.append(cur) - cur = cur * air_alpha - - eta = state.sample() - + `committed_statements[name]` is a list of `(point, eq_values, next_values)` + triples — one per session for that table.""" + bus_beta, air_alpha, eta = state.sample(), state.sample(), state.sample() + alpha_powers = _powers(air_alpha, max(_TABLE_SPECS[n]["n_constraints"] for n in tables) + 1) tables_sorted = sort_tables_by_height(table_log_heights) + eta_powers = _powers(eta, len(tables_sorted)) + extra_data = {"logup_alphas_eq_poly": logup_alphas_eq_poly, "bus_beta": bus_beta, "c": logup_c} - # Build initial_sum. + # Initial AIR sum: Σ η^t · (bus_num · sign + β · (bus_den − c)). initial_sum = EF.zero() - eta_powers: list[EF] = [] - cur = EF.one() - for name, _ in tables_sorted: - bus_num = logup.bus_numerators_values[name] - bus_den = logup.bus_denominators_values[name] - flag = ( - EF.zero() - EF.one() - if tables[name].bus_direction == "Pull" - else EF.one() - ) - bus_final_value = bus_num * flag + bus_beta * (bus_den - logup_c) - initial_sum = initial_sum + cur * bus_final_value - eta_powers.append(cur) - cur = cur * eta + for (name, _), eta_pow in zip(tables_sorted, eta_powers): + sign = -EF.one() if tables[name].bus_direction == "Pull" else EF.one() + bus_value = logup.bus_numerators_values[name] * sign + bus_beta * (logup.bus_denominators_values[name] - logup_c) + initial_sum = initial_sum + eta_pow * bus_value - max_full_degree = max(_TABLE_SPECS[name]["degree"] + 1 for name, _ in tables_sorted) + max_degree_plus_one = max(_TABLE_SPECS[n]["degree"] + 1 for n, _ in tables_sorted) n_max = tables_sorted[0][1] - - sumcheck_result = verify_sumcheck(state, initial_sum, n_max, max_full_degree) - sumcheck_air_point = sumcheck_result.point - claimed_air_final_value = sumcheck_result.value - - # Per-table loop: read col_evals, evaluate AIR, accumulate, build claims. - my_air_final_value = EF.zero() - - # Seed committed_statements with the logup entry per table, mirroring the - # init loop in `verify_execution.rs` (lines 88-98). - committed: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]] = {} - for name in tables: - log_n = table_log_heights[name] - logup_point = from_end(logup.gkr_point, log_n) - committed[name] = [ - (list(logup_point), dict(logup.columns_values[name]), {}), - ] - extra_data = { - "logup_alphas_eq_poly": logup_alphas_eq_poly, - "bus_beta": bus_beta, - "c": logup_c, + sc = verify_sumcheck(state, initial_sum, n_max, max_degree_plus_one) + + # Per-table: read col_evals, evaluate the AIR constraint, accumulate, build claims. + # Each table's committed statements start with its logup eq-values entry. + committed = { + name: [(list(from_end(logup.gkr_point, table_log_heights[name])), + dict(logup.columns_values[name]), {})] + for name in tables } + my_air_final_value = EF.zero() for (name, log_n_rows), eta_pow in zip(tables_sorted, eta_powers): - meta = tables[name] - down_indexes = _TABLE_SPECS[name]["down"] - col_evals = state.next_extension_scalars_vec(meta.n_columns + len(down_indexes)) + meta, down = tables[name], _TABLE_SPECS[name]["down"] + col_evals = state.next_extension_scalars_vec(meta.n_columns + len(down)) constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) # Per-table contribution = η^t · (Π unused-prefix coords) · eq · C(col_evals). - bus_point = from_end(logup.gkr_point, log_n_rows) - natural_pt = list(reversed(sumcheck_air_point[-log_n_rows:])) if log_n_rows else [] + natural_pt = list(reversed(sc.point[-log_n_rows:])) if log_n_rows else [] k_t = EF.one() - for x in sumcheck_air_point[: n_max - log_n_rows]: + for x in sc.point[: n_max - log_n_rows]: k_t = k_t * x - my_air_final_value = my_air_final_value + ( - eta_pow * k_t * eq_poly_outside(bus_point, natural_pt) * constraint_eval - ) + bus_point = from_end(logup.gkr_point, log_n_rows) + my_air_final_value = my_air_final_value + eta_pow * k_t * eq_poly_outside(bus_point, natural_pt) * constraint_eval - # Split col_evals into the per-column eq-values + the next-row evals. eq_values = {i: col_evals[i] for i in range(meta.n_columns)} - next_values = {idx: col_evals[meta.n_columns + j] for j, idx in enumerate(down_indexes)} + next_values = {idx: col_evals[meta.n_columns + j] for j, idx in enumerate(down)} committed[name].append((natural_pt, eq_values, next_values)) - if my_air_final_value != claimed_air_final_value: + if my_air_final_value != sc.value: raise ProofError("AIR sumcheck: my_air_final_value != claimed_air_final_value") - # Public memory evaluation (length is next power of two of public_input). + # Public memory MLE evaluation at a fresh random point. public_memory = padd_with_zero_to_next_power_of_two(public_input) - log_pm = log2_strict_usize(len(public_memory)) - public_memory_random_point = state.sample_vec(log_pm) - public_memory_eval = eval_multilinear_evals( - [EF.from_base(f) for f in public_memory], public_memory_random_point - ) - - return committed, list(public_memory_random_point), public_memory_eval + pm_point = state.sample_vec(log2_strict_usize(len(public_memory))) + pm_eval = eval_multilinear_evals([EF.from_base(f) for f in public_memory], pm_point) + return committed, list(pm_point), pm_eval @@ -2064,180 +1630,123 @@ def verify_execution( ) -> dict: """Verify a leanVM execution proof. Port of `verify_execution.rs`. - The flow is: - 1. observe public input + SNARK domain separator - 2. read dims, validate bounds - 3. parse stacked-PCS WHIR commitment (root + OOD) - 4. verify generic logup (GKR + sum reconstruction) - 5. AIR sumcheck across all tables + per-table constraint evaluation - 6. assemble global WHIR statements and run the final WHIR verify - - `tables` must be in canonical Rust order (= `ALL_TABLES`): execution, - extension_op, poseidon16. `constants` and `bytecode_multilinear` come from - the Rust dump. - """ - + Flow: observe prologue → read dims → parse stacked-PCS WHIR commitment → + generic logup → AIR sumcheck → assemble global WHIR statements → final WHIR. + `tables` must be in canonical Rust order (`ALL_TABLES`).""" state = VerifierState(proof) state.observe_scalars(list(public_input)) state.observe_scalars(poseidon16_compress(bytecode.hash, SNARK_DOMAIN_SEP)) - n_tables = len(tables) - dims = [int(x.value) for x in state.next_base_scalars_vec(3 + n_tables)] - log_inv_rate, log_memory, public_input_len = dims[0], dims[1], dims[2] - table_log_n_rows = dims[3 : 3 + n_tables] + # Dimensions: log_inv_rate, log_memory, public_input_len, then per-table log_n_rows. + dims = [int(x.value) for x in state.next_base_scalars_vec(3 + len(tables))] + log_inv_rate, log_memory, public_input_len, *table_log_n_rows = dims if public_input_len != len(public_input): raise ProofError("InvalidProof: public_input length mismatch") - - if not (MIN_WHIR_LOG_INV_RATE <= log_inv_rate <= MAX_WHIR_LOG_INV_RATE): + if not MIN_WHIR_LOG_INV_RATE <= log_inv_rate <= MAX_WHIR_LOG_INV_RATE: raise ProofError("InvalidRate") - - for log_n_rows in table_log_n_rows: - if log_n_rows < MIN_LOG_N_ROWS_PER_TABLE: - raise ProofError("InvalidProof: table too small") - - max_table_log = max(table_log_n_rows) if table_log_n_rows else 0 - if log_memory < max(max_table_log, bytecode.log_size): + if any(h < MIN_LOG_N_ROWS_PER_TABLE for h in table_log_n_rows): + raise ProofError("InvalidProof: table too small") + if log_memory < max(max(table_log_n_rows, default=0), bytecode.log_size): raise ProofError("InvalidProof: memory smaller than tables/bytecode") - - if not (MIN_LOG_MEMORY_SIZE <= log_memory <= MAX_LOG_MEMORY_SIZE): + if not MIN_LOG_MEMORY_SIZE <= log_memory <= MAX_LOG_MEMORY_SIZE: raise ProofError("InvalidProof: log_memory out of range") - if bytecode.log_size < MIN_BYTECODE_LOG_SIZE: raise ProofError("InvalidProof: bytecode too small") - table_log_heights = {t.name: log_n_rows for t, log_n_rows in zip(tables, table_log_n_rows)} - table_n_columns = {t.name: t.n_columns for t in tables} - tables_by_name = {t.name: t for t in tables} + table_log_heights = {t.name: h for t, h in zip(tables, table_log_n_rows)} + table_n_columns = {t.name: t.n_columns for t in tables} + tables_by_name = {t.name: t for t in tables} parsed_commitment = stacked_pcs_parse_commitment( - state, - log_inv_rate=log_inv_rate, - log_memory=log_memory, - log_bytecode=bytecode.log_size, - table_log_heights=table_log_heights, - table_n_columns=table_n_columns, + state, log_inv_rate, log_memory, bytecode.log_size, table_log_heights, table_n_columns, ) - # Logup challenges. + # Logup phase. logup_c = state.sample() - max_bus_width = 1 + max( - constants["max_precompile_bus_width"], constants["n_instruction_columns"] - ) + max_bus_width = 1 + max(constants["max_precompile_bus_width"], constants["n_instruction_columns"]) logup_alphas = state.sample_vec(log2_ceil_usize(max_bus_width)) - logup_alphas_eq_poly = eval_eq(logup_alphas) - - logup_statements = verify_generic_logup( - state, - logup_c, - logup_alphas, - logup_alphas_eq_poly, - log_memory, - bytecode_multilinear, - table_log_heights, - tables_by_name, - constants, + logup_alphas_eq = eval_eq(logup_alphas) + logup = verify_generic_logup( + state, logup_c, logup_alphas, logup_alphas_eq, log_memory, bytecode_multilinear, + table_log_heights, tables_by_name, constants, ) + # AIR phase. committed, pm_point, pm_eval = verify_air_stage( - state, logup_statements, logup_c, logup_alphas_eq_poly, + state, logup, logup_c, logup_alphas_eq, table_log_heights, tables_by_name, public_input, log_memory, ) - # Build the global WHIR statement list. Three "previous" statements seed it - # (memory + memory_acc, public memory, bytecode acc), then `stacked_pcs_…` - # appends the per-table committed claims. + # WHIR finale: seed previous_statements with memory, public-memory, bytecode-acc. stacked_n_vars = parsed_commitment.num_variables mk = lambda point, values: SparseStatement(stacked_n_vars, list(point), values) - previous_statements = [ - mk(logup_statements.memory_and_acc_point, [ - SparseValue(0, logup_statements.value_memory), - SparseValue(1, logup_statements.value_memory_acc), + previous = [ + mk(logup.memory_and_acc_point, [ + SparseValue(0, logup.value_memory), + SparseValue(1, logup.value_memory_acc), ]), mk(pm_point, [SparseValue(0, pm_eval)]), - mk(logup_statements.bytecode_and_acc_point, [SparseValue( - (2 << log_memory) >> bytecode.log_size, - logup_statements.value_bytecode_acc, + mk(logup.bytecode_and_acc_point, [SparseValue( + (2 << log_memory) >> bytecode.log_size, logup.value_bytecode_acc, )]), ] - global_statements = stacked_pcs_global_statements( - stacked_n_vars, log_memory, bytecode.log_size, previous_statements, + stacked_n_vars, log_memory, bytecode.log_size, previous, table_log_heights, committed, tables_by_name, constants, ) + whir_verify(state, whir_config(log_inv_rate, stacked_n_vars), parsed_commitment, global_statements) - whir_cfg = whir_config(log_inv_rate, stacked_n_vars) - whir_verify(state, whir_cfg, parsed_commitment, global_statements) - - return { - "log_inv_rate": log_inv_rate, - "log_memory": log_memory, - "stacked_n_vars": stacked_n_vars, - } + return {"log_inv_rate": log_inv_rate, "log_memory": log_memory, "stacked_n_vars": stacked_n_vars} # ─── Test vector loader + entry point ────────────────────────────────────────────────────────── -def poseidon_compress_slice(data: Sequence[Fp], use_iv: bool) -> list[Fp]: - """Hash a length-multiple-of-8 sequence into one 8-element digest using - Poseidon16 in Davies-Meyer compression mode. If `use_iv` is false the - first 16 elements seed the sponge directly; if true an all-zero IV is used. - """ +def poseidon_compress_slice_iv(data: Sequence[Fp]) -> list[Fp]: + """Hash a multiple-of-8 sequence with Poseidon16/Davies-Meyer, seeded by + an all-zero IV — matches `utils::poseidon_compress_slice(.., use_iv=true)`.""" assert data and len(data) % 8 == 0 - if use_iv: - h = [Fp(0)] * 8 - for i in range(0, len(data), 8): - h = poseidon16_compress(h, list(data[i : i + 8])) - return h - if len(data) <= 16: - padded = list(data) + [Fp(0)] * (16 - len(data)) - return poseidon16_compress_in_place(padded)[:8] - h = poseidon16_compress_in_place(list(data[:16]))[:8] - for i in range(16, len(data), 8): + h = [Fp(0)] * 8 + for i in range(0, len(data), 8): h = poseidon16_compress(h, list(data[i : i + 8])) return h def main() -> int: """Load the end-to-end test vector and run `verify_execution`.""" - import array - import json + import array, json from pathlib import Path vector_path = Path(__file__).resolve().parents[2] / "target" / "zkvm_test_vectors" / "proof.json" if not vector_path.exists(): - print( - f"Test vector not found at {vector_path}. Generate it first with:\n" - " cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture" - ) + print(f"Test vector not found at {vector_path}. Generate it first with:\n" + " cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture") return 1 print(f"Loading {vector_path.name}...") raw = json.loads(vector_path.read_text()) # Bytecode multilinear (raw u32 LE sidecar). - mle_blob = (vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes() - arr = array.array("I"); arr.frombytes(mle_blob) + arr = array.array("I"); arr.frombytes((vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes()) assert len(arr) == raw["bytecode_multilinear_len"] bytecode_multilinear: list[int] = list(arr) - bytecode = Bytecode([Fp(v) for v in raw["bytecode_hash"]], raw["bytecode_log_size"]) - public_input = [Fp(v) for v in raw["public_input"]] - input_data = [Fp(v) for v in raw["input_data"]] - - openings = [ - MerkleOpening( - leaf_data=[Fp(v) for v in o["leaf_data"]], - path=[[Fp(v) for v in d] for d in o["path"]], - ) - for o in raw["proof"]["merkle_openings"] - ] - proof = Proof(transcript=[Fp(v) for v in raw["proof"]["transcript"]], merkle_openings=openings) + fp_list = lambda xs: [Fp(v) for v in xs] + bytecode = Bytecode(fp_list(raw["bytecode_hash"]), raw["bytecode_log_size"]) + public_input = fp_list(raw["public_input"]) + input_data = fp_list(raw["input_data"]) + proof = Proof( + transcript=fp_list(raw["proof"]["transcript"]), + merkle_openings=[ + MerkleOpening(leaf_data=fp_list(o["leaf_data"]), path=[fp_list(d) for d in o["path"]]) + for o in raw["proof"]["merkle_openings"] + ], + ) # Sanity: re-derive `public_input` from `input_data` to check the hash. - if poseidon_compress_slice(input_data, use_iv=True) != public_input: + if poseidon_compress_slice_iv(input_data) != public_input: print("FAIL: poseidon_compress_slice(input_data) doesn't match dumped public_input") return 1 From e4bc75aab0eecc34ccd3c9bb5831fe87a9092044 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 18 May 2026 10:23:54 +0200 Subject: [PATCH 11/69] wip --- crates/lean_prover/verifier.py | 212 +++++++++------------------------ 1 file changed, 56 insertions(+), 156 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 6f198a042..688569356 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -65,20 +65,14 @@ WHIR_CONFIGS_PATH = "whir_configs.json" - -# ─── Error type + quintic extension field ────────────────────────────────────────────────────────── - - class ProofError(Exception): - """Mirrors backend::ProofError.""" - + pass # Quintic extension field: EF = Fp[X] / (X^5 + X^2 - 1) # Reduction rule: X^5 = 1 - X^2. - class EF: """Quintic extension `Fp[X] / (X⁵ + X² − 1)`. Stored as 5 base coefficients; multiplication reduces with `X⁵ ≡ 1 − X²`.""" @@ -135,10 +129,6 @@ def inv(self) -> "EF": return result - -# ─── Poseidon16-based Challenger ────────────────────────────────────────────────────────── - - _POSEIDON16 = Poseidon1(PARAMS_16) @@ -204,34 +194,20 @@ def sample_in_range(self, bits: int, n_samples: int) -> list[int]: return [int(x.value) & ((1 << bits) - 1) for x in flat] - -# ─── Proof container + VerifierState (transcript reader) ────────────────────────────────────────────────────────── - - @dataclass class MerkleOpening: - """Restored Merkle opening: matches fiat_shamir::transcript::MerkleOpening.""" - leaf_data: list[Fp] - path: list[list[Fp]] # each sibling is a length-DIGEST_ELEMS digest + path: list[list[Fp]] @dataclass class Proof: - """Mirrors `backend::RawProof`. `transcript` is the flat raw transcript - (every absorbed group padded to a multiple of RATE with zeros — the format - the zkDSL recursion verifier reads). `merkle_openings` is the list of - already-restored openings in consumption order.""" - transcript: list[Fp] merkle_openings: list[MerkleOpening] class VerifierState: - """Drives the Fiat-Shamir transcript: reads scalars from `proof.transcript`, - samples challenges from the challenger, yields restored Merkle openings. - - Every read pads to RATE — `n` real scalars are consumed as + """Reads from the raw transcript: every `n` real scalars are consumed as `next_multiple_of(n, RATE)` raw scalars (trailing positions must be zero), and the full RATE-aligned chunk is what the challenger absorbs.""" @@ -282,13 +258,8 @@ def check_pow_grinding(self, bits: int) -> None: raise ProofError("InvalidGrindingWitness") - -# ─── Small helpers (Bytecode metadata, EF utilities, log2, padding) ────────────────────────────────────────────── - - @dataclass class Bytecode: - """What `verify_execution` needs about a bytecode program.""" hash: list[Fp] log_size: int @@ -313,9 +284,6 @@ def padd_with_zero_to_next_power_of_two(values: Sequence[Fp]) -> list[Fp]: return list(values) + [Fp(0)] * (n - len(values)) -# ─── Merkle path verify ────────────────────────────────── - - def merkle_verify_path( commit: list[Fp], log_height: int, @@ -323,7 +291,6 @@ def merkle_verify_path( opened_values: Sequence[Fp], opening_proof: Sequence[list[Fp]], ) -> bool: - """Hash the leaf, walk up `log_height` siblings, compare to the commitment.""" if len(opening_proof) != log_height: return False cur = hash_slice(list(opened_values)) @@ -333,10 +300,6 @@ def merkle_verify_path( return list(commit) == list(cur) - -# ─── WHIR polynomial primitives (poly + whir crates) ────────────────────────────────────────────────────────── - - def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: """`[x, x², x⁴, …, x^(2^(n−1))]` — `MultilinearPoint::expand_from_univariate`.""" out, cur = [], x @@ -479,10 +442,6 @@ def new_next(total: int, point: list[EF], values: list[SparseValue]) -> "SparseS return SparseStatement(total, point, values, is_next=True) - -# ─── WHIR config helpers: derive integer-only parameters from the trimmed JSON ────────────────────────────────────────────────────────── - - def whir_folding_factor_at_round(r: int) -> int: return WHIR_INITIAL_FOLDING_FACTOR if r == 0 else WHIR_SUBSEQUENT_FOLDING_FACTOR @@ -562,10 +521,6 @@ def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: ) - -# ─── WHIR verifier (port of crates/whir/src/verify.rs) ────────────────────────────────────────────────────────── - - @dataclass class ParsedCommitment: num_variables: int @@ -589,13 +544,11 @@ def parsed_commitment_parse(state: VerifierState, num_variables: int, ood_sample @dataclass class Evaluation: - """Claim that a multilinear evaluates to `value` at `point`.""" point: list[EF] value: EF def _eval_univariate(coeffs: list[EF], x: EF) -> EF: - """Horner: c[0] + c[1]*x + c[2]*x^2 + ...""" acc = EF.zero() for c in reversed(coeffs): acc = acc * x + c @@ -788,19 +741,12 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None return folding_flat - -# ─── Table metadata (mirror of lean_vm::tables::table_trait) ────────────────────────────────────────────────────────── - - @dataclass(frozen=True) class TableMeta: - """The bits of `lean_vm::Table` the verifier consumes. Each lookup is a - `(index_column, value_columns)` pair (mirrors `LookupIntoMemory`).""" - name: str n_columns: int - bus_direction: str # "Pull" or "Push" - lookups: tuple[tuple[int, tuple[int, ...]], ...] + bus_direction: str # "Pull" or "Push" + lookups: tuple[tuple[int, tuple[int, ...]], ...] # (index_col, value_cols) def tables_from_json(obj: list[dict]) -> list[TableMeta]: @@ -815,10 +761,6 @@ def tables_from_json(obj: list[dict]) -> list[TableMeta]: ] - -# ─── Stacked PCS — port of sub_protocols/stacked_pcs.rs ────────────────────────────────────────────────────────── - - def compute_stacked_n_vars( log_memory: int, log_bytecode: int, @@ -896,8 +838,6 @@ def stacked_pcs_parse_commitment( return parsed_commitment_parse(state, stacked_n_vars, cfg.commitment_ood_samples) - -# ─── GKR-quotient verifier (port of `sub_protocols::quotient_gkr`) ────────────────────────────────────────────────────────── # Verifies `Σ nᵢ/dᵢ` via a layered sumcheck. @@ -938,10 +878,6 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] return quotient, point, claim_num, claim_den - -# ─── Logup helpers (utils + poly) ────────────────────────────────────────────────────────── - - def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: """`bit_count` bits of `value` MSB-first, each as `EF::ZERO`/`EF::ONE`.""" return [EF.one() if (value >> (bit_count - 1 - i)) & 1 else EF.zero() for i in range(bit_count)] @@ -995,10 +931,6 @@ def eval_eq(point: Sequence[EF]) -> list[EF]: return out - -# ─── Generic logup verifier — port of sub_protocols/logup.rs::verify_generic_logup ────────────────────────────────────────────────────────── - - @dataclass class GenericLogupStatements: memory_and_acc_point: list[EF] @@ -1155,18 +1087,13 @@ def pref_at(offset: int, log_height: int) -> EF: ) - - - -# ─── Pluggable per-table AIR constraint evaluator ────────────────────────────────────────────────────────── - - class ConstraintFolder: - """Each `assert_zero(x)` contributes `alpha_powers[i] · x` to the - running accumulator. `assert_eq` and `assert_bool` are sugar.""" + """`flat` = current-row column evaluations; `shift` = next-row evaluations + restricted to the table's first `n_shift_columns`. Each `assert_zero(x)` + contributes `alpha_powers[i] · x` to the accumulator.""" - def __init__(self, up: Sequence[EF], down: Sequence[EF], alpha_powers: Sequence[EF]) -> None: - self.up, self.down, self.alpha_powers = list(up), list(down), list(alpha_powers) + def __init__(self, flat: Sequence[EF], shift: Sequence[EF], alpha_powers: Sequence[EF]) -> None: + self.flat, self.shift, self.alpha_powers = list(flat), list(shift), list(alpha_powers) self.accumulator: EF = EF.zero() self.i = 0 @@ -1202,8 +1129,8 @@ def air_constraint_eval( ) -> EF: """Evaluate `table`'s AIR constraint polynomial at the given column evals. - `col_evals[:n_columns]` is the `up` row, `col_evals[n_columns:]` is the - `down` row (only present for tables with `down_column_indexes`). + `col_evals[:n_columns]` is the `flat` row, `col_evals[n_columns:]` is the + `shift` (next-row) view, only present for tables with `n_shift_columns > 0`. """ folder = ConstraintFolder(col_evals[:table.n_columns], col_evals[table.n_columns:], alpha_powers) { @@ -1214,18 +1141,14 @@ def air_constraint_eval( return folder.accumulator - -# ─── Execution-table AIR (lean_vm/src/tables/execution/air.rs) ────────────────────────────────────────────────────────── - - def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: # Column layout (execution/air.rs): pc, fp, addr_{a,b,c}, value_{a,b,c}, # operand_{a,b,c}, flag_{a,b,c}, flag_c_fp, flag_ab_fp, mul, jump, aux, - # precompile_data. `down[0..2]` carries the next row's (pc, fp). + # precompile_data. `shift[0..2]` carries the next row's (pc, fp). (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, - flag_ab_fp, mul, jump, aux, precompile_data) = folder.up[:20] - next_pc, next_fp = folder.down[0], folder.down[1] + flag_ab_fp, mul, jump, aux, precompile_data) = folder.flat[:20] + pc_shift, fp_shift = folder.shift[0], folder.shift[1] one = EF.one() # `nu_x = flag · operand + (1 − flag − flag_ab_fp) · value + flag_ab_fp · (fp + operand)`. @@ -1259,15 +1182,11 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: # Constraints 9-13: jump control flow. jc = jump * nu_a folder.assert_zero(jc * (nu_a - one)) - folder.assert_zero(jc * (next_pc - nu_b)) - folder.assert_zero(jc * (next_fp - nu_c)) + folder.assert_zero(jc * (pc_shift - nu_b)) + folder.assert_zero(jc * (fp_shift - nu_c)) not_jc = -(jc - one) - folder.assert_zero(not_jc * (next_pc - (pc + one))) - folder.assert_zero(not_jc * (next_fp - fp)) - - - -# ─── Extension-op-table AIR (lean_vm/src/tables/extension_op/air.rs) ────────────────────────────────────────────────────────── + folder.assert_zero(not_jc * (pc_shift - (pc + one))) + folder.assert_zero(not_jc * (fp_shift - fp)) # Bus-data magic numbers from extension_op/air.rs (precompile-data layout). @@ -1299,15 +1218,17 @@ def dot(row: list[EF]) -> EF: def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: - # `up`: is_be, start, flag_{add,mul,poly_eq}, len, idx_{a,b,res}, then four - # 5-EF blocks va, vb, vres, comp. `down`: start, is_be, len, flag_*, idx_*, - # comp[0..5] (we only use the next-row `start` and `comp` here). - is_be, start, flag_add, flag_mul, flag_poly_eq, len_col, idx_a, idx_b, idx_res = folder.up[:9] - va, vb, vres, comp = (folder.up[9 + 5 * k : 9 + 5 * (k + 1)] for k in range(4)) - # `down`: start, is_be, len, flag_{add,mul,poly_eq}, idx_{a,b}, comp[0..5]. - (start_down, is_be_down, len_down, flag_add_down, flag_mul_down, - flag_poly_eq_down, idx_a_down, idx_b_down) = folder.down[:8] - comp_down = folder.down[8:13] + # Column layout (extension_op/air.rs): the 13 shift columns + # (is_be, start, len, flag_{add,mul,poly_eq}, idx_{a,b}, comp[0..5]) + # occupy positions 0..13, then idx_res, va, vb, vres (5 each). + is_be, start, len_col, flag_add, flag_mul, flag_poly_eq, idx_a, idx_b = folder.flat[:8] + comp = folder.flat[8:13] + idx_res = folder.flat[13] + va, vb, vres = (folder.flat[14 + 5 * k : 14 + 5 * (k + 1)] for k in range(3)) + # `shift[j]` mirrors `flat[j]` for j ∈ 0..13 (convention). + (is_be_shift, start_shift, len_shift, flag_add_shift, flag_mul_shift, + flag_poly_eq_shift, idx_a_shift, idx_b_shift) = folder.shift[:8] + comp_shift = folder.shift[8:13] one = EF.one() # Constraint 1: precompile bus. @@ -1326,9 +1247,9 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat # `va` is `Fp` when in base-extension mode, full EF otherwise; `comp_tail` # carries the next chunk's comp when this row isn't a `start`. - is_ee, not_start_down = -(is_be - one), -(start_down - one) + is_ee, not_start_shift = -(is_be - one), -(start_shift - one) va_f_or_ef = [va[0]] + [va[k] * is_ee for k in range(1, 5)] - comp_tail = [comp_down[k] * not_start_down for k in range(5)] + comp_tail = [comp_shift[k] * not_start_shift for k in range(5)] va_times_vb = _quintic_mul_ef(va_f_or_ef, vb) # Constraints 7-11: add. @@ -1338,10 +1259,10 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat for k in range(5): folder.assert_zero((comp[k] - (va_times_vb[k] + comp_tail[k])) * flag_mul) - # Constraints 17-21: poly_eq gate — `comp ← (2·va·vb − va − vb + 1) · comp_down_or_one`. + # Constraints 17-21: poly_eq gate — `comp ← (2·va·vb − va − vb + 1) · comp_shift_or_one`. poly_eq_val = [va_times_vb[k] + va_times_vb[k] - va_f_or_ef[k] - vb[k] + (one if k == 0 else EF.zero()) for k in range(5)] - comp_down_or_one = [comp_down[0] * not_start_down + start_down] + [comp_down[k] * not_start_down for k in range(1, 5)] - poly_eq_result = _quintic_mul_ef(poly_eq_val, comp_down_or_one) + comp_shift_or_one = [comp_shift[0] * not_start_shift + start_shift] + [comp_shift[k] * not_start_shift for k in range(1, 5)] + poly_eq_result = _quintic_mul_ef(poly_eq_val, comp_shift_or_one) for k in range(5): folder.assert_zero((comp[k] - poly_eq_result[k]) * flag_poly_eq) @@ -1349,24 +1270,20 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat for k in range(5): folder.assert_zero((comp[k] - vres[k]) * start) - # Constraints 27-31: down-row consistency on non-start rows - folder.assert_zero(not_start_down * (len_col - len_down - one)) - folder.assert_zero(not_start_down * (is_be - is_be_down)) - folder.assert_zero(not_start_down * (flag_add - flag_add_down)) - folder.assert_zero(not_start_down * (flag_mul - flag_mul_down)) - folder.assert_zero(not_start_down * (flag_poly_eq - flag_poly_eq_down)) + # Constraints 27-31: shift-row consistency on non-start rows. + folder.assert_zero(not_start_shift * (len_col - len_shift - one)) + folder.assert_zero(not_start_shift * (is_be - is_be_shift)) + folder.assert_zero(not_start_shift * (flag_add - flag_add_shift)) + folder.assert_zero(not_start_shift * (flag_mul - flag_mul_shift)) + folder.assert_zero(not_start_shift * (flag_poly_eq - flag_poly_eq_shift)) - # Constraint 32-33: idx_a / idx_b increment + # Constraint 32-33: idx_a / idx_b increment. a_increment = is_be + is_ee * EF.from_base(Fp(5)) # DIMENSION = 5 - folder.assert_zero(not_start_down * (idx_a_down - idx_a - a_increment)) - folder.assert_zero(not_start_down * (idx_b_down - idx_b - EF.from_base(Fp(5)))) - - # Constraint 34: start_down enforces len=1 - folder.assert_zero(start_down * (len_col - one)) + folder.assert_zero(not_start_shift * (idx_a_shift - idx_a - a_increment)) + folder.assert_zero(not_start_shift * (idx_b_shift - idx_b - EF.from_base(Fp(5)))) - - -# ─── Poseidon16-compress AIR (lean_vm/src/tables/poseidon_16/mod.rs) ────────────────────────────────────────────────────────── + # Constraint 34: `start_shift` enforces len=1. + folder.assert_zero(start_shift * (len_col - one)) @functools.cache @@ -1476,7 +1393,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) - def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: const = _p1c() - up = folder.up + flat = folder.flat one = EF.one() W = _POSEIDON_WIDTH half_initial = const["half_full_rounds"] // 2 @@ -1486,7 +1403,7 @@ def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: o = 0 def take(n: int) -> list[EF]: nonlocal o - chunk, o = up[o : o + n], o + n + chunk, o = flat[o : o + n], o + n return list(chunk) [flag_active, index_b, index_res, flag_half_output, flag_hardcoded_left, @@ -1528,17 +1445,12 @@ def take(n: int) -> list[EF]: }, extra_data) - -# ─── AIR-stage orchestration in verify_execution ────────────────────────────────────────────────────────── - - # Per-table compile-time spec (Rust: `
::{degree_air, n_constraints, -# down_column_indexes}`). The down-column lists for `execution` and -# `extension_op` are exactly what their `Air::down_column_indexes` returns. +# n_shift_columns}`). By convention the shift columns occupy positions `0..n_shift`. _TABLE_SPECS: dict[str, dict] = { - "execution": {"degree": 5, "n_constraints": 13, "down": [0, 1]}, - "extension_op": {"degree": 4, "n_constraints": 16, "down": [1, 0, 5, 2, 3, 4, 6, 7, 24, 25, 26, 27, 28]}, - "poseidon16_compress": {"degree": 10, "n_constraints": 81, "down": []}, + "execution": {"degree": 5, "n_constraints": 13, "n_shift": 2}, + "extension_op": {"degree": 4, "n_constraints": 16, "n_shift": 13}, + "poseidon16_compress": {"degree": 10, "n_constraints": 81, "n_shift": 0}, } @@ -1590,8 +1502,8 @@ def verify_air_stage( } my_air_final_value = EF.zero() for (name, log_n_rows), eta_pow in zip(tables_sorted, eta_powers): - meta, down = tables[name], _TABLE_SPECS[name]["down"] - col_evals = state.next_extension_scalars_vec(meta.n_columns + len(down)) + meta, n_shift = tables[name], _TABLE_SPECS[name]["n_shift"] + col_evals = state.next_extension_scalars_vec(meta.n_columns + n_shift) constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) # Per-table contribution = η^t · (Π unused-prefix coords) · eq · C(col_evals). @@ -1602,8 +1514,9 @@ def verify_air_stage( bus_point = from_end(logup.gkr_point, log_n_rows) my_air_final_value = my_air_final_value + eta_pow * k_t * eq_poly_outside(bus_point, natural_pt) * constraint_eval + # Convention: down column `j` is column `j` of the table. eq_values = {i: col_evals[i] for i in range(meta.n_columns)} - next_values = {idx: col_evals[meta.n_columns + j] for j, idx in enumerate(down)} + next_values = {j: col_evals[meta.n_columns + j] for j in range(n_shift)} committed[name].append((natural_pt, eq_values, next_values)) if my_air_final_value != sc.value: @@ -1616,10 +1529,6 @@ def verify_air_stage( return committed, list(pm_point), pm_eval - -# ─── Top-level verifier ────────────────────────────────────────────────────────── - - def verify_execution( bytecode: Bytecode, public_input: Sequence[Fp], @@ -1628,11 +1537,6 @@ def verify_execution( constants: dict, bytecode_multilinear: list[int], ) -> dict: - """Verify a leanVM execution proof. Port of `verify_execution.rs`. - - Flow: observe prologue → read dims → parse stacked-PCS WHIR commitment → - generic logup → AIR sumcheck → assemble global WHIR statements → final WHIR. - `tables` must be in canonical Rust order (`ALL_TABLES`).""" state = VerifierState(proof) state.observe_scalars(list(public_input)) state.observe_scalars(poseidon16_compress(bytecode.hash, SNARK_DOMAIN_SEP)) @@ -1700,10 +1604,6 @@ def verify_execution( return {"log_inv_rate": log_inv_rate, "log_memory": log_memory, "stacked_n_vars": stacked_n_vars} - -# ─── Test vector loader + entry point ────────────────────────────────────────────────────────── - - def poseidon_compress_slice_iv(data: Sequence[Fp]) -> list[Fp]: """Hash a multiple-of-8 sequence with Poseidon16/Davies-Meyer, seeded by an all-zero IV — matches `utils::poseidon_compress_slice(.., use_iv=true)`.""" From 8b4f16565ef501f2512403cd0c21f22836b84c53 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 18 May 2026 11:38:59 +0200 Subject: [PATCH 12/69] wip --- crates/lean_prover/tests/dump_zkvm_vector.rs | 2 +- crates/lean_prover/verifier.py | 133 ++++++++++++------- 2 files changed, 87 insertions(+), 48 deletions(-) diff --git a/crates/lean_prover/tests/dump_zkvm_vector.rs b/crates/lean_prover/tests/dump_zkvm_vector.rs index be6e9c9f4..2e285ef13 100644 --- a/crates/lean_prover/tests/dump_zkvm_vector.rs +++ b/crates/lean_prover/tests/dump_zkvm_vector.rs @@ -203,7 +203,7 @@ fn dump_zkvm_vector() { logup_bytecode_domainsep: LOGUP_BYTECODE_DOMAINSEP, max_precompile_bus_width: MAX_PRECOMPILE_BUS_WIDTH, starting_pc: STARTING_PC, - ending_pc: ENDING_PC, + ending_pc: bytecode.ending_pc, }, snark_domain_sep: lean_prover::SNARK_DOMAIN_SEP.map(f_to_u32), proof: RawProofJson { diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 688569356..ae8d67f24 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -10,7 +10,7 @@ uv venv .venv --python 3.12 VIRTUAL_ENV=.venv uv pip install "git+https://github.com/leanEthereum/leanSpec.git" - # Rust-side data (hardcoded soundness numbers + Poseidon round constants). + # Rust-side data (hardcoded WHIR numbers + Poseidon round constants). cargo test --release -p lean_prover --test dump_whir_configs -- --nocapture cargo test --release -p lean_prover --test dump_poseidon1_constants -- --nocapture @@ -36,10 +36,11 @@ MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 -# Poseidon16 sponge parameters. The challenger uses the compression-with- -# domain-separator design: `state` is a RATE-sized buffer; `observe(chunk)` -# does `state ← permute(state || chunk)[:RATE]`; sampling re-permutes the -# state with a per-call domain separator. +# Poseidon16 duplex-sponge parameters. The challenger keeps a WIDTH-sized +# state; `observe(chunk)` writes `chunk` into the rate slots `[CAPACITY:]` +# and permutes the full state (no DM feed-forward). Sampling reads the +# rate slots; consecutive samples must be separated by `duplex()` (which +# observes zeros). RATE = 8 WIDTH = 16 CAPACITY = WIDTH - RATE @@ -153,34 +154,50 @@ def hash_slice(data: Sequence[Fp]) -> list[Fp]: class Challenger: - """Compression-with-domain-separator Fiat-Shamir state. + """Duplex-sponge Fiat-Shamir state (mirrors `fiat_shamir::Challenger`). - `state` is a length-`RATE` buffer; `observe(chunk)` does - `state ← permute(state || chunk)[:RATE]`; sampling re-permutes the - state with a per-call domain separator `[i, 0, …, 0]`.""" + State is `WIDTH` field elements, all zero at start. `observe(chunk)` + overwrites `state[CAPACITY:] = chunk` and applies the full Poseidon1 + permutation; the rate slots are then *fresh*. `sample()` reads the + rate slots and marks them stale. Multiple samples between observes + must each be preceded by `duplex()` (which absorbs a zero chunk).""" def __init__(self) -> None: - self.state: list[Fp] = [Fp(0)] * RATE + self.state: list[Fp] = [Fp(0)] * WIDTH + self.rate_fresh: bool = False + + def observe(self, chunk: Sequence[Fp]) -> None: + assert len(chunk) == RATE + new_state = self.state[:CAPACITY] + list(chunk) + self.state = _POSEIDON16.permute(new_state) + self.rate_fresh = True def observe_many(self, scalars: Sequence[Fp]) -> None: for i in range(0, len(scalars), RATE): chunk = list(scalars[i : i + RATE]) chunk += [Fp(0)] * (RATE - len(chunk)) - self.state = poseidon16_compress_in_place(self.state + chunk)[:RATE] - - def _sample_blocks(self, n_blocks: int) -> list[Fp]: - """Run `n_blocks + 1` permutations with domain separators, advance the - state to the last, return the first `n_blocks * RATE` scalars flat.""" - flat: list[Fp] = [] - for i in range(n_blocks + 1): - ds = [Fp(i)] + [Fp(0)] * (RATE - 1) - hashed = poseidon16_compress_in_place(ds + self.state)[:RATE] - if i < n_blocks: flat.extend(hashed) - else: self.state = hashed - return flat + self.observe(chunk) + + def duplex(self) -> None: + self.observe([Fp(0)] * RATE) + + def sample(self) -> list[Fp]: + assert self.rate_fresh, "stale rate — insert duplex() before sampling" + self.rate_fresh = False + return list(self.state[CAPACITY:]) + + def sample_many(self, n: int) -> list[Fp]: + """Concatenated rate outputs from `n` sponge squeezes (`duplex` between).""" + if n == 0: + return [] + out = self.sample() + for _ in range(1, n): + self.duplex() + out.extend(self.sample()) + return out def sample_ef_vec(self, n: int) -> list[EF]: - flat = self._sample_blocks((n * EF.DIMENSION + RATE - 1) // RATE)[: n * EF.DIMENSION] + flat = self.sample_many((n * EF.DIMENSION + RATE - 1) // RATE)[: n * EF.DIMENSION] return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] def sample_ef(self) -> EF: @@ -190,7 +207,7 @@ def sample_in_range(self, bits: int, n_samples: int) -> list[int]: """Truncate the low `bits` bits of `n_samples` field samples — matches `challenger::sample_in_range` (not perfectly uniform).""" assert bits < 31 - flat = self._sample_blocks((n_samples + RATE - 1) // RATE)[:n_samples] + flat = self.sample_many((n_samples + RATE - 1) // RATE)[:n_samples] return [int(x.value) & ((1 << bits) - 1) for x in flat] @@ -243,6 +260,7 @@ def next_extension_scalar(self) -> EF: return self.next_extension_scal def sample(self) -> EF: return self.challenger.sample_ef() def sample_vec(self, n: int) -> list[EF]: return self.challenger.sample_ef_vec(n) def sample_in_range(self, b: int, n: int) -> list[int]: return self.challenger.sample_in_range(b, n) + def duplex(self) -> None: self.challenger.duplex() def next_merkle_opening(self) -> MerkleOpening: if self.open_idx >= len(self.openings): @@ -251,10 +269,11 @@ def next_merkle_opening(self) -> MerkleOpening: return self.openings[self.open_idx - 1] def check_pow_grinding(self, bits: int) -> None: - """Grinding witness is `[witness, 0, …, 0]` (RATE-padded).""" + """Grinding witness is `[witness, 0, …, 0]` (RATE-padded). After absorbing + the witness, the first rate slot must be `0 mod 2^bits`.""" if bits == 0: return self._read_padded(1) - if int(self.challenger.state[0].value) & ((1 << bits) - 1) != 0: + if int(self.challenger.state[CAPACITY].value) & ((1 << bits) - 1) != 0: raise ProofError("InvalidGrindingWitness") @@ -690,6 +709,7 @@ def whir_verify( def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None: nonlocal target + state.duplex() new_target, combo = combine_constraints(state, target, constraints) round_constraints.append((combo, constraints)) sc = verify_sumcheck(state, new_target, n_fold, 2, pow_bits) @@ -861,6 +881,7 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] claim_den = eval_multilinear_evals(dens, point) for layer_n_vars in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): + state.duplex() alpha = state.sample() sc = verify_sumcheck(state, claim_num + alpha * claim_den, layer_n_vars, 3) sc_point = list(reversed(sc.point)) @@ -1310,9 +1331,10 @@ def _p1c() -> dict: _POSEIDON_WIDTH = 16 _HALF_DIGEST_LEN = 4 -_POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1 -_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT = 1 << 2 -_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = 1 << 3 +_POSEIDON_PERMUTE_SHIFT = 1 << 1 +_POSEIDON_HALF_OUTPUT_SHIFT = 1 << 2 +_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT = 1 << 3 +_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = 1 << 4 def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: @@ -1378,17 +1400,22 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) - folder.assert_eq(state[i], post) state[i] = post - # Last full round + Davies-Meyer: state += initial_state, then compare to - # outputs (always for the first half; gated by `flag_half_output` after). + # Last full round (no Davies-Meyer add here — the `+ initial[i]` is folded + # into the compression constraint below). Behaviour depends on flag_permute: + # - compression mode (flag_permute = 0): `outputs_left[i] = state[i] + initial[i]` + # for the first 4 lanes always; for lanes 4..8 only when flag_half_output = 0. + # - permute mode (flag_permute = 1): `outputs_left = state[:8]` and + # `outputs_right = state[8:]`. last = 2 * (half_final - 1) state = _full_round(state, const["final_constants"][last], const["final_constants"][last + 1]) - state = [s + init for s, init in zip(state, initial)] - one_minus_half = EF.one() - cols["flag_half_output"] - for idx in range(_POSEIDON_WIDTH // 2): - if idx < _HALF_DIGEST_LEN: - folder.assert_eq(state[idx], cols["outputs"][idx]) - else: - folder.assert_zero(one_minus_half * (state[idx] - cols["outputs"][idx])) + flag_permute = cols["flag_permute"] + not_permute = EF.one() - flag_permute + compression_last4 = not_permute - cols["flag_half_output"] + for i in range(_POSEIDON_WIDTH // 2): + compression_gate = not_permute if i < _HALF_DIGEST_LEN else compression_last4 + folder.assert_zero(compression_gate * (state[i] + initial[i] - cols["outputs_left"][i])) + folder.assert_zero(flag_permute * (state[i] - cols["outputs_left"][i])) + folder.assert_zero(flag_permute * (state[i + _POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: @@ -1407,16 +1434,19 @@ def take(n: int) -> list[EF]: return list(chunk) [flag_active, index_b, index_res, flag_half_output, flag_hardcoded_left, - offset_hardcoded_left, effective_index_left_first, effective_index_left_second] = take(8) + offset_hardcoded_left, effective_index_left_first, effective_index_left_second, + flag_permute] = take(9) inputs = take(W) beginning_full_rounds = [take(W) for _ in range(half_initial)] partial_cols = take(const["partial_rounds"]) ending_full_rounds = [take(W) for _ in range(half_final - 1)] - outputs = take(W // 2) + outputs_left = take(W // 2) + outputs_right = take(W // 2) # Reconstruct `precompile_data` from the flags + offset. precompile_data = ( one + + flag_permute * EF.from_base(Fp(_POSEIDON_PERMUTE_SHIFT)) + flag_half_output * EF.from_base(Fp(_POSEIDON_HALF_OUTPUT_SHIFT)) + flag_hardcoded_left * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT)) + flag_hardcoded_left * offset_hardcoded_left * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT)) @@ -1428,10 +1458,12 @@ def take(n: int) -> list[EF]: folder.assert_zero(_eval_virtual_bus_column( extra_data, flag_active, [precompile_data, index_a, index_b, index_res], )) - # Constraints 2-4: bool flags. - for f in (flag_active, flag_half_output, flag_hardcoded_left): + # Constraints 2-5: bool flags. + for f in (flag_active, flag_half_output, flag_hardcoded_left, flag_permute): folder.assert_bool(f) - # Constraints 5-6: hardcoded-left consistency. + # Constraint 6: mutex — flag_permute can't coexist with half-output or hardcoded-left. + folder.assert_zero(flag_permute * (flag_half_output + flag_hardcoded_left)) + # Constraints 7-8: hardcoded-left consistency. folder.assert_zero(flag_hardcoded_left * (offset_hardcoded_left - effective_index_left_first)) folder.assert_zero(not_hcl * (index_a - effective_index_left_first)) @@ -1440,8 +1472,10 @@ def take(n: int) -> list[EF]: "beginning_full_rounds": beginning_full_rounds, "partial_rounds": partial_cols, "ending_full_rounds": ending_full_rounds, - "outputs": outputs, + "outputs_left": outputs_left, + "outputs_right": outputs_right, "flag_half_output": flag_half_output, + "flag_permute": flag_permute, }, extra_data) @@ -1449,8 +1483,8 @@ def take(n: int) -> list[EF]: # n_shift_columns}`). By convention the shift columns occupy positions `0..n_shift`. _TABLE_SPECS: dict[str, dict] = { "execution": {"degree": 5, "n_constraints": 13, "n_shift": 2}, - "extension_op": {"degree": 4, "n_constraints": 16, "n_shift": 13}, - "poseidon16_compress": {"degree": 10, "n_constraints": 81, "n_shift": 0}, + "extension_op": {"degree": 4, "n_constraints": 33, "n_shift": 13}, + "poseidon16_compress": {"degree": 10, "n_constraints": 99, "n_shift": 0}, } @@ -1476,7 +1510,11 @@ def verify_air_stage( """Returns `(committed_statements, public_memory_random_point, public_memory_eval)`. `committed_statements[name]` is a list of `(point, eq_values, next_values)` triples — one per session for that table.""" - bus_beta, air_alpha, eta = state.sample(), state.sample(), state.sample() + bus_beta = state.sample() + state.duplex() + air_alpha = state.sample() + state.duplex() + eta = state.sample() alpha_powers = _powers(air_alpha, max(_TABLE_SPECS[n]["n_constraints"] for n in tables) + 1) tables_sorted = sort_tables_by_height(table_log_heights) eta_powers = _powers(eta, len(tables_sorted)) @@ -1568,6 +1606,7 @@ def verify_execution( # Logup phase. logup_c = state.sample() + state.duplex() max_bus_width = 1 + max(constants["max_precompile_bus_width"], constants["n_instruction_columns"]) logup_alphas = state.sample_vec(log2_ceil_usize(max_bus_width)) logup_alphas_eq = eval_eq(logup_alphas) From 3aaebfa85da7a104f4575a35e509204e1e88a543 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 18 May 2026 11:55:52 +0200 Subject: [PATCH 13/69] wip --- crates/lean_prover/verifier.py | 381 ++++++++++++--------------------- 1 file changed, 138 insertions(+), 243 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index ae8d67f24..6fe081d86 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -277,12 +277,6 @@ def check_pow_grinding(self, bits: int) -> None: raise ProofError("InvalidGrindingWitness") -@dataclass -class Bytecode: - hash: list[Fp] - log_size: int - - # Multiplicative inverse of 2 in Fp (KoalaBear). Used to halve EF elements. _INV_TWO = Fp(pow(2, P - 2, P)) @@ -427,12 +421,6 @@ def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: return lo + hi * point[0] -@dataclass -class SparseValue: - selector: int - value: EF - - @dataclass class SparseStatement: """A claim with a multilinear `point` over the last `len(point)` variables @@ -441,7 +429,7 @@ class SparseStatement: total_num_variables: int point: list[EF] - values: list[SparseValue] + values: list[tuple[int, EF]] # each entry is (selector, value) is_next: bool = False @property @@ -450,14 +438,14 @@ def selector_num_variables(self) -> int: @staticmethod def dense(point: list[EF], value: EF) -> "SparseStatement": - return SparseStatement(len(point), point, [SparseValue(0, value)]) + return SparseStatement(len(point), point, [(0, value)]) @staticmethod def unique_value(total: int, index: int, value: EF) -> "SparseStatement": - return SparseStatement(total, [], [SparseValue(index, value)]) + return SparseStatement(total, [], [(index, value)]) @staticmethod - def new_next(total: int, point: list[EF], values: list[SparseValue]) -> "SparseStatement": + def new_next(total: int, point: list[EF], values: list[tuple[int, EF]]) -> "SparseStatement": return SparseStatement(total, point, values, is_next=True) @@ -475,13 +463,6 @@ def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: return n, nv - n * WHIR_SUBSEQUENT_FOLDING_FACTOR -def whir_log_inv_rate_at(start_rate: int, r: int) -> int: - """Initial rate, then each round adds `folding_factor − rs_reduction`.""" - return start_rate + r * (WHIR_SUBSEQUENT_FOLDING_FACTOR - 1) + ( - WHIR_INITIAL_FOLDING_FACTOR - RS_DOMAIN_INITIAL_REDUCTION_FACTOR if r >= 1 else 0 - ) - - def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: """`log₂(domain_size)` going into round `r`: starts at `num_vars + rate` and shrinks by the per-round RS reduction (`5` at round 0, `1` thereafter).""" @@ -502,38 +483,17 @@ def two_adic_generator(bits: int) -> Fp: return Fp(KB_TWO_ADIC_GENERATORS[bits]) -@dataclass(frozen=True) -class WhirRoundConfig: - num_queries: int - ood_samples: int - query_pow_bits: int - folding_pow_bits: int - - -@dataclass(frozen=True) -class WhirConfig: - log_inv_rate: int - num_variables: int - commitment_ood_samples: int - starting_folding_pow_bits: int - final_queries: int - final_query_pow_bits: int - rounds: tuple[WhirRoundConfig, ...] - - +# A WHIR config is a dict with: log_inv_rate, num_variables, commitment_ood_samples, +# starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds (list of +# dicts with num_queries, ood_samples, query_pow_bits, folding_pow_bits). @functools.cache -def whir_config(log_inv_rate: int, num_variables: int) -> WhirConfig: - """Loads the Rust-dumped JSON (float-derived query/OOD/grinding numbers). - Everything else is recomputed on the fly via the helpers above.""" +def whir_config(log_inv_rate: int, num_variables: int) -> dict: + """Look up the Rust-dumped soundness numbers for this (rate, num_variables).""" import json from pathlib import Path - raw = json.loads(Path(__file__).with_name(WHIR_CONFIGS_PATH).read_text()) - for c in raw: + for c in json.loads(Path(__file__).with_name(WHIR_CONFIGS_PATH).read_text()): if (c["log_inv_rate"], c["num_variables"]) == (log_inv_rate, num_variables): - return WhirConfig( - **{k: c[k] for k in WhirConfig.__annotations__ if k != "rounds"}, - rounds=tuple(WhirRoundConfig(**r) for r in c["rounds"]), - ) + return c raise KeyError( f"No WHIR config for (log_inv_rate={log_inv_rate}, num_variables={num_variables}). " "Regenerate with: cargo test -p lean_prover --test dump_whir_configs" @@ -554,13 +514,6 @@ def oods_constraints(self) -> list[SparseStatement]: ] -def parsed_commitment_parse(state: VerifierState, num_variables: int, ood_samples: int) -> ParsedCommitment: - root = state.next_base_scalars_vec(DIGEST_ELEMS) - ood_points = state.sample_vec(ood_samples) if ood_samples else [] - ood_answers = state.next_extension_scalars_vec(ood_samples) if ood_samples else [] - return ParsedCommitment(num_variables, root, ood_points, ood_answers) - - @dataclass class Evaluation: point: list[EF] @@ -610,7 +563,7 @@ def combine_constraints( combo = [EF.one()] for smt in constraints: for v in smt.values: - target = target + combo[-1] * v.value + target = target + combo[-1] * v[1] combo.append(combo[-1] * gamma) combo.pop() return target, combo @@ -618,7 +571,7 @@ def combine_constraints( def verify_stir_challenges( state: VerifierState, - cfg: WhirConfig, + cfg: dict, round_index: int, num_variables: int, folding_factor: int, @@ -629,7 +582,7 @@ def verify_stir_challenges( ) -> list[SparseStatement]: """Read `num_queries` Merkle openings, fold each answer at `folding_randomness`, and emit a dense STIR constraint per query.""" - log_domain = whir_log_domain_size_at(cfg.num_variables, cfg.log_inv_rate, round_index) + log_domain = whir_log_domain_size_at(cfg["num_variables"], cfg["log_inv_rate"], round_index) log_height = log_domain - folding_factor gen = two_adic_generator(log_height) @@ -661,7 +614,7 @@ def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> b if any(a * a != b for a, b in zip(constraint.point, constraint.point[1:])): return False univ_eval = _eval_univariate(coeffs, alpha) - return all(univ_eval == v.value for v in constraint.values) + return all(univ_eval == v[1] for v in constraint.values) def eval_constraints_poly( @@ -685,7 +638,7 @@ def eval_constraints_poly( for v in smt.values: lagrange = EF.one() for j in range(sel_n): - bit = (v.selector >> (sel_n - 1 - j)) & 1 + bit = (v[0] >> (sel_n - 1 - j)) & 1 lagrange = lagrange * (pt[j] if bit else (EF.one() - pt[j])) value = value + lagrange * common * randomness[i] i += 1 @@ -695,14 +648,14 @@ def eval_constraints_poly( def whir_verify( state: VerifierState, - cfg: WhirConfig, + cfg: dict, parsed_commitment: ParsedCommitment, statement: list[SparseStatement], ) -> list[EF]: for s in statement: assert s.total_num_variables == parsed_commitment.num_variables - n_rounds, final_sumcheck_rounds = whir_n_rounds_and_final_sumcheck(cfg.num_variables) + n_rounds, final_sumcheck_rounds = whir_n_rounds_and_final_sumcheck(cfg["num_variables"]) round_constraints: list[tuple[list[EF], list[SparseStatement]]] = [] round_folding: list[list[EF]] = [] target = EF.zero() @@ -718,22 +671,27 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None # Initial: OODS + caller statement, then run the first folding sumcheck. step(parsed_commitment.oods_constraints() + statement, - whir_folding_factor_at_round(0), cfg.starting_folding_pow_bits) + whir_folding_factor_at_round(0), cfg["starting_folding_pow_bits"]) # Per-round loop: new commitment → STIR → combine → fold sumcheck. prev_commitment = parsed_commitment - nvars_round = cfg.num_variables + nvars_round = cfg["num_variables"] for r in range(n_rounds): - rp = cfg.rounds[r] + rp = cfg["rounds"][r] nvars_round -= whir_folding_factor_at_round(r) - new_commitment = parsed_commitment_parse(state, nvars_round, rp.ood_samples) + nood = rp["ood_samples"] + new_commitment = ParsedCommitment( + nvars_round, state.next_base_scalars_vec(DIGEST_ELEMS), + state.sample_vec(nood) if nood else [], + state.next_extension_scalars_vec(nood) if nood else [], + ) stir = verify_stir_challenges( state, cfg, r, nvars_round, - whir_folding_factor_at_round(r), rp.num_queries, rp.query_pow_bits, + whir_folding_factor_at_round(r), rp["num_queries"], rp["query_pow_bits"], prev_commitment, round_folding[-1], ) step(new_commitment.oods_constraints() + stir, - whir_folding_factor_at_round(r + 1), rp.folding_pow_bits) + whir_folding_factor_at_round(r + 1), rp["folding_pow_bits"]) prev_commitment = new_commitment # Final round: send poly in coefficient form, verify STIR queries against it. @@ -741,7 +699,7 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None final_coeffs = state.next_extension_scalars_vec(1 << n_vars_final) final_stir = verify_stir_challenges( state, cfg, n_rounds, n_vars_final, - whir_folding_factor_at_round(n_rounds), cfg.final_queries, cfg.final_query_pow_bits, + whir_folding_factor_at_round(n_rounds), cfg["final_queries"], cfg["final_query_pow_bits"], prev_commitment, round_folding[-1], ) for smt in final_stir: @@ -781,20 +739,6 @@ def tables_from_json(obj: list[dict]) -> list[TableMeta]: ] -def compute_stacked_n_vars( - log_memory: int, - log_bytecode: int, - table_log_heights: dict[str, int], - table_n_columns: dict[str, int], -) -> int: - """`log₂` of the stacked polynomial length: 2·memory + bytecode-acc (padded - to the tallest table) + Σ per-table `n_columns × 2^log_n_rows`.""" - max_h = max(table_log_heights.values()) - total = (2 << log_memory) + (1 << max(log_bytecode, max_h)) - total += sum(table_n_columns[n] << h for n, h in table_log_heights.items()) - return log2_ceil_usize(total) - - def stacked_pcs_global_statements( stacked_n_vars: int, memory_n_vars: int, @@ -815,10 +759,10 @@ def stacked_pcs_global_statements( offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, tables_sorted[0][1])) col_pc = constants["col_pc"] - def values_at(d: dict[int, EF], n_vars: int) -> list[SparseValue]: + def values_at(d: dict[int, EF], n_vars: int) -> list[tuple[int, EF]]: # Rust uses BTreeMap → ascending-key iteration; Python dicts are # insertion-ordered, so we sort explicitly here. - return [SparseValue((offset >> n_vars) + i, v) for i, v in sorted(d.items())] + return [((offset >> n_vars) + i, v) for i, v in sorted(d.items())] for name, n_vars in tables_sorted: if name == "execution": @@ -838,26 +782,6 @@ def values_at(d: dict[int, EF], n_vars: int) -> list[SparseValue]: return out -def stacked_pcs_parse_commitment( - state: VerifierState, - log_inv_rate: int, - log_memory: int, - log_bytecode: int, - table_log_heights: dict[str, int], - table_n_columns: dict[str, int], -) -> ParsedCommitment: - """Validate sizing invariants (memory ≥ execution ≥ all other tables, - stacked-poly fits the WHIR domain bound), then parse the commitment.""" - exec_log = table_log_heights["execution"] - if log_memory < exec_log or exec_log < max(table_log_heights.values()): - raise ProofError("InvalidProof: memory or execution table size invariants broken") - stacked_n_vars = compute_stacked_n_vars(log_memory, log_bytecode, table_log_heights, table_n_columns) - if stacked_n_vars > BASE_TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: - raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") - cfg = whir_config(log_inv_rate, stacked_n_vars) - return parsed_commitment_parse(state, stacked_n_vars, cfg.commitment_ood_samples) - - # Verifies `Σ nᵢ/dᵢ` via a layered sumcheck. @@ -952,19 +876,6 @@ def eval_eq(point: Sequence[EF]) -> list[EF]: return out -@dataclass -class GenericLogupStatements: - memory_and_acc_point: list[EF] - value_memory: EF - value_memory_acc: EF - bytecode_and_acc_point: list[EF] - value_bytecode_acc: EF - bus_numerators_values: dict[str, EF] - bus_denominators_values: dict[str, EF] - gkr_point: list[EF] - columns_values: dict[str, dict[int, EF]] - - def verify_generic_logup( state: VerifierState, c: EF, @@ -975,7 +886,7 @@ def verify_generic_logup( table_log_heights: dict[str, int], tables: dict[str, TableMeta], constants: dict, -) -> GenericLogupStatements: +) -> dict: """Run the GKR-quotient protocol and reconstruct numerator/denominator sums section by section (memory / bytecode / per-table). Each section contributes a `pref · (num_term, den_term)` pair to the running totals.""" @@ -1095,17 +1006,15 @@ def pref_at(offset: int, log_height: int) -> EF: if num != claim_num: raise ProofError("logup: numerators value mismatch") if den != claim_den: raise ProofError("logup: denominators value mismatch") - return GenericLogupStatements( - memory_and_acc_point=list(mem_pt), - value_memory=value_memory, - value_memory_acc=value_memory_acc, - bytecode_and_acc_point=list(byte_pt), - value_bytecode_acc=value_bytecode_acc, - bus_numerators_values=bus_num_vals, - bus_denominators_values=bus_den_vals, - gkr_point=list(point_gkr), - columns_values=columns_values, - ) + return { + "value_memory": value_memory, + "value_memory_acc": value_memory_acc, + "value_bytecode_acc": value_bytecode_acc, + "bus_num": bus_num_vals, + "bus_den": bus_den_vals, + "gkr_point": list(point_gkr), + "columns_values": columns_values, + } class ConstraintFolder: @@ -1488,87 +1397,9 @@ def take(n: int) -> list[EF]: } -def _powers(x: EF, n: int) -> list[EF]: - """`[1, x, x², …, x^(n−1)]`.""" - out, cur = [], EF.one() - for _ in range(n): - out.append(cur) - cur = cur * x - return out - - -def verify_air_stage( - state: VerifierState, - logup: GenericLogupStatements, - logup_c: EF, - logup_alphas_eq_poly: list[EF], - table_log_heights: dict[str, int], - tables: dict[str, TableMeta], - public_input: Sequence[Fp], - log_memory: int, -) -> tuple[dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]], list[EF], EF]: - """Returns `(committed_statements, public_memory_random_point, public_memory_eval)`. - `committed_statements[name]` is a list of `(point, eq_values, next_values)` - triples — one per session for that table.""" - bus_beta = state.sample() - state.duplex() - air_alpha = state.sample() - state.duplex() - eta = state.sample() - alpha_powers = _powers(air_alpha, max(_TABLE_SPECS[n]["n_constraints"] for n in tables) + 1) - tables_sorted = sort_tables_by_height(table_log_heights) - eta_powers = _powers(eta, len(tables_sorted)) - extra_data = {"logup_alphas_eq_poly": logup_alphas_eq_poly, "bus_beta": bus_beta, "c": logup_c} - - # Initial AIR sum: Σ η^t · (bus_num · sign + β · (bus_den − c)). - initial_sum = EF.zero() - for (name, _), eta_pow in zip(tables_sorted, eta_powers): - sign = -EF.one() if tables[name].bus_direction == "Pull" else EF.one() - bus_value = logup.bus_numerators_values[name] * sign + bus_beta * (logup.bus_denominators_values[name] - logup_c) - initial_sum = initial_sum + eta_pow * bus_value - - max_degree_plus_one = max(_TABLE_SPECS[n]["degree"] + 1 for n, _ in tables_sorted) - n_max = tables_sorted[0][1] - sc = verify_sumcheck(state, initial_sum, n_max, max_degree_plus_one) - - # Per-table: read col_evals, evaluate the AIR constraint, accumulate, build claims. - # Each table's committed statements start with its logup eq-values entry. - committed = { - name: [(list(from_end(logup.gkr_point, table_log_heights[name])), - dict(logup.columns_values[name]), {})] - for name in tables - } - my_air_final_value = EF.zero() - for (name, log_n_rows), eta_pow in zip(tables_sorted, eta_powers): - meta, n_shift = tables[name], _TABLE_SPECS[name]["n_shift"] - col_evals = state.next_extension_scalars_vec(meta.n_columns + n_shift) - constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) - - # Per-table contribution = η^t · (Π unused-prefix coords) · eq · C(col_evals). - natural_pt = list(reversed(sc.point[-log_n_rows:])) if log_n_rows else [] - k_t = EF.one() - for x in sc.point[: n_max - log_n_rows]: - k_t = k_t * x - bus_point = from_end(logup.gkr_point, log_n_rows) - my_air_final_value = my_air_final_value + eta_pow * k_t * eq_poly_outside(bus_point, natural_pt) * constraint_eval - - # Convention: down column `j` is column `j` of the table. - eq_values = {i: col_evals[i] for i in range(meta.n_columns)} - next_values = {j: col_evals[meta.n_columns + j] for j in range(n_shift)} - committed[name].append((natural_pt, eq_values, next_values)) - - if my_air_final_value != sc.value: - raise ProofError("AIR sumcheck: my_air_final_value != claimed_air_final_value") - - # Public memory MLE evaluation at a fresh random point. - public_memory = padd_with_zero_to_next_power_of_two(public_input) - pm_point = state.sample_vec(log2_strict_usize(len(public_memory))) - pm_eval = eval_multilinear_evals([EF.from_base(f) for f in public_memory], pm_point) - return committed, list(pm_point), pm_eval - - def verify_execution( - bytecode: Bytecode, + bytecode_hash: list[Fp], + bytecode_log_size: int, public_input: Sequence[Fp], proof: Proof, tables: Sequence[TableMeta], @@ -1577,36 +1408,48 @@ def verify_execution( ) -> dict: state = VerifierState(proof) state.observe_scalars(list(public_input)) - state.observe_scalars(poseidon16_compress(bytecode.hash, SNARK_DOMAIN_SEP)) + state.observe_scalars(poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP)) - # Dimensions: log_inv_rate, log_memory, public_input_len, then per-table log_n_rows. + # --- Prologue: read & validate the verifier-side dimensions. --- dims = [int(x.value) for x in state.next_base_scalars_vec(3 + len(tables))] log_inv_rate, log_memory, public_input_len, *table_log_n_rows = dims - if public_input_len != len(public_input): raise ProofError("InvalidProof: public_input length mismatch") if not MIN_WHIR_LOG_INV_RATE <= log_inv_rate <= MAX_WHIR_LOG_INV_RATE: raise ProofError("InvalidRate") if any(h < MIN_LOG_N_ROWS_PER_TABLE for h in table_log_n_rows): raise ProofError("InvalidProof: table too small") - if log_memory < max(max(table_log_n_rows, default=0), bytecode.log_size): + if log_memory < max(max(table_log_n_rows, default=0), bytecode_log_size): raise ProofError("InvalidProof: memory smaller than tables/bytecode") if not MIN_LOG_MEMORY_SIZE <= log_memory <= MAX_LOG_MEMORY_SIZE: raise ProofError("InvalidProof: log_memory out of range") - if bytecode.log_size < MIN_BYTECODE_LOG_SIZE: + if bytecode_log_size < MIN_BYTECODE_LOG_SIZE: raise ProofError("InvalidProof: bytecode too small") table_log_heights = {t.name: h for t, h in zip(tables, table_log_n_rows)} - table_n_columns = {t.name: t.n_columns for t in tables} - tables_by_name = {t.name: t for t in tables} + tables_by_name = {t.name: t for t in tables} + tables_sorted = sort_tables_by_height(table_log_heights) - parsed_commitment = stacked_pcs_parse_commitment( - state, log_inv_rate, log_memory, bytecode.log_size, table_log_heights, table_n_columns, + # --- Stacked-PCS commitment parse. --- + # Sizing invariants: memory ≥ execution ≥ all other tables; stacked length + # = 2·memory + bytecode-acc (padded to tallest table) + Σ n_cols × 2^log_n. + if log_memory < table_log_heights["execution"] or table_log_heights["execution"] < tables_sorted[0][1]: + raise ProofError("InvalidProof: memory or execution table size invariants broken") + total_stacked = ( + (2 << log_memory) + (1 << max(bytecode_log_size, tables_sorted[0][1])) + + sum(t.n_columns << table_log_heights[t.name] for t in tables) ) + stacked_n_vars = log2_ceil_usize(total_stacked) + if stacked_n_vars > BASE_TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: + raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") + cfg = whir_config(log_inv_rate, stacked_n_vars) + root = state.next_base_scalars_vec(DIGEST_ELEMS) + ood_points = state.sample_vec(cfg["commitment_ood_samples"]) if cfg["commitment_ood_samples"] else [] + ood_answers = state.next_extension_scalars_vec(cfg["commitment_ood_samples"]) if cfg["commitment_ood_samples"] else [] + parsed_commitment = ParsedCommitment(stacked_n_vars, root, ood_points, ood_answers) - # Logup phase. - logup_c = state.sample() - state.duplex() + # --- Logup phase. --- + logup_c = state.sample(); state.duplex() max_bus_width = 1 + max(constants["max_precompile_bus_width"], constants["n_instruction_columns"]) logup_alphas = state.sample_vec(log2_ceil_usize(max_bus_width)) logup_alphas_eq = eval_eq(logup_alphas) @@ -1614,31 +1457,83 @@ def verify_execution( state, logup_c, logup_alphas, logup_alphas_eq, log_memory, bytecode_multilinear, table_log_heights, tables_by_name, constants, ) + gkr_point = logup["gkr_point"] - # AIR phase. - committed, pm_point, pm_eval = verify_air_stage( - state, logup, logup_c, logup_alphas_eq, - table_log_heights, tables_by_name, public_input, log_memory, - ) + # --- AIR phase: bus → batched-degree sumcheck → per-table constraint check. --- + bus_beta = state.sample(); state.duplex() + air_alpha = state.sample(); state.duplex() + eta = state.sample() + # alpha_powers = [1, α, α², …]; eta_powers = [1, η, η², …]. + def powers(x: EF, n: int) -> list[EF]: + out, cur = [], EF.one() + for _ in range(n): + out.append(cur); cur = cur * x + return out + alpha_powers = powers(air_alpha, max(_TABLE_SPECS[n]["n_constraints"] for n in tables_by_name) + 1) + eta_powers = powers(eta, len(tables_sorted)) + extra_data = {"logup_alphas_eq_poly": logup_alphas_eq, "bus_beta": bus_beta, "c": logup_c} + + # Initial AIR sum: Σ η^t · (bus_num · sign + β · (bus_den − c)). + initial_sum = EF.zero() + for (name, _), eta_pow in zip(tables_sorted, eta_powers): + sign = -EF.one() if tables_by_name[name].bus_direction == "Pull" else EF.one() + initial_sum = initial_sum + eta_pow * ( + logup["bus_num"][name] * sign + bus_beta * (logup["bus_den"][name] - logup_c) + ) + n_max = tables_sorted[0][1] + sc = verify_sumcheck(state, initial_sum, n_max, max(_TABLE_SPECS[n]["degree"] + 1 for n, _ in tables_sorted)) - # WHIR finale: seed previous_statements with memory, public-memory, bytecode-acc. - stacked_n_vars = parsed_commitment.num_variables + # Per-table: read col_evals, evaluate AIR, accumulate. Each table's committed + # statements start with its logup eq-values entry. + committed = { + name: [(list(from_end(gkr_point, table_log_heights[name])), + dict(logup["columns_values"][name]), {})] + for name in tables_by_name + } + my_air_final = EF.zero() + for (name, log_n_rows), eta_pow in zip(tables_sorted, eta_powers): + meta, n_shift = tables_by_name[name], _TABLE_SPECS[name]["n_shift"] + col_evals = state.next_extension_scalars_vec(meta.n_columns + n_shift) + constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) + + # Per-table contribution = η^t · (Π unused-prefix coords) · eq · C(col_evals). + natural_pt = list(reversed(sc.point[-log_n_rows:])) if log_n_rows else [] + k_t = EF.one() + for x in sc.point[: n_max - log_n_rows]: + k_t = k_t * x + my_air_final = my_air_final + eta_pow * k_t * eq_poly_outside( + from_end(gkr_point, log_n_rows), natural_pt + ) * constraint_eval + + # Shift column `j` is column `j` of the table (by convention). + eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} + next_vals = {j: col_evals[meta.n_columns + j] for j in range(n_shift)} + committed[name].append((natural_pt, eq_vals, next_vals)) + if my_air_final != sc.value: + raise ProofError("AIR sumcheck: claimed value mismatch") + + # Public-memory MLE evaluation at a fresh random point. + public_memory = padd_with_zero_to_next_power_of_two(public_input) + pm_point = state.sample_vec(log2_strict_usize(len(public_memory))) + pm_eval = eval_multilinear_evals([EF.from_base(f) for f in public_memory], pm_point) + + # --- WHIR finale. Seed previous_statements with memory + public-memory + bytecode-acc claims. --- mk = lambda point, values: SparseStatement(stacked_n_vars, list(point), values) previous = [ - mk(logup.memory_and_acc_point, [ - SparseValue(0, logup.value_memory), - SparseValue(1, logup.value_memory_acc), + mk(from_end(gkr_point, log_memory), [ + (0, logup["value_memory"]), + (1, logup["value_memory_acc"]), + ]), + mk(pm_point, [(0, pm_eval)]), + mk(from_end(gkr_point, bytecode_log_size), [ + ((2 << log_memory) >> bytecode_log_size, logup["value_bytecode_acc"]), ]), - mk(pm_point, [SparseValue(0, pm_eval)]), - mk(logup.bytecode_and_acc_point, [SparseValue( - (2 << log_memory) >> bytecode.log_size, logup.value_bytecode_acc, - )]), ] global_statements = stacked_pcs_global_statements( - stacked_n_vars, log_memory, bytecode.log_size, previous, + stacked_n_vars, log_memory, bytecode_log_size, previous, table_log_heights, committed, tables_by_name, constants, ) - whir_verify(state, whir_config(log_inv_rate, stacked_n_vars), parsed_commitment, global_statements) + whir_verify(state, cfg, parsed_commitment, global_statements) return {"log_inv_rate": log_inv_rate, "log_memory": log_memory, "stacked_n_vars": stacked_n_vars} @@ -1673,7 +1568,6 @@ def main() -> int: bytecode_multilinear: list[int] = list(arr) fp_list = lambda xs: [Fp(v) for v in xs] - bytecode = Bytecode(fp_list(raw["bytecode_hash"]), raw["bytecode_log_size"]) public_input = fp_list(raw["public_input"]) input_data = fp_list(raw["input_data"]) proof = Proof( @@ -1691,7 +1585,8 @@ def main() -> int: try: result = verify_execution( - bytecode, public_input, proof, + fp_list(raw["bytecode_hash"]), raw["bytecode_log_size"], + public_input, proof, tables_from_json(raw["tables"]), raw["constants"], bytecode_multilinear, ) except ProofError as e: From 6ef99a7f569eed5f6518a74deb3534a89c13ddf4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 18 May 2026 12:31:16 +0200 Subject: [PATCH 14/69] wip --- crates/lean_prover/verifier.py | 907 +++++++++++++++------------------ 1 file changed, 410 insertions(+), 497 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 6fe081d86..0d303f122 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -5,20 +5,17 @@ Run this script to verify it. Setup (one-time): - # Python venv + lean_spec (gives us KoalaBear `Fp` and the Poseidon1 - # permutation). uv venv .venv --python 3.12 VIRTUAL_ENV=.venv uv pip install "git+https://github.com/leanEthereum/leanSpec.git" - - # Rust-side data (hardcoded WHIR numbers + Poseidon round constants). cargo test --release -p lean_prover --test dump_whir_configs -- --nocapture cargo test --release -p lean_prover --test dump_poseidon1_constants -- --nocapture - - # The end-to-end test vector (~17 MiB; takes a minute or two). cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture Run: .venv/bin/python crates/lean_prover/verifier.py + +Format: + .venv/bin/ruff format --line-length 120 crates/lean_prover/verifier.py """ from __future__ import annotations @@ -30,39 +27,22 @@ from lean_spec.subspecs.koalabear import Fp, P from lean_spec.subspecs.poseidon1 import PARAMS_16, Poseidon1 -# WHIR builder constants (lean_prover/src/lib.rs). -WHIR_INITIAL_FOLDING_FACTOR = 7 -WHIR_SUBSEQUENT_FOLDING_FACTOR = 5 +WHIR_INITIAL_FOLDING_FACTOR, WHIR_SUBSEQUENT_FOLDING_FACTOR = 7, 5 MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 - -# Poseidon16 duplex-sponge parameters. The challenger keeps a WIDTH-sized -# state; `observe(chunk)` writes `chunk` into the rate slots `[CAPACITY:]` -# and permutes the full state (no DM feed-forward). Sampling reads the -# rate slots; consecutive samples must be separated by `duplex()` (which -# observes zeros). -RATE = 8 -WIDTH = 16 +RATE, WIDTH, DIGEST_ELEMS = 8, 16, 8 CAPACITY = WIDTH - RATE -DIGEST_ELEMS = 8 -# leanVM SNARK domain separator (lean_prover/src/lib.rs). -SNARK_DOMAIN_SEP = [ - Fp(v) for v in ( - 130704175, 1303721200, 493664240, 1035493700, - 2063844858, 1410214009, 1938905908, 1696767928, - ) -] +# fmt: off +SNARK_DOMAIN_SEP = [Fp(v) for v in ( + 130704175, 1303721200, 493664240, 1035493700, + 2063844858, 1410214009, 1938905908, 1696767928, +)] +# fmt: on -# Bounds (lean_vm/src/core/constants.rs). MIN_WHIR_LOG_INV_RATE, MAX_WHIR_LOG_INV_RATE = 1, 4 MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 -MIN_LOG_N_ROWS_PER_TABLE = 8 -MIN_BYTECODE_LOG_SIZE = 8 -BASE_TWO_ADICITY = 24 # KoalaBear - -# WHIR config table dumped by `cargo test -p lean_prover --test dump_whir_configs`. -# Lives next to this file. +MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, BASE_TWO_ADICITY = 8, 8, 24 WHIR_CONFIGS_PATH = "whir_configs.json" @@ -70,13 +50,8 @@ class ProofError(Exception): pass -# Quintic extension field: EF = Fp[X] / (X^5 + X^2 - 1) -# Reduction rule: X^5 = 1 - X^2. - - class EF: - """Quintic extension `Fp[X] / (X⁵ + X² − 1)`. Stored as 5 base coefficients; - multiplication reduces with `X⁵ ≡ 1 − X²`.""" + """Quintic extension `Fp[X] / (X⁵ + X² − 1)`.""" __slots__ = ("c",) DIMENSION = 5 @@ -86,45 +61,62 @@ def __init__(self, coeffs: Sequence[Fp]): self.c = tuple(coeffs) @staticmethod - def zero() -> "EF": return EF([Fp(0)] * 5) + def zero() -> "EF": + return EF([Fp(0)] * 5) + @staticmethod - def one() -> "EF": return EF([Fp(1)] + [Fp(0)] * 4) + def one() -> "EF": + return EF([Fp(1)] + [Fp(0)] * 4) + @staticmethod - def from_base(x: Fp) -> "EF": return EF([x] + [Fp(0)] * 4) + def from_base(x: Fp) -> "EF": + return EF([x] + [Fp(0)] * 4) def __add__(self, o): - if isinstance(o, Fp): return EF([self.c[0] + o, *self.c[1:]]) + if isinstance(o, Fp): + return EF([self.c[0] + o, *self.c[1:]]) return EF([a + b for a, b in zip(self.c, o.c)]) + def __sub__(self, o): - if isinstance(o, Fp): return EF([self.c[0] - o, *self.c[1:]]) + if isinstance(o, Fp): + return EF([self.c[0] - o, *self.c[1:]]) return EF([a - b for a, b in zip(self.c, o.c)]) - def __neg__(self): return EF([-a for a in self.c]) + + def __neg__(self): + return EF([-a for a in self.c]) + __radd__ = __add__ def __mul__(self, o): - if isinstance(o, Fp): return EF([a * o for a in self.c]) - # Schoolbook degree-8 product, reduced with X^k = X^(k-5)·(1 − X²) for k ≥ 5. + if isinstance(o, Fp): + return EF([a * o for a in self.c]) a, b = self.c, o.c prod = [Fp(0)] * 9 for i in range(5): for j in range(5): prod[i + j] = prod[i + j] + a[i] * b[j] - for k in range(8, 4, -1): + for k in range(8, 4, -1): # X^k = X^(k-5)·(1 − X²) for k ≥ 5. coef = prod[k] prod[k - 5] = prod[k - 5] + coef prod[k - 3] = prod[k - 3] - coef return EF(prod[:5]) + __rmul__ = __mul__ - def __eq__(self, o): return isinstance(o, EF) and self.c == o.c - def __hash__(self): return hash(self.c) - def __repr__(self): return f"EF({[int(x.value) for x in self.c]})" + def __eq__(self, o): + return isinstance(o, EF) and self.c == o.c + + def __hash__(self): + return hash(self.c) + + def __repr__(self): + return f"EF({[int(x.value) for x in self.c]})" def inv(self) -> "EF": - """Fermat: `a^(P⁵ − 2)`.""" - result, base, n = EF.one(), self, P ** 5 - 2 + result, base, n = EF.one(), self, P**5 - 2 while n > 0: - if n & 1: result = result * base + if n & 1: + result = result * base base = base * base n >>= 1 return result @@ -134,18 +126,15 @@ def inv(self) -> "EF": def poseidon16_compress_in_place(state: list[Fp]) -> list[Fp]: - """Davies-Meyer compression: `permute(state) + state` (length WIDTH).""" assert len(state) == WIDTH return [a + b for a, b in zip(_POSEIDON16.permute(state), state)] def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: - """2:1 Merkle compression: top `DIGEST_ELEMS` of `permute(L || R) + (L || R)`.""" return poseidon16_compress_in_place(list(left) + list(right))[:DIGEST_ELEMS] def hash_slice(data: Sequence[Fp]) -> list[Fp]: - """`symetric::hash_slice` with `WIDTH=16, RATE=OUT=8` (right-to-left absorb).""" assert len(data) % RATE == 0 and len(data) >= WIDTH state = poseidon16_compress_in_place(list(data[-WIDTH:])) for k in range(len(data) // RATE - 3, -1, -1): @@ -154,13 +143,8 @@ def hash_slice(data: Sequence[Fp]) -> list[Fp]: class Challenger: - """Duplex-sponge Fiat-Shamir state (mirrors `fiat_shamir::Challenger`). - - State is `WIDTH` field elements, all zero at start. `observe(chunk)` - overwrites `state[CAPACITY:] = chunk` and applies the full Poseidon1 - permutation; the rate slots are then *fresh*. `sample()` reads the - rate slots and marks them stale. Multiple samples between observes - must each be preceded by `duplex()` (which absorbs a zero chunk).""" + """Duplex-sponge Fiat-Shamir. `observe(chunk)` permutes `state[:CAPACITY] || chunk`; + `_sample_rate()` reads `state[CAPACITY:]` once per `duplex()`.""" def __init__(self) -> None: self.state: list[Fp] = [Fp(0)] * WIDTH @@ -168,8 +152,7 @@ def __init__(self) -> None: def observe(self, chunk: Sequence[Fp]) -> None: assert len(chunk) == RATE - new_state = self.state[:CAPACITY] + list(chunk) - self.state = _POSEIDON16.permute(new_state) + self.state = _POSEIDON16.permute(self.state[:CAPACITY] + list(chunk)) self.rate_fresh = True def observe_many(self, scalars: Sequence[Fp]) -> None: @@ -181,33 +164,30 @@ def observe_many(self, scalars: Sequence[Fp]) -> None: def duplex(self) -> None: self.observe([Fp(0)] * RATE) - def sample(self) -> list[Fp]: + def _sample_rate(self) -> list[Fp]: assert self.rate_fresh, "stale rate — insert duplex() before sampling" self.rate_fresh = False return list(self.state[CAPACITY:]) - def sample_many(self, n: int) -> list[Fp]: - """Concatenated rate outputs from `n` sponge squeezes (`duplex` between).""" + def _sample_many(self, n: int) -> list[Fp]: if n == 0: return [] - out = self.sample() + out = self._sample_rate() for _ in range(1, n): self.duplex() - out.extend(self.sample()) + out.extend(self._sample_rate()) return out - def sample_ef_vec(self, n: int) -> list[EF]: - flat = self.sample_many((n * EF.DIMENSION + RATE - 1) // RATE)[: n * EF.DIMENSION] + def sample_vec(self, n: int) -> list[EF]: + flat = self._sample_many((n * EF.DIMENSION + RATE - 1) // RATE)[: n * EF.DIMENSION] return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] - def sample_ef(self) -> EF: - return self.sample_ef_vec(1)[0] + def sample(self) -> EF: + return self.sample_vec(1)[0] def sample_in_range(self, bits: int, n_samples: int) -> list[int]: - """Truncate the low `bits` bits of `n_samples` field samples — matches - `challenger::sample_in_range` (not perfectly uniform).""" assert bits < 31 - flat = self.sample_many((n_samples + RATE - 1) // RATE)[:n_samples] + flat = self._sample_many((n_samples + RATE - 1) // RATE)[:n_samples] return [int(x.value) & ((1 << bits) - 1) for x in flat] @@ -223,31 +203,30 @@ class Proof: merkle_openings: list[MerkleOpening] -class VerifierState: - """Reads from the raw transcript: every `n` real scalars are consumed as - `next_multiple_of(n, RATE)` raw scalars (trailing positions must be zero), - and the full RATE-aligned chunk is what the challenger absorbs.""" +class VerifierState(Challenger): + """Challenger + transcript reader. `n` scalars are consumed as `next_multiple_of(n, RATE)` + raw scalars (trailing must be zero) and absorbed.""" def __init__(self, proof: Proof) -> None: - self.challenger = Challenger() + super().__init__() self.transcript = list(proof.transcript) self.openings = list(proof.merkle_openings) self.offset = 0 self.open_idx = 0 def _read_padded(self, n: int) -> list[Fp]: - n_pad = -(-n // RATE) * RATE # next multiple of RATE + n_pad = -(-n // RATE) * RATE if self.offset + n_pad > len(self.transcript): raise ProofError("ExceededTranscript") chunk = self.transcript[self.offset : self.offset + n_pad] self.offset += n_pad if any(int(chunk[i].value) for i in range(n, n_pad)): raise ProofError("InvalidTranscript: non-zero padding") - self.challenger.observe_many(chunk) + self.observe_many(chunk) return chunk def observe_scalars(self, scalars: Sequence[Fp]) -> None: - self.challenger.observe_many(list(scalars)) + self.observe_many(list(scalars)) def next_base_scalars_vec(self, n: int) -> list[Fp]: return self._read_padded(n)[:n] @@ -256,11 +235,8 @@ def next_extension_scalars_vec(self, n: int) -> list[EF]: flat = self.next_base_scalars_vec(n * EF.DIMENSION) return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] - def next_extension_scalar(self) -> EF: return self.next_extension_scalars_vec(1)[0] - def sample(self) -> EF: return self.challenger.sample_ef() - def sample_vec(self, n: int) -> list[EF]: return self.challenger.sample_ef_vec(n) - def sample_in_range(self, b: int, n: int) -> list[int]: return self.challenger.sample_in_range(b, n) - def duplex(self) -> None: self.challenger.duplex() + def next_extension_scalar(self) -> EF: + return self.next_extension_scalars_vec(1)[0] def next_merkle_opening(self) -> MerkleOpening: if self.open_idx >= len(self.openings): @@ -269,15 +245,13 @@ def next_merkle_opening(self) -> MerkleOpening: return self.openings[self.open_idx - 1] def check_pow_grinding(self, bits: int) -> None: - """Grinding witness is `[witness, 0, …, 0]` (RATE-padded). After absorbing - the witness, the first rate slot must be `0 mod 2^bits`.""" - if bits == 0: return + if bits == 0: + return self._read_padded(1) - if int(self.challenger.state[CAPACITY].value) & ((1 << bits) - 1) != 0: + if int(self.state[CAPACITY].value) & ((1 << bits) - 1) != 0: raise ProofError("InvalidGrindingWitness") -# Multiplicative inverse of 2 in Fp (KoalaBear). Used to halve EF elements. _INV_TWO = Fp(pow(2, P - 2, P)) @@ -314,7 +288,7 @@ def merkle_verify_path( def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: - """`[x, x², x⁴, …, x^(2^(n−1))]` — `MultilinearPoint::expand_from_univariate`.""" + """`[x, x², x⁴, …, x^(2^(n−1))]`.""" out, cur = [], x for _ in range(num_variables): out.append(cur) @@ -323,7 +297,7 @@ def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: - """`Π (1 − a_i − b_i + 2·a_i·b_i)` — multilinear `eq` polynomial.""" + """`Π (1 − a_i − b_i + 2·a_i·b_i)`.""" assert len(a) == len(b) one, acc = EF.one(), EF.one() for x, y in zip(a, b): @@ -333,12 +307,9 @@ def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: - """The "next-row" weight `ν(x, y)`: multilinear extension of `y = x + 1` - (big-endian, mod `2^n`). Sums one term per "carry boundary" position plus - the all-ones wraparound `Π x_i · y_i`.""" + """Multilinear extension of `y = x + 1` (big-endian, mod `2^n`).""" assert len(x) == len(y) one = EF.one() - # Prefix of `eq(x_i, y_i)` and suffix of `Π x_i · (1 − y_i)` (the low-bits term). n = len(x) eq_prefix = [one] for i in range(n): @@ -356,8 +327,7 @@ def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: - """Evaluate a multilinear in evaluation form (length `2^n`) at `point ∈ EF^n`. - Big-endian indexing — fold variables last-to-first.""" + """Evaluate a multilinear in evaluation form at `point` (big-endian fold).""" assert len(evals) == 1 << len(point) cur = list(evals) for r in reversed(point): @@ -366,16 +336,13 @@ def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: - """Same fold as `eval_multilinear_evals`, specialized for base-field evals. - - Uses numpy to skip per-scalar Python/`Fp` wrapper overhead — the bytecode - multilinear is a 2²²-entry fold, far too big for the generic path. - """ + """numpy-backed fold for the 2²²-entry bytecode multilinear.""" import numpy as np + assert len(base_evals) == 1 << len(point) pt = [tuple(int(ci.value) for ci in p.c) for p in point] cur = np.asarray(base_evals, dtype=np.int64) % P - # First round: base → EF. a + (b-a)*r, with r ∈ EF, a,b ∈ base. + # First round: base → EF. a + (b-a)·r with r ∈ EF. a, b = cur[0::2], cur[1::2] d = (b - a) % P r = pt[-1] @@ -383,9 +350,7 @@ def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: [(a + d * r[0]) % P, *[(d * r[k]) % P for k in range(1, 5)]], axis=1, ) - # Remaining rounds: EF × EF. Schoolbook product reduced mod X^5+X^2-1 - # (using X^5≡1-X², X^6≡X-X³, X^7≡X²-X⁴, X^8≡X³+X²-1). - # Reduce every multiply before summing to stay inside int64. + # EF × EF rounds: schoolbook product reduced mod X⁵+X²−1. for r0, r1, r2, r3, r4 in (pt[i] for i in range(len(pt) - 2, -1, -1)): a, b = cur[0::2], cur[1::2] d = (b - a) % P @@ -400,13 +365,16 @@ def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: p6 = (m(d2, r4) + m(d3, r3) + m(d4, r2)) % P p7 = (m(d3, r4) + m(d4, r3)) % P p8 = m(d4, r4) - cur = np.stack([ - (a[:, 0] + p0 + p5 - p8) % P, - (a[:, 1] + p1 + p6) % P, - (a[:, 2] + p2 - p5 + p7 + p8) % P, - (a[:, 3] + p3 - p6 + p8) % P, - (a[:, 4] + p4 - p7) % P, - ], axis=1) + cur = np.stack( + [ + (a[:, 0] + p0 + p5 - p8) % P, + (a[:, 1] + p1 + p6) % P, + (a[:, 2] + p2 - p5 + p7 + p8) % P, + (a[:, 3] + p3 - p6 + p8) % P, + (a[:, 4] + p4 - p7) % P, + ], + axis=1, + ) return EF([Fp(int(v)) for v in cur[0]]) @@ -423,13 +391,12 @@ def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: @dataclass class SparseStatement: - """A claim with a multilinear `point` over the last `len(point)` variables - and one or more `(selector, value)` pairs indexing the leading selector - bits. `is_next` swaps the eq-weight for a "next-row" weight (`next_mle`).""" + """Claim with multilinear `point` and `(selector, value)` pairs. `is_next` + swaps the eq-weight for `next_mle`.""" total_num_variables: int point: list[EF] - values: list[tuple[int, EF]] # each entry is (selector, value) + values: list[tuple[int, EF]] is_next: bool = False @property @@ -454,8 +421,6 @@ def whir_folding_factor_at_round(r: int) -> int: def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: - """FoldingFactor::compute_number_of_rounds with default (7, 5, max_send=8). - Returns `(n_rounds, final_sumcheck_rounds)`.""" nv = num_variables - WHIR_INITIAL_FOLDING_FACTOR if nv < MAX_NUM_VARIABLES_TO_SEND_COEFFS: return 0, nv @@ -464,33 +429,28 @@ def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: - """`log₂(domain_size)` going into round `r`: starts at `num_vars + rate` - and shrinks by the per-round RS reduction (`5` at round 0, `1` thereafter).""" return num_variables + start_rate - (RS_DOMAIN_INITIAL_REDUCTION_FACTOR + r - 1 if r >= 1 else 0) -# KoalaBear two-adic generators: index `bits` is the primitive 2^bits-th root -# of unity (canonical-form u32 values, mirrors `TWO_ADIC_GENERATORS`). +# fmt: off KB_TWO_ADIC_GENERATORS: list[int] = [ 0x1, 0x7F000000, 0x7E010002, 0x6832FE4A, 0x08DBD69C, 0x0A28F031, 0x5C4A5B99, 0x29B75A80, 0x17668B8A, 0x27AD539B, 0x334D48C7, 0x7744959C, 0x768FC6FA, 0x303964B2, 0x3E687D4D, 0x45A60E61, 0x6E2F4D7A, 0x163BD499, 0x6C4A8A45, 0x143EF899, 0x514DDCAD, 0x484EF19B, 0x205D63C3, 0x68E7DD49, 0x6AC49F88, ] +# fmt: on def two_adic_generator(bits: int) -> Fp: return Fp(KB_TWO_ADIC_GENERATORS[bits]) -# A WHIR config is a dict with: log_inv_rate, num_variables, commitment_ood_samples, -# starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds (list of -# dicts with num_queries, ood_samples, query_pow_bits, folding_pow_bits). @functools.cache def whir_config(log_inv_rate: int, num_variables: int) -> dict: - """Look up the Rust-dumped soundness numbers for this (rate, num_variables).""" import json from pathlib import Path + for c in json.loads(Path(__file__).with_name(WHIR_CONFIGS_PATH).read_text()): if (c["log_inv_rate"], c["num_variables"]) == (log_inv_rate, num_variables): return c @@ -503,7 +463,7 @@ def whir_config(log_inv_rate: int, num_variables: int) -> dict: @dataclass class ParsedCommitment: num_variables: int - root: list[Fp] # length DIGEST_ELEMS + root: list[Fp] # length DIGEST_ELEMS ood_points: list[EF] ood_answers: list[EF] @@ -514,12 +474,6 @@ def oods_constraints(self) -> list[SparseStatement]: ] -@dataclass -class Evaluation: - point: list[EF] - value: EF - - def _eval_univariate(coeffs: list[EF], x: EF) -> EF: acc = EF.zero() for c in reversed(coeffs): @@ -528,19 +482,13 @@ def _eval_univariate(coeffs: list[EF], x: EF) -> EF: def verify_sumcheck( - state: VerifierState, - target: EF, - n_vars: int, - degree: int, - pow_bits: int = 0, -) -> "Evaluation": - """Read `n_vars` round polynomials in univariate basis (`degree + 1` coeffs - each). Per round: check `h(0) + h(1) == target`, optional PoW grinding, - sample a challenge, fold the target into `h(challenge)`.""" + state: VerifierState, target: EF, n_vars: int, degree: int, pow_bits: int = 0 +) -> tuple[list[EF], EF]: + """Round-by-round: check `h(0) + h(1) == target`, grind, sample, fold. Returns (point, value).""" point: list[EF] = [] for _ in range(n_vars): coeffs = state.next_extension_scalars_vec(degree + 1) - s = coeffs[0] # h(0) + h(1) = coeffs[0] + Σ coeffs. + s = coeffs[0] for c in coeffs: s = s + c if s != target: @@ -549,16 +497,10 @@ def verify_sumcheck( r = state.sample() point.append(r) target = _eval_univariate(coeffs, r) - return Evaluation(point=point, value=target) + return point, target -def combine_constraints( - state: VerifierState, - target: EF, - constraints: list[SparseStatement], -) -> tuple[EF, list[EF]]: - """Linear combination of constraint values by random `γ` powers. Returns - the updated target and the per-value combination weights `[1, γ, γ², ...]`.""" +def combine_constraints(state: VerifierState, target: EF, constraints: list[SparseStatement]) -> tuple[EF, list[EF]]: gamma: EF = state.sample() combo = [EF.one()] for smt in constraints: @@ -580,17 +522,12 @@ def verify_stir_challenges( commitment: ParsedCommitment, folding_randomness: list[EF], ) -> list[SparseStatement]: - """Read `num_queries` Merkle openings, fold each answer at - `folding_randomness`, and emit a dense STIR constraint per query.""" - log_domain = whir_log_domain_size_at(cfg["num_variables"], cfg["log_inv_rate"], round_index) - log_height = log_domain - folding_factor + log_height = whir_log_domain_size_at(cfg["num_variables"], cfg["log_inv_rate"], round_index) - folding_factor gen = two_adic_generator(log_height) - state.check_pow_grinding(query_pow_bits) indices = state.sample_in_range(log_height, num_queries) def pack_answers(leaf: list[Fp]) -> list[EF]: - # Round 0 leaves are base-field; later rounds carry packed EF (5 base → 1 EF). if round_index == 0: return [EF.from_base(f) for f in leaf] return [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] @@ -607,8 +544,7 @@ def pack_answers(leaf: list[Fp]) -> list[EF]: def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> bool: - """Checks `constraint.point == [α, α², α⁴, …]` and that the univariate - `Σ coeffs[i] · α^i` matches every claimed value.""" + """Checks `point == [α, α², α⁴, …]` and `Σ coeffs[i]·α^i == value`.""" assert constraint.selector_num_variables == 0 alpha = constraint.point[0] if any(a * a != b for a, b in zip(constraint.point, constraint.point[1:])): @@ -617,22 +553,15 @@ def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> b return all(univ_eval == v[1] for v in constraint.values) -def eval_constraints_poly( - constraints: list[tuple[list[EF], list[SparseStatement]]], - point: list[EF], -) -> EF: - """Per-round `(combination_weights, statements)`: at each round we slice - `point` by the previous round's folding factor, then sum - `lagrange(selector) · common_weight(smt.point, inner_pt) · γ^i` over all - `(smt, value)` pairs.""" +def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatement]]], point: list[EF]) -> EF: value = EF.zero() pt = list(point) for round_idx, (randomness, smts) in enumerate(constraints): if round_idx > 0: - pt = pt[whir_folding_factor_at_round(round_idx - 1):] + pt = pt[whir_folding_factor_at_round(round_idx - 1) :] i = 0 for smt in smts: - inner_pt = pt[len(pt) - len(smt.point):] + inner_pt = pt[len(pt) - len(smt.point) :] common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly_outside(smt.point, inner_pt) sel_n = smt.selector_num_variables for v in smt.values: @@ -665,15 +594,15 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None state.duplex() new_target, combo = combine_constraints(state, target, constraints) round_constraints.append((combo, constraints)) - sc = verify_sumcheck(state, new_target, n_fold, 2, pow_bits) - round_folding.append(sc.point) - target = sc.value + sc_point, target = verify_sumcheck(state, new_target, n_fold, 2, pow_bits) + round_folding.append(sc_point) - # Initial: OODS + caller statement, then run the first folding sumcheck. - step(parsed_commitment.oods_constraints() + statement, - whir_folding_factor_at_round(0), cfg["starting_folding_pow_bits"]) + step( + parsed_commitment.oods_constraints() + statement, + whir_folding_factor_at_round(0), + cfg["starting_folding_pow_bits"], + ) - # Per-round loop: new commitment → STIR → combine → fold sumcheck. prev_commitment = parsed_commitment nvars_round = cfg["num_variables"] for r in range(n_rounds): @@ -681,39 +610,53 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None nvars_round -= whir_folding_factor_at_round(r) nood = rp["ood_samples"] new_commitment = ParsedCommitment( - nvars_round, state.next_base_scalars_vec(DIGEST_ELEMS), + nvars_round, + state.next_base_scalars_vec(DIGEST_ELEMS), state.sample_vec(nood) if nood else [], state.next_extension_scalars_vec(nood) if nood else [], ) stir = verify_stir_challenges( - state, cfg, r, nvars_round, - whir_folding_factor_at_round(r), rp["num_queries"], rp["query_pow_bits"], - prev_commitment, round_folding[-1], + state, + cfg, + r, + nvars_round, + whir_folding_factor_at_round(r), + rp["num_queries"], + rp["query_pow_bits"], + prev_commitment, + round_folding[-1], + ) + step( + new_commitment.oods_constraints() + stir, + whir_folding_factor_at_round(r + 1), + rp["folding_pow_bits"], ) - step(new_commitment.oods_constraints() + stir, - whir_folding_factor_at_round(r + 1), rp["folding_pow_bits"]) prev_commitment = new_commitment - # Final round: send poly in coefficient form, verify STIR queries against it. n_vars_final = nvars_round - whir_folding_factor_at_round(n_rounds) final_coeffs = state.next_extension_scalars_vec(1 << n_vars_final) final_stir = verify_stir_challenges( - state, cfg, n_rounds, n_vars_final, - whir_folding_factor_at_round(n_rounds), cfg["final_queries"], cfg["final_query_pow_bits"], - prev_commitment, round_folding[-1], + state, + cfg, + n_rounds, + n_vars_final, + whir_folding_factor_at_round(n_rounds), + cfg["final_queries"], + cfg["final_query_pow_bits"], + prev_commitment, + round_folding[-1], ) for smt in final_stir: if not verify_constraint_coeffs(smt, final_coeffs): raise ProofError("Final STIR constraint mismatch") - # Final sumcheck — closes the protocol against the constraint-weights MLE. - final_sc = verify_sumcheck(state, target, final_sumcheck_rounds, 2) - round_folding.append(final_sc.point) + final_sc_point, final_sc_value = verify_sumcheck(state, target, final_sumcheck_rounds, 2) + round_folding.append(final_sc_point) folding_flat = [r for chunk in round_folding for r in chunk] eval_weights = eval_constraints_poly(round_constraints, folding_flat) - final_value = eval_multilinear_coeffs(final_coeffs, list(reversed(final_sc.point))) - if final_sc.value != eval_weights * final_value: + final_value = eval_multilinear_coeffs(final_coeffs, list(reversed(final_sc_point))) + if final_sc_value != eval_weights * final_value: raise ProofError("WHIR final sumcheck check failed") return folding_flat @@ -749,9 +692,6 @@ def stacked_pcs_global_statements( tables: dict[str, TableMeta], constants: dict, ) -> list[SparseStatement]: - """Stacks the per-table column claims into the global statement list passed to - `WhirConfig::verify`. Tables are processed in descending-height order. - """ assert len(table_log_heights) == len(committed_statements) tables_sorted = sort_tables_by_height(table_log_heights) @@ -759,20 +699,21 @@ def stacked_pcs_global_statements( offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, tables_sorted[0][1])) col_pc = constants["col_pc"] + # Rust uses BTreeMap (sorted); Python dicts are insertion-ordered, sort here. def values_at(d: dict[int, EF], n_vars: int) -> list[tuple[int, EF]]: - # Rust uses BTreeMap → ascending-key iteration; Python dicts are - # insertion-ordered, so we sort explicitly here. return [((offset >> n_vars) + i, v) for i, v in sorted(d.items())] for name, n_vars in tables_sorted: if name == "execution": - # PC column: first row pinned to `starting_pc`, last row to `ending_pc`. - for idx_in_col, pc_value in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: - out.append(SparseStatement.unique_value( - stacked_n_vars, offset + (col_pc << n_vars) + idx_in_col, EF.from_base(Fp(pc_value)), - )) - - for (point, eq_values, next_values) in committed_statements[name]: + # PC column: pin first row to starting_pc, last row to ending_pc. + for idx, pc in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: + out.append( + SparseStatement.unique_value( + stacked_n_vars, offset + (col_pc << n_vars) + idx, EF.from_base(Fp(pc)) + ) + ) + + for point, eq_values, next_values in committed_statements[name]: if next_values: out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values, n_vars))) out.append(SparseStatement(stacked_n_vars, list(point), values_at(eq_values, n_vars))) @@ -782,16 +723,11 @@ def values_at(d: dict[int, EF], n_vars: int) -> list[tuple[int, EF]]: return out -# Verifies `Σ nᵢ/dᵢ` via a layered sumcheck. - - N_VARS_TO_SEND_GKR_COEFFS = 5 def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF], EF, EF]: - """Returns `(quotient, point, claim_num, claim_den)`. Reads the top-level - coefficients, then collapses one variable per layer with a degree-3 - sumcheck on `n·d_r + n_r·d` plus the next (num, den) random combination.""" + """Layered sumcheck for `Σ nᵢ/dᵢ`. Returns `(quotient, point, claim_num, claim_den)`.""" assert n_vars > N_VARS_TO_SEND_GKR_COEFFS nums = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) @@ -807,12 +743,10 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] for layer_n_vars in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): state.duplex() alpha = state.sample() - sc = verify_sumcheck(state, claim_num + alpha * claim_den, layer_n_vars, 3) - sc_point = list(reversed(sc.point)) - # Inner evaluations: (n_left, n_right, d_left, d_right) at sc.point. + raw_pt, sc_value = verify_sumcheck(state, claim_num + alpha * claim_den, layer_n_vars, 3) + sc_point = list(reversed(raw_pt)) nl, nr, dl, dr = state.next_extension_scalars_vec(4) - # Sumcheck identity: eq(point, sc_point) · (α·dl·dr + (nl·dr + nr·dl)). - if sc.value != eq_poly_outside(point, sc_point) * (alpha * dl * dr + nl * dr + nr * dl): + if sc_value != eq_poly_outside(point, sc_point) * (alpha * dl * dr + nl * dr + nr * dl): raise ProofError("GKR step: postponed value mismatch") beta = state.sample() one_minus = EF.one() - beta @@ -824,12 +758,10 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: - """`bit_count` bits of `value` MSB-first, each as `EF::ZERO`/`EF::ONE`.""" return [EF.one() if (value >> (bit_count - 1 - i)) & 1 else EF.zero() for i in range(bit_count)] def from_end(seq: Sequence, n: int) -> list: - """Last `n` elements of `seq`.""" return list(seq[len(seq) - n :]) if n else [] @@ -845,8 +777,10 @@ def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: """MLE of `[0]*n_zeros ++ [1]*(2^len(point) − n_zeros)` at `point`.""" n_values = 1 << len(point) assert n_zeros <= n_values - if n_zeros == 0: return EF.one() - if n_zeros == n_values: return EF.zero() + if n_zeros == 0: + return EF.one() + if n_zeros == n_values: + return EF.zero() half, tail = n_values >> 1, point[1:] if n_zeros < half: return (EF.one() - point[0]) * mle_of_zeros_then_ones(n_zeros, tail) + point[0] @@ -854,7 +788,7 @@ def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: def finger_print(table: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: - """`Σᵢ αᵢ · dataᵢ + α_last · table` — Reed-Solomon-style fingerprint.""" + """`Σᵢ αᵢ · dataᵢ + α_last · table`.""" assert len(alphas_eq_poly) > len(data) acc = EF.zero() for a, d in zip(alphas_eq_poly, data): @@ -868,8 +802,7 @@ def sort_tables_by_height(table_log_heights: dict[str, int]) -> list[tuple[str, def eval_eq(point: Sequence[EF]) -> list[EF]: - """Length-`2^n` evaluation table of `eq(point, ·)`: big-endian-bit-indexed - `Πⱼ (point[j] if bitⱼ(i) else 1 − point[j])`.""" + """Length-`2^n` evaluation table of `eq(point, ·)`.""" out = [EF.one()] for p in point: out = [w for v in out for w in (v * (EF.one() - p), v * p)] @@ -887,25 +820,23 @@ def verify_generic_logup( tables: dict[str, TableMeta], constants: dict, ) -> dict: - """Run the GKR-quotient protocol and reconstruct numerator/denominator - sums section by section (memory / bytecode / per-table). Each section - contributes a `pref · (num_term, den_term)` pair to the running totals.""" - n_instr_cols = constants["n_instruction_columns"] + """GKR-quotient + section-by-section (memory/bytecode/per-table) reconstruction.""" + n_instr_cols = constants["n_instruction_columns"] n_runtime_cols = constants["n_runtime_columns"] - col_pc = constants["col_pc"] - dom_mem = Fp(constants["logup_memory_domainsep"]) - dom_byte = Fp(constants["logup_bytecode_domainsep"]) + col_pc = constants["col_pc"] + dom_mem = Fp(constants["logup_memory_domainsep"]) + dom_byte = Fp(constants["logup_bytecode_domainsep"]) tables_sorted = sort_tables_by_height(table_log_heights) - log_bytecode = log2_strict_usize(len(bytecode_multilinear) // (1 << log2_ceil_usize(n_instr_cols))) - log_instr = log2_ceil_usize(n_instr_cols) - log_n_cycles = table_log_heights["execution"] + log_bytecode = log2_strict_usize(len(bytecode_multilinear) // (1 << log2_ceil_usize(n_instr_cols))) + log_instr = log2_ceil_usize(n_instr_cols) + log_n_cycles = table_log_heights["execution"] - # Total active length: memory + max(bytecode, tallest table) + execution - # cycles + Σ per-table (lookup arity sum + 1 bus column) × 2^log_n_rows. table_cols = lambda n: sum(len(vs) for _, vs in tables[n].lookups) + 1 total_active_len = ( - (1 << log_memory) + max(1 << log_bytecode, 1 << tables_sorted[0][1]) + (1 << log_n_cycles) + (1 << log_memory) + + max(1 << log_bytecode, 1 << tables_sorted[0][1]) + + (1 << log_n_cycles) + sum(table_cols(n) << h for n, h in tables_sorted) ) total_gkr_n_vars = log2_ceil_usize(total_active_len) @@ -915,32 +846,29 @@ def verify_generic_logup( raise ProofError("logup: GKR sum != 0") num, den = EF.zero(), EF.zero() + def pref_at(offset: int, log_height: int) -> EF: n_missing = total_gkr_n_vars - log_height bits = to_big_endian_in_field(offset >> log_height, n_missing) return eq_poly_outside(bits, point_gkr[:n_missing]) - # Memory section: subtracts the accumulator from `num`, adds (c − fp) to `den`. - mem_pt = from_end(point_gkr, log_memory) - pref = pref_at(0, log_memory) + # Memory. + mem_pt = from_end(point_gkr, log_memory) + pref = pref_at(0, log_memory) value_memory_acc = state.next_extension_scalar() - value_memory = state.next_extension_scalar() + value_memory = state.next_extension_scalar() fp_mem = finger_print(dom_mem, [value_memory, mle_of_01234567_etc(mem_pt)], alphas_eq_poly) num = num - pref * value_memory_acc den = den + pref * (c - fp_mem) offset = 1 << log_memory - # Bytecode section: same shape; the bytecode MLE is evaluated at the - # `bytecode_and_acc_point + last log_instr coords of alphas` point and - # corrected by `Π (1 − alpha_i)` over the bus-data prefix. + # Bytecode (padded to the tallest table). log_byte_pad = max(log_bytecode, tables_sorted[0][1]) - byte_pt = from_end(point_gkr, log_bytecode) - pref = pref_at(offset, log_bytecode) - pref_pad = pref_at(offset, log_byte_pad) + byte_pt = from_end(point_gkr, log_bytecode) + pref = pref_at(offset, log_bytecode) + pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() - bytecode_value = eval_mle_base_at_ef( - bytecode_multilinear, list(byte_pt) + list(from_end(alphas, log_instr)) - ) + bytecode_value = eval_mle_base_at_ef(bytecode_multilinear, list(byte_pt) + list(from_end(alphas, log_instr))) correction = EF.one() for a in alphas[: len(alphas) - log_instr]: correction = correction * (EF.one() - a) @@ -950,12 +878,14 @@ def pref_at(offset: int, log_height: int) -> EF: + alphas_eq_poly[-1] * EF.from_base(dom_byte) ) num = num - pref * value_bytecode_acc - den = den + pref * (c - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, from_end(point_gkr, log_byte_pad)) + den = ( + den + + pref * (c - fp_byte) + + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, from_end(point_gkr, log_byte_pad)) + ) offset += 1 << log_byte_pad - # Per-table sections: execution-only bytecode lookup, bus column, then - # one (index, value-array) lookup per `meta.lookups`. Each contributes - # `pref` to `num` and `pref · (c − fingerprint)` to `den`. + # Per-table: execution bytecode lookup + bus column + per-lookup memory reads. bus_num_vals: dict[str, EF] = {} bus_den_vals: dict[str, EF] = {} columns_values: dict[str, dict[int, EF]] = {} @@ -974,9 +904,8 @@ def pref_at(offset: int, log_height: int) -> EF: den = den + pref * (c - fp) offset += 1 << log_n_rows - # Bus column (data flow between tables). eval_on_selector = state.next_extension_scalar() - eval_on_data = state.next_extension_scalar() + eval_on_data = state.next_extension_scalar() pref = pref_at(offset, log_n_rows) num = num + pref * eval_on_selector den = den + pref * eval_on_data @@ -984,7 +913,6 @@ def pref_at(offset: int, log_height: int) -> EF: bus_den_vals[name] = eval_on_data offset += 1 << log_n_rows - # Lookups into memory. for index_col, value_cols in meta.lookups: index_eval = state.next_extension_scalar() assert index_col not in table_values @@ -994,17 +922,22 @@ def pref_at(offset: int, log_height: int) -> EF: assert col_index not in table_values table_values[col_index] = value_eval pref = pref_at(offset, log_n_rows) - fp = finger_print(dom_mem, [value_eval, index_eval + EF.from_base(Fp(i))], alphas_eq_poly) + fp = finger_print( + dom_mem, + [value_eval, index_eval + EF.from_base(Fp(i))], + alphas_eq_poly, + ) num = num + pref den = den + pref * (c - fp) offset += 1 << log_n_rows columns_values[name] = table_values - # Padding tail (zeros over the [offset .. 2^total_gkr_n_vars) region). den = den + mle_of_zeros_then_ones(offset, point_gkr) - if num != claim_num: raise ProofError("logup: numerators value mismatch") - if den != claim_den: raise ProofError("logup: denominators value mismatch") + if num != claim_num: + raise ProofError("logup: numerators value mismatch") + if den != claim_den: + raise ProofError("logup: denominators value mismatch") return { "value_memory": value_memory, @@ -1018,12 +951,15 @@ def pref_at(offset: int, log_height: int) -> EF: class ConstraintFolder: - """`flat` = current-row column evaluations; `shift` = next-row evaluations - restricted to the table's first `n_shift_columns`. Each `assert_zero(x)` - contributes `alpha_powers[i] · x` to the accumulator.""" + """`flat`/`shift` = current-row / next-row column evals. Each `assert_zero(x)` + adds `alpha_powers[i]·x` to the accumulator.""" def __init__(self, flat: Sequence[EF], shift: Sequence[EF], alpha_powers: Sequence[EF]) -> None: - self.flat, self.shift, self.alpha_powers = list(flat), list(shift), list(alpha_powers) + self.flat, self.shift, self.alpha_powers = ( + list(flat), + list(shift), + list(alpha_powers), + ) self.accumulator: EF = EF.zero() self.i = 0 @@ -1035,19 +971,18 @@ def assert_eq(self, x: EF, y: EF) -> None: self.assert_zero(x - y) def assert_bool(self, x: EF) -> None: - # bool_check(x) = x · (1 − x), zero iff x ∈ {0, 1}. self.assert_zero(x * (EF.one() - x)) def _eval_virtual_bus_column(extra_data: dict, flag: EF, data: Sequence[EF]) -> EF: - """`(Σ αᵢ · dataᵢ + α_last · LOGUP_PRECOMPILE_DOMAINSEP) · bus_beta + flag`.""" + """`(Σ αᵢ·dataᵢ + α_last·DOMAINSEP)·β + flag` (LOGUP_PRECOMPILE_DOMAINSEP = 1).""" alphas: list[EF] = extra_data["logup_alphas_eq_poly"] bus_beta: EF = extra_data["bus_beta"] assert len(data) < len(alphas) inner = EF.zero() for a, d in zip(alphas, data): inner = inner + a * d - inner = inner + alphas[-1] * EF.from_base(Fp(1)) # LOGUP_PRECOMPILE_DOMAINSEP = 1 + inner = inner + alphas[-1] * EF.from_base(Fp(1)) return inner * bus_beta + flag @@ -1057,12 +992,7 @@ def air_constraint_eval( alpha_powers: Sequence[EF], extra_data: dict, ) -> EF: - """Evaluate `table`'s AIR constraint polynomial at the given column evals. - - `col_evals[:n_columns]` is the `flat` row, `col_evals[n_columns:]` is the - `shift` (next-row) view, only present for tables with `n_shift_columns > 0`. - """ - folder = ConstraintFolder(col_evals[:table.n_columns], col_evals[table.n_columns:], alpha_powers) + folder = ConstraintFolder(col_evals[: table.n_columns], col_evals[table.n_columns :], alpha_powers) { "execution": _eval_air_execution, "extension_op": _eval_air_extension_op, @@ -1072,16 +1002,15 @@ def air_constraint_eval( def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: - # Column layout (execution/air.rs): pc, fp, addr_{a,b,c}, value_{a,b,c}, - # operand_{a,b,c}, flag_{a,b,c}, flag_c_fp, flag_ab_fp, mul, jump, aux, - # precompile_data. `shift[0..2]` carries the next row's (pc, fp). + # fmt: off (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, flag_ab_fp, mul, jump, aux, precompile_data) = folder.flat[:20] + # fmt: on pc_shift, fp_shift = folder.shift[0], folder.shift[1] one = EF.one() - # `nu_x = flag · operand + (1 − flag − flag_ab_fp) · value + flag_ab_fp · (fp + operand)`. + # nu_x = flag·operand + (1 − flag − flag_ab_fp)·value + flag_ab_fp·(fp + operand) nfa = -(flag_a + flag_ab_fp - one) nfb = -(flag_b + flag_ab_fp - one) nfc = -(flag_c + flag_c_fp - one) @@ -1089,37 +1018,30 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: nu_b = flag_b * operand_b + nfb * value_b + flag_ab_fp * (fp + operand_b) nu_c = flag_c * operand_c + nfc * value_c + flag_c_fp * (fp + operand_c) - # `aux` is a 2-bit gadget: aux=0→nothing, aux=1→add, aux=2→deref. From it - # we derive boolean flags (`add` / `deref`) and the precompile catch-all. - add = aux * EF.from_base(Fp(2)) - aux * aux + # aux ∈ {0,1,2}: 0=nothing, 1=add, 2=deref. + add = aux * EF.from_base(Fp(2)) - aux * aux deref = aux * (aux - one) * EF.from_base(_INV_TWO) is_precompile = -(add + mul + deref + jump - one) - # Constraint 1: precompile bus column. - folder.assert_zero(_eval_virtual_bus_column( - extra_data, is_precompile, [precompile_data, nu_a, nu_b, nu_c], - )) - # Constraints 2-4: address consistency on memory operands. - folder.assert_zero(nfa * (addr_a - (fp + operand_a))) - folder.assert_zero(nfb * (addr_b - (fp + operand_b))) - folder.assert_zero(nfc * (addr_c - (fp + operand_c))) - # Constraints 5-6: add / mul gates. - folder.assert_zero(add * (nu_b - (nu_a + nu_c))) - folder.assert_zero(mul * (nu_b - nu_a * nu_c)) - # Constraints 7-8: deref — `addr_b == value_a + operand_b` and `value_b == nu_c`. - folder.assert_zero(deref * (addr_b - (value_a + operand_b))) - folder.assert_zero(deref * (value_b - nu_c)) - # Constraints 9-13: jump control flow. + az = folder.assert_zero + az(_eval_virtual_bus_column(extra_data, is_precompile, [precompile_data, nu_a, nu_b, nu_c])) + az(nfa * (addr_a - (fp + operand_a))) + az(nfb * (addr_b - (fp + operand_b))) + az(nfc * (addr_c - (fp + operand_c))) + az(add * (nu_b - (nu_a + nu_c))) + az(mul * (nu_b - nu_a * nu_c)) + az(deref * (addr_b - (value_a + operand_b))) + az(deref * (value_b - nu_c)) jc = jump * nu_a - folder.assert_zero(jc * (nu_a - one)) - folder.assert_zero(jc * (pc_shift - nu_b)) - folder.assert_zero(jc * (fp_shift - nu_c)) + az(jc * (nu_a - one)) + az(jc * (pc_shift - nu_b)) + az(jc * (fp_shift - nu_c)) not_jc = -(jc - one) - folder.assert_zero(not_jc * (pc_shift - (pc + one))) - folder.assert_zero(not_jc * (fp_shift - fp)) + az(not_jc * (pc_shift - (pc + one))) + az(not_jc * (fp_shift - fp)) -# Bus-data magic numbers from extension_op/air.rs (precompile-data layout). +# extension_op/air.rs precompile-data layout. _EXT_OP_FLAG_IS_BE = 4 _EXT_OP_FLAG_ADD = 8 _EXT_OP_FLAG_MUL = 16 @@ -1128,8 +1050,7 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: - """Port of `quintic_mul` from `koalabear/quintic_extension/extension.rs` — - multiplication of 5-tuples of EF as quintic-extension elements.""" + """Port of `quintic_mul` (multiplication of two EF⁵ as quintic-extension elements).""" assert len(a) == 5 and len(b) == 5 b0m3, b1m4, b4m2 = b[0] - b[3], b[1] - b[4], b[4] - b[2] rows = [ @@ -1139,115 +1060,108 @@ def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: [b[3], b[2], b1m4, b0m3, b4m2], [b[4], b[3], b[2], b1m4, b0m3], ] + def dot(row: list[EF]) -> EF: acc = a[0] * row[0] for i in range(1, 5): acc = acc + a[i] * row[i] return acc + return [dot(row) for row in rows] def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: - # Column layout (extension_op/air.rs): the 13 shift columns - # (is_be, start, len, flag_{add,mul,poly_eq}, idx_{a,b}, comp[0..5]) - # occupy positions 0..13, then idx_res, va, vb, vres (5 each). - is_be, start, len_col, flag_add, flag_mul, flag_poly_eq, idx_a, idx_b = folder.flat[:8] - comp = folder.flat[8:13] - idx_res = folder.flat[13] - va, vb, vres = (folder.flat[14 + 5 * k : 14 + 5 * (k + 1)] for k in range(3)) - # `shift[j]` mirrors `flat[j]` for j ∈ 0..13 (convention). - (is_be_shift, start_shift, len_shift, flag_add_shift, flag_mul_shift, - flag_poly_eq_shift, idx_a_shift, idx_b_shift) = folder.shift[:8] - comp_shift = folder.shift[8:13] - one = EF.one() + # Layout: shift columns 0..13 = (is_be, start, len, flag_{add,mul,poly_eq}, + # idx_{a,b}, comp[0..5]); then idx_res, va, vb, vres (5 each). + f = folder.flat + is_be, start, len_col, flag_add, flag_mul, flag_poly_eq, idx_a, idx_b = f[:8] + comp, idx_res = f[8:13], f[13] + va, vb, vres = f[14:19], f[19:24], f[24:29] + s = folder.shift + is_be_sh, start_sh, len_sh, flag_add_sh, flag_mul_sh, flag_poly_eq_sh, idx_a_sh, idx_b_sh = s[:8] + comp_sh = s[8:13] + one, zero = EF.one(), EF.zero() + Fb = lambda v: EF.from_base(Fp(v)) + + aux = ( + is_be * Fb(_EXT_OP_FLAG_IS_BE) + + flag_add * Fb(_EXT_OP_FLAG_ADD) + + flag_mul * Fb(_EXT_OP_FLAG_MUL) + + flag_poly_eq * Fb(_EXT_OP_FLAG_POLY_EQ) + + len_col * Fb(_EXT_OP_LEN_MULTIPLIER) + ) + folder.assert_zero( + _eval_virtual_bus_column(extra_data, start * (flag_add + flag_mul + flag_poly_eq), [aux, idx_a, idx_b, idx_res]) + ) - # Constraint 1: precompile bus. - aux = (is_be * EF.from_base(Fp(_EXT_OP_FLAG_IS_BE)) - + flag_add * EF.from_base(Fp(_EXT_OP_FLAG_ADD)) - + flag_mul * EF.from_base(Fp(_EXT_OP_FLAG_MUL)) - + flag_poly_eq * EF.from_base(Fp(_EXT_OP_FLAG_POLY_EQ)) - + len_col * EF.from_base(Fp(_EXT_OP_LEN_MULTIPLIER))) - folder.assert_zero(_eval_virtual_bus_column( - extra_data, start * (flag_add + flag_mul + flag_poly_eq), [aux, idx_a, idx_b, idx_res], - )) - - # Constraints 2-6: bool flags. - for f in (is_be, start, flag_add, flag_mul, flag_poly_eq): - folder.assert_bool(f) + for x in (is_be, start, flag_add, flag_mul, flag_poly_eq): + folder.assert_bool(x) - # `va` is `Fp` when in base-extension mode, full EF otherwise; `comp_tail` - # carries the next chunk's comp when this row isn't a `start`. - is_ee, not_start_shift = -(is_be - one), -(start_shift - one) - va_f_or_ef = [va[0]] + [va[k] * is_ee for k in range(1, 5)] - comp_tail = [comp_shift[k] * not_start_shift for k in range(5)] - va_times_vb = _quintic_mul_ef(va_f_or_ef, vb) + is_ee, not_start_sh = -(is_be - one), -(start_sh - one) + va_x = [va[0]] + [va[k] * is_ee for k in range(1, 5)] + comp_tail = [comp_sh[k] * not_start_sh for k in range(5)] + va_vb = _quintic_mul_ef(va_x, vb) - # Constraints 7-11: add. for k in range(5): - folder.assert_zero((comp[k] - (va_f_or_ef[k] + vb[k] + comp_tail[k])) * flag_add) - # Constraints 12-16: mul. + folder.assert_zero((comp[k] - (va_x[k] + vb[k] + comp_tail[k])) * flag_add) for k in range(5): - folder.assert_zero((comp[k] - (va_times_vb[k] + comp_tail[k])) * flag_mul) + folder.assert_zero((comp[k] - (va_vb[k] + comp_tail[k])) * flag_mul) - # Constraints 17-21: poly_eq gate — `comp ← (2·va·vb − va − vb + 1) · comp_shift_or_one`. - poly_eq_val = [va_times_vb[k] + va_times_vb[k] - va_f_or_ef[k] - vb[k] + (one if k == 0 else EF.zero()) for k in range(5)] - comp_shift_or_one = [comp_shift[0] * not_start_shift + start_shift] + [comp_shift[k] * not_start_shift for k in range(1, 5)] - poly_eq_result = _quintic_mul_ef(poly_eq_val, comp_shift_or_one) + # poly_eq: comp ← (2·va·vb − va − vb + 1) · comp_sh_or_one. + poly_eq_val = [va_vb[k] + va_vb[k] - va_x[k] - vb[k] + (one if k == 0 else zero) for k in range(5)] + comp_sh_or_one = [comp_sh[0] * not_start_sh + start_sh] + [comp_sh[k] * not_start_sh for k in range(1, 5)] + poly_eq_result = _quintic_mul_ef(poly_eq_val, comp_sh_or_one) for k in range(5): folder.assert_zero((comp[k] - poly_eq_result[k]) * flag_poly_eq) - - # Constraints 22-26: result matches `comp` when `start`. for k in range(5): folder.assert_zero((comp[k] - vres[k]) * start) - # Constraints 27-31: shift-row consistency on non-start rows. - folder.assert_zero(not_start_shift * (len_col - len_shift - one)) - folder.assert_zero(not_start_shift * (is_be - is_be_shift)) - folder.assert_zero(not_start_shift * (flag_add - flag_add_shift)) - folder.assert_zero(not_start_shift * (flag_mul - flag_mul_shift)) - folder.assert_zero(not_start_shift * (flag_poly_eq - flag_poly_eq_shift)) + for x, y in [ + (len_col, len_sh + one), + (is_be, is_be_sh), + (flag_add, flag_add_sh), + (flag_mul, flag_mul_sh), + (flag_poly_eq, flag_poly_eq_sh), + ]: + folder.assert_zero(not_start_sh * (x - y)) - # Constraint 32-33: idx_a / idx_b increment. - a_increment = is_be + is_ee * EF.from_base(Fp(5)) # DIMENSION = 5 - folder.assert_zero(not_start_shift * (idx_a_shift - idx_a - a_increment)) - folder.assert_zero(not_start_shift * (idx_b_shift - idx_b - EF.from_base(Fp(5)))) - - # Constraint 34: `start_shift` enforces len=1. - folder.assert_zero(start_shift * (len_col - one)) + folder.assert_zero(not_start_sh * (idx_a_sh - idx_a - (is_be + is_ee * Fb(5)))) + folder.assert_zero(not_start_sh * (idx_b_sh - idx_b - Fb(5))) + folder.assert_zero(start_sh * (len_col - one)) @functools.cache def _p1c() -> dict: - """Poseidon1 round constants + matrices, lifted from the Rust-dumped JSON.""" import json from pathlib import Path + raw = json.loads(Path(__file__).with_name("poseidon1_constants.json").read_text()) fp_mat = lambda m: [[Fp(v) for v in row] for row in m] fp_vec = lambda v: [Fp(x) for x in v] return { - "half_full_rounds": raw["half_full_rounds"], - "partial_rounds": raw["partial_rounds"], + "half_full_rounds": raw["half_full_rounds"], + "partial_rounds": raw["partial_rounds"], "initial_constants": fp_mat(raw["initial_constants"]), - "final_constants": fp_mat(raw["final_constants"]), - "sparse_m_i": fp_mat(raw["sparse_m_i"]), - "sparse_first_row": fp_mat(raw["sparse_first_row"]), - "sparse_v": fp_mat(raw["sparse_v"]), - "sparse_first_rc": fp_vec(raw["sparse_first_round_constants"]), - "sparse_scalar_rc": fp_vec(raw["sparse_scalar_round_constants"]), - "mds_dense": fp_mat(raw["mds_dense"]), + "final_constants": fp_mat(raw["final_constants"]), + "sparse_m_i": fp_mat(raw["sparse_m_i"]), + "sparse_first_row": fp_mat(raw["sparse_first_row"]), + "sparse_v": fp_mat(raw["sparse_v"]), + "sparse_first_rc": fp_vec(raw["sparse_first_round_constants"]), + "sparse_scalar_rc": fp_vec(raw["sparse_scalar_round_constants"]), + "mds_dense": fp_mat(raw["mds_dense"]), } _POSEIDON_WIDTH = 16 _HALF_DIGEST_LEN = 4 -_POSEIDON_PERMUTE_SHIFT = 1 << 1 -_POSEIDON_HALF_OUTPUT_SHIFT = 1 << 2 -_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT = 1 << 3 -_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = 1 << 4 +_POSEIDON_PERMUTE_SHIFT, _POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1, 1 << 2 +_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT, _POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = ( + 1 << 3, + 1 << 4, +) def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: - """`mat × state` — base-field matrix times EF-vector.""" out: list[EF] = [] for row in mat: acc = EF.zero() @@ -1258,42 +1172,38 @@ def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: - """Two MDS-sandwiched cubic S-boxes — one "full round" pair.""" for rc in (rc1, rc2): - state = _matvec_kb(_p1c()["mds_dense"], [(s + EF.from_base(c)) * (s + EF.from_base(c)) * (s + EF.from_base(c)) for s, c in zip(state, rc)]) + state = _matvec_kb( + _p1c()["mds_dense"], + [(s + EF.from_base(c)) * (s + EF.from_base(c)) * (s + EF.from_base(c)) for s, c in zip(state, rc)], + ) return state def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) -> None: - """Evaluate the Poseidon1-16 permutation as AIR constraints. Each "post" - column commits an intermediate state; we assert local computation matches, - then *adopt* the committed value to bound polynomial degree.""" + """AIR for Poseidon1-16. Each `post` column commits an intermediate state, which we + constrain against the local computation, then adopt to bound polynomial degree.""" const = _p1c() state = list(cols["inputs"]) - initial = list(cols["inputs"]) # used for Davies-Meyer at the end + initial = list(cols["inputs"]) + half_initial = half_final = const["half_full_rounds"] // 2 - # Initial full rounds (HALF_INITIAL_FULL_ROUNDS = 2 pairs each). - half_initial = const["half_full_rounds"] // 2 for r in range(half_initial): state = _full_round(state, const["initial_constants"][2 * r], const["initial_constants"][2 * r + 1]) for i, post in enumerate(cols["beginning_full_rounds"][r]): folder.assert_eq(state[i], post) state[i] = post - # Transition into partial rounds (uses sparse `m_i`, no constraints). state = [s + EF.from_base(c) for s, c in zip(state, const["sparse_first_rc"])] state = _matvec_kb(const["sparse_m_i"], state) - # Partial rounds: S-box on `state[0]` only, then sparse linear layer. n_partial = const["partial_rounds"] for r in range(n_partial): - cubed = state[0] * state[0] * state[0] - folder.assert_eq(cubed, cols["partial_rounds"][r]) + folder.assert_eq(state[0] * state[0] * state[0], cols["partial_rounds"][r]) state[0] = cols["partial_rounds"][r] if r < n_partial - 1: state[0] = state[0] + const["sparse_scalar_rc"][r] old_s0 = state[0] - # new_s0 = ⟨first_row[r], state⟩ ; state[i] += old_s0 · v[r][i-1]. new_s0 = EF.zero() for j in range(_POSEIDON_WIDTH): new_s0 = new_s0 + state[j] * const["sparse_first_row"][r][j] @@ -1301,99 +1211,88 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) - for i in range(1, _POSEIDON_WIDTH): state[i] = state[i] + old_s0 * const["sparse_v"][r][i - 1] - # Ending full rounds except the very last pair. - half_final = const["half_full_rounds"] // 2 for r in range(half_final - 1): state = _full_round(state, const["final_constants"][2 * r], const["final_constants"][2 * r + 1]) for i, post in enumerate(cols["ending_full_rounds"][r]): folder.assert_eq(state[i], post) state[i] = post - # Last full round (no Davies-Meyer add here — the `+ initial[i]` is folded - # into the compression constraint below). Behaviour depends on flag_permute: - # - compression mode (flag_permute = 0): `outputs_left[i] = state[i] + initial[i]` - # for the first 4 lanes always; for lanes 4..8 only when flag_half_output = 0. - # - permute mode (flag_permute = 1): `outputs_left = state[:8]` and - # `outputs_right = state[8:]`. + # Last full round: compression mode adds `initial` (gated by flag_half_output for lanes 4..8); + # permute mode (flag_permute=1) outputs raw state. last = 2 * (half_final - 1) state = _full_round(state, const["final_constants"][last], const["final_constants"][last + 1]) flag_permute = cols["flag_permute"] not_permute = EF.one() - flag_permute compression_last4 = not_permute - cols["flag_half_output"] for i in range(_POSEIDON_WIDTH // 2): - compression_gate = not_permute if i < _HALF_DIGEST_LEN else compression_last4 - folder.assert_zero(compression_gate * (state[i] + initial[i] - cols["outputs_left"][i])) + gate = not_permute if i < _HALF_DIGEST_LEN else compression_last4 + folder.assert_zero(gate * (state[i] + initial[i] - cols["outputs_left"][i])) folder.assert_zero(flag_permute * (state[i] - cols["outputs_left"][i])) folder.assert_zero(flag_permute * (state[i + _POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: const = _p1c() - flat = folder.flat - one = EF.one() - W = _POSEIDON_WIDTH - half_initial = const["half_full_rounds"] // 2 - half_final = const["half_full_rounds"] // 2 + flat, one, W = folder.flat, EF.one(), _POSEIDON_WIDTH + half_initial = half_final = const["half_full_rounds"] // 2 + Fb = lambda v: EF.from_base(Fp(v)) - # Decode the Poseidon1Cols16 layout by sequential slicing. o = 0 + def take(n: int) -> list[EF]: nonlocal o chunk, o = flat[o : o + n], o + n return list(chunk) + # fmt: off [flag_active, index_b, index_res, flag_half_output, flag_hardcoded_left, - offset_hardcoded_left, effective_index_left_first, effective_index_left_second, - flag_permute] = take(9) - inputs = take(W) + offset_hardcoded_left, eff_idx_left_first, eff_idx_left_second, flag_permute] = take(9) + # fmt: on + inputs = take(W) beginning_full_rounds = [take(W) for _ in range(half_initial)] - partial_cols = take(const["partial_rounds"]) - ending_full_rounds = [take(W) for _ in range(half_final - 1)] - outputs_left = take(W // 2) - outputs_right = take(W // 2) + partial_cols = take(const["partial_rounds"]) + ending_full_rounds = [take(W) for _ in range(half_final - 1)] + outputs_left, outputs_right = take(W // 2), take(W // 2) - # Reconstruct `precompile_data` from the flags + offset. precompile_data = ( one - + flag_permute * EF.from_base(Fp(_POSEIDON_PERMUTE_SHIFT)) - + flag_half_output * EF.from_base(Fp(_POSEIDON_HALF_OUTPUT_SHIFT)) - + flag_hardcoded_left * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT)) - + flag_hardcoded_left * offset_hardcoded_left * EF.from_base(Fp(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT)) + + flag_permute * Fb(_POSEIDON_PERMUTE_SHIFT) + + flag_half_output * Fb(_POSEIDON_HALF_OUTPUT_SHIFT) + + flag_hardcoded_left * Fb(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) + + flag_hardcoded_left * offset_hardcoded_left * Fb(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT) ) not_hcl = one - flag_hardcoded_left - index_a = effective_index_left_second - not_hcl * EF.from_base(Fp(_HALF_DIGEST_LEN)) + index_a = eff_idx_left_second - not_hcl * Fb(_HALF_DIGEST_LEN) - # Constraint 1: bus column. - folder.assert_zero(_eval_virtual_bus_column( - extra_data, flag_active, [precompile_data, index_a, index_b, index_res], - )) - # Constraints 2-5: bool flags. + folder.assert_zero( + _eval_virtual_bus_column(extra_data, flag_active, [precompile_data, index_a, index_b, index_res]) + ) for f in (flag_active, flag_half_output, flag_hardcoded_left, flag_permute): folder.assert_bool(f) - # Constraint 6: mutex — flag_permute can't coexist with half-output or hardcoded-left. folder.assert_zero(flag_permute * (flag_half_output + flag_hardcoded_left)) - # Constraints 7-8: hardcoded-left consistency. - folder.assert_zero(flag_hardcoded_left * (offset_hardcoded_left - effective_index_left_first)) - folder.assert_zero(not_hcl * (index_a - effective_index_left_first)) - - _eval_poseidon1_16(folder, { - "inputs": inputs, - "beginning_full_rounds": beginning_full_rounds, - "partial_rounds": partial_cols, - "ending_full_rounds": ending_full_rounds, - "outputs_left": outputs_left, - "outputs_right": outputs_right, - "flag_half_output": flag_half_output, - "flag_permute": flag_permute, - }, extra_data) - - -# Per-table compile-time spec (Rust: `
::{degree_air, n_constraints, -# n_shift_columns}`). By convention the shift columns occupy positions `0..n_shift`. + folder.assert_zero(flag_hardcoded_left * (offset_hardcoded_left - eff_idx_left_first)) + folder.assert_zero(not_hcl * (index_a - eff_idx_left_first)) + + _eval_poseidon1_16( + folder, + { + "inputs": inputs, + "beginning_full_rounds": beginning_full_rounds, + "partial_rounds": partial_cols, + "ending_full_rounds": ending_full_rounds, + "outputs_left": outputs_left, + "outputs_right": outputs_right, + "flag_half_output": flag_half_output, + "flag_permute": flag_permute, + }, + extra_data, + ) + + _TABLE_SPECS: dict[str, dict] = { - "execution": {"degree": 5, "n_constraints": 13, "n_shift": 2}, - "extension_op": {"degree": 4, "n_constraints": 33, "n_shift": 13}, - "poseidon16_compress": {"degree": 10, "n_constraints": 99, "n_shift": 0}, + "execution": {"degree": 5, "n_constraints": 13, "n_shift": 2}, + "extension_op": {"degree": 4, "n_constraints": 33, "n_shift": 13}, + "poseidon16_compress": {"degree": 10, "n_constraints": 99, "n_shift": 0}, } @@ -1410,7 +1309,6 @@ def verify_execution( state.observe_scalars(list(public_input)) state.observe_scalars(poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP)) - # --- Prologue: read & validate the verifier-side dimensions. --- dims = [int(x.value) for x in state.next_base_scalars_vec(3 + len(tables))] log_inv_rate, log_memory, public_input_len, *table_log_n_rows = dims if public_input_len != len(public_input): @@ -1430,13 +1328,12 @@ def verify_execution( tables_by_name = {t.name: t for t in tables} tables_sorted = sort_tables_by_height(table_log_heights) - # --- Stacked-PCS commitment parse. --- - # Sizing invariants: memory ≥ execution ≥ all other tables; stacked length - # = 2·memory + bytecode-acc (padded to tallest table) + Σ n_cols × 2^log_n. + # memory ≥ execution ≥ all other tables. if log_memory < table_log_heights["execution"] or table_log_heights["execution"] < tables_sorted[0][1]: raise ProofError("InvalidProof: memory or execution table size invariants broken") total_stacked = ( - (2 << log_memory) + (1 << max(bytecode_log_size, tables_sorted[0][1])) + (2 << log_memory) + + (1 << max(bytecode_log_size, tables_sorted[0][1])) + sum(t.n_columns << table_log_heights[t.name] for t in tables) ) stacked_n_vars = log2_ceil_usize(total_stacked) @@ -1445,30 +1342,42 @@ def verify_execution( cfg = whir_config(log_inv_rate, stacked_n_vars) root = state.next_base_scalars_vec(DIGEST_ELEMS) ood_points = state.sample_vec(cfg["commitment_ood_samples"]) if cfg["commitment_ood_samples"] else [] - ood_answers = state.next_extension_scalars_vec(cfg["commitment_ood_samples"]) if cfg["commitment_ood_samples"] else [] + ood_answers = ( + state.next_extension_scalars_vec(cfg["commitment_ood_samples"]) if cfg["commitment_ood_samples"] else [] + ) parsed_commitment = ParsedCommitment(stacked_n_vars, root, ood_points, ood_answers) - # --- Logup phase. --- - logup_c = state.sample(); state.duplex() + logup_c = state.sample() + state.duplex() max_bus_width = 1 + max(constants["max_precompile_bus_width"], constants["n_instruction_columns"]) logup_alphas = state.sample_vec(log2_ceil_usize(max_bus_width)) logup_alphas_eq = eval_eq(logup_alphas) logup = verify_generic_logup( - state, logup_c, logup_alphas, logup_alphas_eq, log_memory, bytecode_multilinear, - table_log_heights, tables_by_name, constants, + state, + logup_c, + logup_alphas, + logup_alphas_eq, + log_memory, + bytecode_multilinear, + table_log_heights, + tables_by_name, + constants, ) gkr_point = logup["gkr_point"] - # --- AIR phase: bus → batched-degree sumcheck → per-table constraint check. --- - bus_beta = state.sample(); state.duplex() - air_alpha = state.sample(); state.duplex() + bus_beta = state.sample() + state.duplex() + air_alpha = state.sample() + state.duplex() eta = state.sample() - # alpha_powers = [1, α, α², …]; eta_powers = [1, η, η², …]. + def powers(x: EF, n: int) -> list[EF]: out, cur = [], EF.one() for _ in range(n): - out.append(cur); cur = cur * x + out.append(cur) + cur = cur * x return out + alpha_powers = powers(air_alpha, max(_TABLE_SPECS[n]["n_constraints"] for n in tables_by_name) + 1) eta_powers = powers(eta, len(tables_sorted)) extra_data = {"logup_alphas_eq_poly": logup_alphas_eq, "bus_beta": bus_beta, "c": logup_c} @@ -1481,13 +1390,12 @@ def powers(x: EF, n: int) -> list[EF]: logup["bus_num"][name] * sign + bus_beta * (logup["bus_den"][name] - logup_c) ) n_max = tables_sorted[0][1] - sc = verify_sumcheck(state, initial_sum, n_max, max(_TABLE_SPECS[n]["degree"] + 1 for n, _ in tables_sorted)) + sc_point, sc_value = verify_sumcheck( + state, initial_sum, n_max, max(_TABLE_SPECS[n]["degree"] + 1 for n, _ in tables_sorted) + ) - # Per-table: read col_evals, evaluate AIR, accumulate. Each table's committed - # statements start with its logup eq-values entry. committed = { - name: [(list(from_end(gkr_point, table_log_heights[name])), - dict(logup["columns_values"][name]), {})] + name: [(list(from_end(gkr_point, table_log_heights[name])), dict(logup["columns_values"][name]), {})] for name in tables_by_name } my_air_final = EF.zero() @@ -1496,42 +1404,43 @@ def powers(x: EF, n: int) -> list[EF]: col_evals = state.next_extension_scalars_vec(meta.n_columns + n_shift) constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) - # Per-table contribution = η^t · (Π unused-prefix coords) · eq · C(col_evals). - natural_pt = list(reversed(sc.point[-log_n_rows:])) if log_n_rows else [] + natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = EF.one() - for x in sc.point[: n_max - log_n_rows]: + for x in sc_point[: n_max - log_n_rows]: k_t = k_t * x - my_air_final = my_air_final + eta_pow * k_t * eq_poly_outside( - from_end(gkr_point, log_n_rows), natural_pt - ) * constraint_eval + my_air_final = ( + my_air_final + + eta_pow * k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval + ) - # Shift column `j` is column `j` of the table (by convention). eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} next_vals = {j: col_evals[meta.n_columns + j] for j in range(n_shift)} committed[name].append((natural_pt, eq_vals, next_vals)) - if my_air_final != sc.value: + if my_air_final != sc_value: raise ProofError("AIR sumcheck: claimed value mismatch") - # Public-memory MLE evaluation at a fresh random point. public_memory = padd_with_zero_to_next_power_of_two(public_input) pm_point = state.sample_vec(log2_strict_usize(len(public_memory))) pm_eval = eval_multilinear_evals([EF.from_base(f) for f in public_memory], pm_point) - # --- WHIR finale. Seed previous_statements with memory + public-memory + bytecode-acc claims. --- mk = lambda point, values: SparseStatement(stacked_n_vars, list(point), values) previous = [ - mk(from_end(gkr_point, log_memory), [ - (0, logup["value_memory"]), - (1, logup["value_memory_acc"]), - ]), + mk(from_end(gkr_point, log_memory), [(0, logup["value_memory"]), (1, logup["value_memory_acc"])]), mk(pm_point, [(0, pm_eval)]), - mk(from_end(gkr_point, bytecode_log_size), [ - ((2 << log_memory) >> bytecode_log_size, logup["value_bytecode_acc"]), - ]), + mk( + from_end(gkr_point, bytecode_log_size), + [((2 << log_memory) >> bytecode_log_size, logup["value_bytecode_acc"])], + ), ] global_statements = stacked_pcs_global_statements( - stacked_n_vars, log_memory, bytecode_log_size, previous, - table_log_heights, committed, tables_by_name, constants, + stacked_n_vars, + log_memory, + bytecode_log_size, + previous, + table_log_heights, + committed, + tables_by_name, + constants, ) whir_verify(state, cfg, parsed_commitment, global_statements) @@ -1539,8 +1448,7 @@ def powers(x: EF, n: int) -> list[EF]: def poseidon_compress_slice_iv(data: Sequence[Fp]) -> list[Fp]: - """Hash a multiple-of-8 sequence with Poseidon16/Davies-Meyer, seeded by - an all-zero IV — matches `utils::poseidon_compress_slice(.., use_iv=true)`.""" + """Hash a multiple-of-8 sequence (Poseidon16/Davies-Meyer, all-zero IV).""" assert data and len(data) % 8 == 0 h = [Fp(0)] * 8 for i in range(0, len(data), 8): @@ -1549,27 +1457,28 @@ def poseidon_compress_slice_iv(data: Sequence[Fp]) -> list[Fp]: def main() -> int: - """Load the end-to-end test vector and run `verify_execution`.""" import array, json from pathlib import Path vector_path = Path(__file__).resolve().parents[2] / "target" / "zkvm_test_vectors" / "proof.json" if not vector_path.exists(): - print(f"Test vector not found at {vector_path}. Generate it first with:\n" - " cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture") + print( + f"Test vector not found at {vector_path}. Generate it first with:\n" + " cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture" + ) return 1 print(f"Loading {vector_path.name}...") raw = json.loads(vector_path.read_text()) - # Bytecode multilinear (raw u32 LE sidecar). - arr = array.array("I"); arr.frombytes((vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes()) + arr = array.array("I") + arr.frombytes((vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes()) assert len(arr) == raw["bytecode_multilinear_len"] bytecode_multilinear: list[int] = list(arr) fp_list = lambda xs: [Fp(v) for v in xs] public_input = fp_list(raw["public_input"]) - input_data = fp_list(raw["input_data"]) + input_data = fp_list(raw["input_data"]) proof = Proof( transcript=fp_list(raw["proof"]["transcript"]), merkle_openings=[ @@ -1578,16 +1487,19 @@ def main() -> int: ], ) - # Sanity: re-derive `public_input` from `input_data` to check the hash. if poseidon_compress_slice_iv(input_data) != public_input: print("FAIL: poseidon_compress_slice(input_data) doesn't match dumped public_input") return 1 try: result = verify_execution( - fp_list(raw["bytecode_hash"]), raw["bytecode_log_size"], - public_input, proof, - tables_from_json(raw["tables"]), raw["constants"], bytecode_multilinear, + fp_list(raw["bytecode_hash"]), + raw["bytecode_log_size"], + public_input, + proof, + tables_from_json(raw["tables"]), + raw["constants"], + bytecode_multilinear, ) except ProofError as e: print(f"FAIL: {e}") @@ -1599,4 +1511,5 @@ def main() -> int: if __name__ == "__main__": import sys as _sys + _sys.exit(main()) From 7e534822ee48de4c8bfe69b7898cd03e76fe41b6 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 19 May 2026 16:43:41 +0100 Subject: [PATCH 15/69] logup: use a single field element for domain separation between byetcode / memory / poseidon / extension_op interractions --- crates/backend/air/src/symbolic.rs | 27 ++++++---- .../lean_compiler/src/instruction_encoder.rs | 6 +-- crates/lean_prover/src/prove_execution.rs | 2 +- crates/lean_prover/src/verify_execution.rs | 2 +- crates/lean_vm/src/core/constants.rs | 5 +- crates/lean_vm/src/tables/execution/air.rs | 16 +++--- crates/lean_vm/src/tables/execution/mod.rs | 15 +++--- crates/lean_vm/src/tables/extension_op/air.rs | 16 +++--- .../lean_vm/src/tables/extension_op/exec.rs | 5 +- crates/lean_vm/src/tables/extension_op/mod.rs | 21 ++++---- crates/lean_vm/src/tables/mod.rs | 11 ++-- crates/lean_vm/src/tables/poseidon_16/mod.rs | 53 ++++++++++--------- crates/lean_vm/src/tables/table_enum.rs | 4 +- crates/lean_vm/src/tables/table_trait.rs | 3 +- crates/lean_vm/src/tables/utils.rs | 9 ++-- crates/rec_aggregation/src/compilation.rs | 29 +++++----- .../rec_aggregation/zkdsl_implem/recursion.py | 13 +++-- crates/sub_protocols/src/logup.rs | 51 ++++++++++-------- .../sub_protocols/tests/prove_poseidon_16.rs | 4 +- crates/utils/src/multilinear.rs | 12 ++--- 20 files changed, 161 insertions(+), 143 deletions(-) diff --git a/crates/backend/air/src/symbolic.rs b/crates/backend/air/src/symbolic.rs index a23c804d0..d09ac7b5a 100644 --- a/crates/backend/air/src/symbolic.rs +++ b/crates/backend/air/src/symbolic.rs @@ -252,7 +252,8 @@ struct SymbolicAirBuilder { flat: Vec>, shift: Vec>, constraints: Vec>, - bus_flag_value: Option>, + bus_multiplicity_value: Option>, + bus_discriminator_value: Option>, bus_data_values: Option>>, } @@ -269,7 +270,8 @@ impl SymbolicAirBuilder { flat, shift, constraints: Vec::new(), - bus_flag_value: None, + bus_multiplicity_value: None, + bus_discriminator_value: None, bus_data_values: None, } } @@ -300,10 +302,15 @@ impl AirBuilder for SymbolicAirBuilder { self.constraints.push(x); } + /// Bus declaration: called three times — first the multiplicity, then the + /// fingerprint discriminator, then the data tuple. fn declare_values(&mut self, values: &[Self::IF]) { - if self.bus_flag_value.is_none() { + if self.bus_multiplicity_value.is_none() { assert_eq!(values.len(), 1); - self.bus_flag_value = Some(values[0]); + self.bus_multiplicity_value = Some(values[0]); + } else if self.bus_discriminator_value.is_none() { + assert_eq!(values.len(), 1); + self.bus_discriminator_value = Some(values[0]); } else { assert!(self.bus_data_values.is_none()); self.bus_data_values = Some(values.to_vec()); @@ -311,13 +318,14 @@ impl AirBuilder for SymbolicAirBuilder { } } -pub fn get_symbolic_constraints_and_bus_data_values( - air: &A, -) -> ( +pub type SymbolicAirData = ( Vec>, SymbolicExpression, + SymbolicExpression, Vec>, -) +); + +pub fn get_symbolic_constraints_and_bus_data_values(air: &A) -> SymbolicAirData where A::ExtraData: Default, { @@ -328,7 +336,8 @@ where air.eval(&mut builder, &Default::default()); ( builder.constraints(), - builder.bus_flag_value.unwrap(), + builder.bus_multiplicity_value.unwrap(), + builder.bus_discriminator_value.unwrap(), builder.bus_data_values.unwrap(), ) } diff --git a/crates/lean_compiler/src/instruction_encoder.rs b/crates/lean_compiler/src/instruction_encoder.rs index b9bb1ea08..84c36f14b 100644 --- a/crates/lean_compiler/src/instruction_encoder.rs +++ b/crates/lean_compiler/src/instruction_encoder.rs @@ -47,7 +47,7 @@ pub fn field_representation(instr: &Instruction) -> [F; N_INSTRUCTION_COLUMNS] { set_nu_c(&mut fields, updated_fp); } Instruction::Precompile(precompile) => { - let precompile_data = match &precompile.data { + let discriminator = match &precompile.data { PrecompileCompTimeArgs::Poseidon16 { half_output, hardcoded_offset_left, @@ -55,7 +55,7 @@ pub fn field_representation(instr: &Instruction) -> [F; N_INSTRUCTION_COLUMNS] { } => { let flag_left = hardcoded_offset_left.is_some() as usize; let hardcoded_offset_left_val = hardcoded_offset_left.unwrap_or(0); - POSEIDON_PRECOMPILE_DATA + POSEIDON_DISCRIMINATOR_BASE + POSEIDON_PERMUTE_SHIFT * (*permute as usize) + POSEIDON_HALF_OUTPUT_SHIFT * (*half_output as usize) + POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT * flag_left @@ -66,7 +66,7 @@ pub fn field_representation(instr: &Instruction) -> [F; N_INSTRUCTION_COLUMNS] { mode.flag_encoding() + EXT_OP_LEN_MULTIPLIER * size } }; - fields[instr_idx(COL_PRECOMPILE_DATA)] = F::from_usize(precompile_data); + fields[instr_idx(COL_PRECOMPILE_DISCRIMINATOR)] = F::from_usize(discriminator); match (precompile.arg_0, precompile.arg_1) { (MemOrFpOrConstant::FpRelative { offset: off_a }, MemOrFpOrConstant::FpRelative { offset: off_b }) => { fields[instr_idx(COL_FLAG_AB_FP)] = F::ONE; diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 7cc6aee8d..91cae5a57 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -122,7 +122,7 @@ pub fn prove_execution( // logup (GKR) let logup_c = prover_state.sample(); prover_state.duplex(); - let logup_alphas = prover_state.sample_vec(log2_ceil_usize(max_bus_width_including_domainsep())); + let logup_alphas = prover_state.sample_vec(log2_ceil_usize(max_bus_width_including_bytecode())); let logup_alphas_eq_poly = eval_eq(&logup_alphas); let logup_statements = prove_generic_logup( diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 9cafa9433..b563fc2f1 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -72,7 +72,7 @@ pub fn verify_execution( let logup_c = verifier_state.sample(); verifier_state.duplex(); - let logup_alphas = verifier_state.sample_vec(log2_ceil_usize(max_bus_width_including_domainsep())); + let logup_alphas = verifier_state.sample_vec(log2_ceil_usize(max_bus_width_including_bytecode())); let logup_alphas_eq_poly = eval_eq(&logup_alphas); let logup_statements = verify_generic_logup( diff --git a/crates/lean_vm/src/core/constants.rs b/crates/lean_vm/src/core/constants.rs index 1f34acdae..6873255d4 100644 --- a/crates/lean_vm/src/core/constants.rs +++ b/crates/lean_vm/src/core/constants.rs @@ -1,9 +1,8 @@ use crate::Table; /// Domain separation in logup -pub const LOGUP_MEMORY_DOMAINSEP: usize = 0; -pub const LOGUP_PRECOMPILE_DOMAINSEP: usize = 1; -pub const LOGUP_BYTECODE_DOMAINSEP: usize = 2; +pub const LOGUP_MEMORY_DISCRIMINATOR: usize = 1; +pub const LOGUP_BYTECODE_DISCRIMINATOR: usize = 2; /// Large field = extension field of degree DIMENSION over koala-bear pub const DIMENSION: usize = 5; diff --git a/crates/lean_vm/src/tables/execution/air.rs b/crates/lean_vm/src/tables/execution/air.rs index 8811536bc..35aa211fd 100644 --- a/crates/lean_vm/src/tables/execution/air.rs +++ b/crates/lean_vm/src/tables/execution/air.rs @@ -27,7 +27,7 @@ pub const COL_FLAG_AB_FP: usize = 15; pub const COL_MUL: usize = 16; pub const COL_JUMP: usize = 17; pub const COL_AUX: usize = 18; -pub const COL_PRECOMPILE_DATA: usize = 19; +pub const COL_PRECOMPILE_DISCRIMINATOR: usize = 19; // Temporary columns (stored to avoid duplicate computations) pub const N_TEMPORARY_EXEC_COLUMNS: usize = 4; @@ -67,7 +67,7 @@ impl Air for ExecutionTable { let mul = flat[COL_MUL]; let jump = flat[COL_JUMP]; let aux = flat[COL_AUX]; - let precompile_data = flat[COL_PRECOMPILE_DATA]; + let discriminator = flat[COL_PRECOMPILE_DISCRIMINATOR]; let (value_a, value_b, value_c) = (flat[COL_MEM_VALUE_A], flat[COL_MEM_VALUE_B], flat[COL_MEM_VALUE_C]); let pc = flat[COL_PC]; @@ -94,17 +94,19 @@ impl Air for ExecutionTable { let add = aux * AB::F::TWO - aux * aux; let deref = (aux * (aux - AB::F::ONE)).halve(); - let is_precompile = -(add + mul + deref + jump - AB::F::ONE); + let multiplicity = -(add + mul + deref + jump - AB::F::ONE); if BUS { builder.assert_zero_ef(eval_virtual_bus_column::( extra_data, - is_precompile, - &[precompile_data, nu_a, nu_b, nu_c], + multiplicity, + discriminator, + &[nu_a, nu_b, nu_c], )); } else { - builder.declare_values(&[is_precompile]); - builder.declare_values(&[precompile_data, nu_a, nu_b, nu_c]); + builder.declare_values(&[multiplicity]); + builder.declare_values(std::slice::from_ref(&discriminator)); + builder.declare_values(&[nu_a, nu_b, nu_c]); } builder.assert_zero(one_minus_flag_a_and_flag_ab_fp * (addr_a - fp_plus_operand_a)); diff --git a/crates/lean_vm/src/tables/execution/mod.rs b/crates/lean_vm/src/tables/execution/mod.rs index 93f6b2df9..b854d350d 100644 --- a/crates/lean_vm/src/tables/execution/mod.rs +++ b/crates/lean_vm/src/tables/execution/mod.rs @@ -42,17 +42,16 @@ impl TableT for ExecutionTable { ] } - #[allow(clippy::vec_init_then_push)] // https://github.com/leanEthereum/leanMultisig/issues/198 fn bus(&self) -> Bus { - let mut data = Vec::with_capacity(4); - data.push(BusData::Column(COL_PRECOMPILE_DATA)); - data.push(BusData::Column(COL_EXEC_NU_A)); - data.push(BusData::Column(COL_EXEC_NU_B)); - data.push(BusData::Column(COL_EXEC_NU_C)); Bus { direction: BusDirection::Push, - selector: COL_IS_PRECOMPILE, - data, + multiplicity: COL_IS_PRECOMPILE, + discriminator: BusData::Column(COL_PRECOMPILE_DISCRIMINATOR), + data: vec![ + BusData::Column(COL_EXEC_NU_A), + BusData::Column(COL_EXEC_NU_B), + BusData::Column(COL_EXEC_NU_C), + ], } } diff --git a/crates/lean_vm/src/tables/extension_op/air.rs b/crates/lean_vm/src/tables/extension_op/air.rs index 5070557c6..0020f92de 100644 --- a/crates/lean_vm/src/tables/extension_op/air.rs +++ b/crates/lean_vm/src/tables/extension_op/air.rs @@ -27,8 +27,8 @@ pub(super) const COL_VB: usize = 19; pub(super) const COL_VRES: usize = 24; // Virtual columns (not explicitely in AIR) -pub(super) const COL_ACTIVATION_FLAG: usize = 29; -pub(super) const COL_AUX_EXTENSION_OP: usize = 30; +pub(super) const COL_MULTIPLICITY_EXTENSION_OP: usize = 29; +pub(super) const COL_DISCRIMINATOR_EXTENSION_OP: usize = 30; use backend::quintic_extension::extension::quintic_mul; @@ -86,7 +86,7 @@ impl Air for ExtensionOpPrecompile { let comp_shift: [AB::IF; 5] = std::array::from_fn(|k| shift[COL_COMP + k]); let active = flag_add + flag_mul + flag_poly_eq; - let activation_flag = start * active; + let multiplicity = start * active; let aux = is_be * AB::F::from_usize(EXT_OP_FLAG_IS_BE) + flag_add * AB::F::from_usize(EXT_OP_FLAG_ADD) @@ -99,12 +99,14 @@ impl Air for ExtensionOpPrecompile { if BUS { builder.assert_zero_ef(eval_virtual_bus_column::( extra_data, - activation_flag, - &[aux, idx_a, idx_b, idx_r], + multiplicity, + aux, + &[idx_a, idx_b, idx_r], )); } else { - builder.declare_values(&[activation_flag]); - builder.declare_values(&[aux, idx_a, idx_b, idx_r]); + builder.declare_values(&[multiplicity]); + builder.declare_values(std::slice::from_ref(&aux)); + builder.declare_values(&[idx_a, idx_b, idx_r]); } let is_ee = -(is_be - AB::F::ONE); diff --git a/crates/lean_vm/src/tables/extension_op/exec.rs b/crates/lean_vm/src/tables/extension_op/exec.rs index d0770d9b9..276203dd2 100644 --- a/crates/lean_vm/src/tables/extension_op/exec.rs +++ b/crates/lean_vm/src/tables/extension_op/exec.rs @@ -183,8 +183,9 @@ pub(super) fn exec_multi_row( } // Virtual columns - trace.columns[COL_ACTIVATION_FLAG].push(F::from_bool(is_start)); - trace.columns[COL_AUX_EXTENSION_OP].push(F::from_usize(mode_bits + EXT_OP_LEN_MULTIPLIER * current_len)); + trace.columns[COL_MULTIPLICITY_EXTENSION_OP].push(F::from_bool(is_start)); + trace.columns[COL_DISCRIMINATOR_EXTENSION_OP] + .push(F::from_usize(mode_bits + EXT_OP_LEN_MULTIPLIER * current_len)); } Ok(()) diff --git a/crates/lean_vm/src/tables/extension_op/mod.rs b/crates/lean_vm/src/tables/extension_op/mod.rs index 5f72b65c2..e70e42c14 100644 --- a/crates/lean_vm/src/tables/extension_op/mod.rs +++ b/crates/lean_vm/src/tables/extension_op/mod.rs @@ -6,7 +6,7 @@ use air::*; mod exec; pub use exec::fill_trace_extension_op; -// `PRECOMPILE_DATA` encoding: see `tables/mod.rs`. +// Discriminator encoding: see `tables/mod.rs`. pub(crate) const EXT_OP_FLAG_IS_BE: usize = 4; pub(crate) const EXT_OP_FLAG_ADD: usize = 8; pub(crate) const EXT_OP_FLAG_MUL: usize = 16; @@ -104,29 +104,28 @@ impl TableT for ExtensionOpPrecompile { ] } - #[allow(clippy::vec_init_then_push)] // https://github.com/leanEthereum/leanMultisig/issues/198 fn bus(&self) -> Bus { - let mut data = Vec::with_capacity(4); - data.push(BusData::Column(COL_AUX_EXTENSION_OP)); - data.push(BusData::Column(COL_IDX_A)); - data.push(BusData::Column(COL_IDX_B)); - data.push(BusData::Column(COL_IDX_RES)); Bus { direction: BusDirection::Pull, - selector: COL_ACTIVATION_FLAG, - data, + multiplicity: COL_MULTIPLICITY_EXTENSION_OP, + discriminator: BusData::Column(COL_DISCRIMINATOR_EXTENSION_OP), + data: vec![ + BusData::Column(COL_IDX_A), + BusData::Column(COL_IDX_B), + BusData::Column(COL_IDX_RES), + ], } } fn n_columns_total(&self) -> usize { - self.n_columns() + 2 // +2 for COL_ACTIVATION_FLAG and COL_AUX_EXTENSION_OP (non-AIR, used in bus logup) + self.n_columns() + 2 // +2 for COL_MULTIPLICITY_EXTENSION_OP and COL_DISCRIMINATOR_EXTENSION_OP (non-AIR, used in bus logup) } fn padding_row(&self, zero_vec_ptr: usize, _null_hash_ptr: usize, _ending_pc: usize) -> Vec { let mut row = vec![F::ZERO; self.n_columns_total()]; row[COL_START] = F::ONE; row[COL_LEN] = F::ONE; - row[COL_AUX_EXTENSION_OP] = F::from_usize(EXT_OP_LEN_MULTIPLIER); + row[COL_DISCRIMINATOR_EXTENSION_OP] = F::from_usize(EXT_OP_LEN_MULTIPLIER); row[COL_IDX_A] = F::from_usize(zero_vec_ptr); row[COL_IDX_B] = F::from_usize(zero_vec_ptr); row[COL_IDX_RES] = F::from_usize(zero_vec_ptr); diff --git a/crates/lean_vm/src/tables/mod.rs b/crates/lean_vm/src/tables/mod.rs index cc652a185..125ff6d23 100644 --- a/crates/lean_vm/src/tables/mod.rs +++ b/crates/lean_vm/src/tables/mod.rs @@ -16,10 +16,11 @@ pub use execution::*; mod utils; pub(crate) use utils::*; -// `PRECOMPILE_DATA` is the bus discriminator separating the two precompile -// tables. Disjointness is by parity of bit 0: +// In logup interractions, the `discriminator` separates the two precompile tables from each other (Poseidon16 is odd, ExtensionOp +// is a multiple of 4), and — since every value is odd `>= 3` (Poseidon16) or a multiple +// of 4 (ExtensionOp) — also from the memory and bytecode lookups, whose reserved +// discriminators are respectively 1 and 2. // -// Poseidon16 (odd): 1 + 2·flag_permute + 4·flag_half + 8·flag_left + 16·flag_left·offset_left -// ExtensionOp (even): 4·is_be + 8·flag_add + 16·flag_mul + 32·flag_poly_eq + 64·len +// Poseidon16 (odd >= 3): 3 + 2·flag_permute + 4·flag_half + 8·flag_left + 16·flag_left·offset_left +// ExtensionOp (0 mod 4): 4·is_be + 8·flag_add + 16·flag_mul + 32·flag_poly_eq + 64·len // -// Multiplying `offset_left` by `flag_left` is needed for soundness: see 3.4.1 in minimal_zkVM.pdf diff --git a/crates/lean_vm/src/tables/poseidon_16/mod.rs b/crates/lean_vm/src/tables/poseidon_16/mod.rs index ececb8577..1394a3931 100644 --- a/crates/lean_vm/src/tables/poseidon_16/mod.rs +++ b/crates/lean_vm/src/tables/poseidon_16/mod.rs @@ -89,14 +89,14 @@ const HALF_INITIAL_FULL_ROUNDS: usize = POSEIDON1_HALF_FULL_ROUNDS / 2; const PARTIAL_ROUNDS: usize = POSEIDON1_PARTIAL_ROUNDS; const HALF_FINAL_FULL_ROUNDS: usize = POSEIDON1_HALF_FULL_ROUNDS / 2; -// `PRECOMPILE_DATA` encoding: see `tables/mod.rs`. -pub const POSEIDON_PRECOMPILE_DATA: usize = 1; +// Discriminator encoding: see `tables/mod.rs`. +pub const POSEIDON_DISCRIMINATOR_BASE: usize = 3; pub const POSEIDON_PERMUTE_SHIFT: usize = 1 << 1; pub const POSEIDON_HALF_OUTPUT_SHIFT: usize = 1 << 2; pub const POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT: usize = 1 << 3; pub const POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT: usize = 1 << 4; -pub const POSEIDON_16_COL_FLAG: ColIndex = 0; +pub const POSEIDON_16_COL_MULTIPLICITY: ColIndex = 0; pub const POSEIDON_16_COL_INDEX_INPUT_RIGHT: ColIndex = 1; pub const POSEIDON_16_COL_INDEX_INPUT_RES: ColIndex = 2; pub const POSEIDON_16_COL_FLAG_HALF_OUTPUT: ColIndex = 3; @@ -110,7 +110,7 @@ pub const POSEIDON_16_COL_OUTPUT_LEFT: ColIndex = num_cols_poseidon_16() - 16; pub const POSEIDON_16_COL_OUTPUT_RIGHT: ColIndex = num_cols_poseidon_16() - 8; /// Non-committed columns ("virtual"): pub const POSEIDON_16_COL_INDEX_INPUT_LEFT: ColIndex = num_cols_poseidon_16(); -pub const POSEIDON_16_COL_PRECOMPILE_DATA: ColIndex = num_cols_poseidon_16() + 1; +pub const POSEIDON_16_COL_DISCRIMINATOR: ColIndex = num_cols_poseidon_16() + 1; pub const POSEIDON16_NAME: &str = "poseidon16_compress"; pub const POSEIDON16_HALF_NAME: &str = "poseidon16_compress_half"; @@ -165,17 +165,16 @@ impl TableT for Poseidon16Precompile { num_cols_total_poseidon_16() } - #[allow(clippy::vec_init_then_push)] // https://github.com/leanEthereum/leanMultisig/issues/198 fn bus(&self) -> Bus { - let mut data = Vec::with_capacity(4); - data.push(BusData::Column(POSEIDON_16_COL_PRECOMPILE_DATA)); - data.push(BusData::Column(POSEIDON_16_COL_INDEX_INPUT_LEFT)); - data.push(BusData::Column(POSEIDON_16_COL_INDEX_INPUT_RIGHT)); - data.push(BusData::Column(POSEIDON_16_COL_INDEX_INPUT_RES)); Bus { direction: BusDirection::Pull, - selector: POSEIDON_16_COL_FLAG, - data, + multiplicity: POSEIDON_16_COL_MULTIPLICITY, + discriminator: BusData::Column(POSEIDON_16_COL_DISCRIMINATOR), + data: vec![ + BusData::Column(POSEIDON_16_COL_INDEX_INPUT_LEFT), + BusData::Column(POSEIDON_16_COL_INDEX_INPUT_RIGHT), + BusData::Column(POSEIDON_16_COL_INDEX_INPUT_RES), + ], } } @@ -187,7 +186,7 @@ impl TableT for Poseidon16Precompile { let perm: &mut Poseidon1Cols16<&mut F> = unsafe { &mut *(ptrs.as_ptr() as *mut Poseidon1Cols16<&mut F>) }; perm.inputs.iter_mut().for_each(|x| **x = F::ZERO); - *perm.flag_active = F::ZERO; + *perm.multiplicity = F::ZERO; *perm.index_b = F::from_usize(zero_vec_ptr); *perm.index_res = F::from_usize(null_hash_ptr); *perm.flag_half_output = F::ZERO; @@ -198,7 +197,7 @@ impl TableT for Poseidon16Precompile { *perm.flag_permute = F::ZERO; perm.outputs_right.iter_mut().for_each(|x| **x = F::ZERO); row[POSEIDON_16_COL_INDEX_INPUT_LEFT] = F::from_usize(zero_vec_ptr); - row[POSEIDON_16_COL_PRECOMPILE_DATA] = F::from_usize(POSEIDON_PRECOMPILE_DATA); + row[POSEIDON_16_COL_DISCRIMINATOR] = F::from_usize(POSEIDON_DISCRIMINATOR_BASE); generate_trace_rows_for_perm(perm); row @@ -264,7 +263,7 @@ impl TableT for Poseidon16Precompile { let hardcoded_offset_left_val = hardcoded_offset_left.unwrap_or(0); - trace.columns[POSEIDON_16_COL_FLAG].push(F::ONE); + trace.columns[POSEIDON_16_COL_MULTIPLICITY].push(F::ONE); trace.columns[POSEIDON_16_COL_INDEX_INPUT_RIGHT].push(arg_b); trace.columns[POSEIDON_16_COL_INDEX_INPUT_RES].push(index_res_a); trace.columns[POSEIDON_16_COL_FLAG_HALF_OUTPUT].push(F::from_bool(half_output)); @@ -278,12 +277,12 @@ impl TableT for Poseidon16Precompile { } // Non-committed columns trace.columns[POSEIDON_16_COL_INDEX_INPUT_LEFT].push(arg_a); - let precompile_data = POSEIDON_PRECOMPILE_DATA + let discriminator = POSEIDON_DISCRIMINATOR_BASE + POSEIDON_PERMUTE_SHIFT * (permute as usize) + POSEIDON_HALF_OUTPUT_SHIFT * (half_output as usize) + POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT * (flag_hardcoded as usize) + POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT * hardcoded_offset_left_val; - trace.columns[POSEIDON_16_COL_PRECOMPILE_DATA].push(F::from_usize(precompile_data)); + trace.columns[POSEIDON_16_COL_DISCRIMINATOR].push(F::from_usize(discriminator)); // the rest of the trace is filled at the end of the execution (to get parallelism + SIMD) @@ -323,7 +322,7 @@ impl Air for Poseidon16Precompile { unsafe { std::ptr::read(&shorts[0]) } }; - let precompile_data_reconstructed = AB::IF::ONE + let discriminator_reconstructed = AB::IF::from_usize(POSEIDON_DISCRIMINATOR_BASE) + cols.flag_half_output * AB::F::from_usize(POSEIDON_HALF_OUTPUT_SHIFT) + cols.flag_hardcoded_left * AB::F::from_usize(POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) + cols.flag_hardcoded_left @@ -336,19 +335,21 @@ impl Air for Poseidon16Precompile { let index_a = cols.effective_index_left_second - one_minus_flag_hardcoded_left * AB::F::from_usize(HALF_DIGEST_LEN); - // Bus data: [precompile_data, a, b, res] + // Bus: discriminator = discriminator, data = [a, b, res] if BUS { builder.assert_zero_ef(eval_virtual_bus_column::( extra_data, - cols.flag_active, - &[precompile_data_reconstructed, index_a, cols.index_b, cols.index_res], + cols.multiplicity, + discriminator_reconstructed, + &[index_a, cols.index_b, cols.index_res], )); } else { - builder.declare_values(std::slice::from_ref(&cols.flag_active)); - builder.declare_values(&[precompile_data_reconstructed, index_a, cols.index_b, cols.index_res]); + builder.declare_values(std::slice::from_ref(&cols.multiplicity)); + builder.declare_values(std::slice::from_ref(&discriminator_reconstructed)); + builder.declare_values(&[index_a, cols.index_b, cols.index_res]); } - builder.assert_bool(cols.flag_active); + builder.assert_bool(cols.multiplicity); builder.assert_bool(cols.flag_half_output); builder.assert_bool(cols.flag_hardcoded_left); builder.assert_bool(cols.flag_permute); @@ -364,7 +365,7 @@ impl Air for Poseidon16Precompile { #[repr(C)] #[derive(Debug)] pub(super) struct Poseidon1Cols16 { - pub flag_active: T, // 0 = padding, 1 = active + pub multiplicity: T, // 0 = padding, 1 = active pub index_b: T, pub index_res: T, pub flag_half_output: T, @@ -453,7 +454,7 @@ pub const fn num_cols_poseidon_16() -> usize { } pub const fn num_cols_total_poseidon_16() -> usize { - // +2 for non-committed columns: POSEIDON_16_COL_INDEX_INPUT_LEFT, POSEIDON_16_COL_PRECOMPILE_DATA + // +2 for non-committed columns: POSEIDON_16_COL_INDEX_INPUT_LEFT, POSEIDON_16_COL_DISCRIMINATOR num_cols_poseidon_16() + 2 } diff --git a/crates/lean_vm/src/tables/table_enum.rs b/crates/lean_vm/src/tables/table_enum.rs index 6f521d793..e0c90f76a 100644 --- a/crates/lean_vm/src/tables/table_enum.rs +++ b/crates/lean_vm/src/tables/table_enum.rs @@ -5,7 +5,7 @@ use crate::*; pub const N_TABLES: usize = 3; pub const ALL_TABLES: [Table; N_TABLES] = [Table::execution(), Table::extension_op(), Table::poseidon16()]; -pub const MAX_PRECOMPILE_BUS_WIDTH: usize = 4; +pub const MAX_PRECOMPILE_BUS_WIDTH: usize = 3; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(usize)] @@ -106,7 +106,7 @@ impl Air for Table { } } -pub fn max_bus_width_including_domainsep() -> usize { +pub fn max_bus_width_including_bytecode() -> usize { 1 + MAX_PRECOMPILE_BUS_WIDTH.max(N_INSTRUCTION_COLUMNS) // "+1" for domain separation in logup between memory / bytecode / precompiles interactions } diff --git a/crates/lean_vm/src/tables/table_trait.rs b/crates/lean_vm/src/tables/table_trait.rs index 5623e5d6d..9905d482b 100644 --- a/crates/lean_vm/src/tables/table_trait.rs +++ b/crates/lean_vm/src/tables/table_trait.rs @@ -42,7 +42,8 @@ pub enum BusData { #[derive(Debug)] pub struct Bus { pub direction: BusDirection, - pub selector: ColIndex, + pub multiplicity: ColIndex, + pub discriminator: BusData, pub data: Vec, } diff --git a/crates/lean_vm/src/tables/utils.rs b/crates/lean_vm/src/tables/utils.rs index 6257ba669..e203917be 100644 --- a/crates/lean_vm/src/tables/utils.rs +++ b/crates/lean_vm/src/tables/utils.rs @@ -1,10 +1,11 @@ use backend::*; -use crate::{ExtraDataForBuses, LOGUP_PRECOMPILE_DOMAINSEP}; +use crate::ExtraDataForBuses; pub(crate) fn eval_virtual_bus_column>>( extra_data: &ExtraDataForBuses, - flag: AB::IF, + multiplicity: AB::IF, + discriminator: AB::IF, data: &[AB::IF], ) -> AB::EF { let (logup_alphas_eq_poly, bus_beta) = extra_data.transmute_bus_data::(); @@ -15,7 +16,7 @@ pub(crate) fn eval_virtual_bus_column> .zip(data) .map(|(c, d)| *c * *d) .sum::() - + *logup_alphas_eq_poly.last().unwrap() * AB::F::from_usize(LOGUP_PRECOMPILE_DOMAINSEP)) + + *logup_alphas_eq_poly.last().unwrap() * discriminator) * *bus_beta - + flag + + multiplicity } diff --git a/crates/rec_aggregation/src/compilation.rs b/crates/rec_aggregation/src/compilation.rs index 5f4c93ca2..2bc735f9b 100644 --- a/crates/rec_aggregation/src/compilation.rs +++ b/crates/rec_aggregation/src/compilation.rs @@ -245,19 +245,15 @@ fn build_replacements(log_inner_bytecode: usize, bytecode_zero_eval: F) -> BTree ); replacements.insert( "MAX_BUS_WIDTH_PLACEHOLDER".to_string(), - max_bus_width_including_domainsep().to_string(), + max_bus_width_including_bytecode().to_string(), ); replacements.insert( - "LOGUP_MEMORY_DOMAINSEP_PLACEHOLDER".to_string(), - LOGUP_MEMORY_DOMAINSEP.to_string(), + "LOGUP_MEMORY_DISCRIMINATOR_PLACEHOLDER".to_string(), + LOGUP_MEMORY_DISCRIMINATOR.to_string(), ); replacements.insert( - "LOGUP_PRECOMPILE_DOMAINSEP_PLACEHOLDER".to_string(), - LOGUP_PRECOMPILE_DOMAINSEP.to_string(), - ); - replacements.insert( - "LOGUP_BYTECODE_DOMAINSEP_PLACEHOLDER".to_string(), - LOGUP_BYTECODE_DOMAINSEP.to_string(), + "LOGUP_BYTECODE_DISCRIMINATOR_PLACEHOLDER".to_string(), + LOGUP_BYTECODE_DISCRIMINATOR.to_string(), ); replacements.insert( "LOG_GUEST_BYTECODE_LEN_PLACEHOLDER".to_string(), @@ -457,7 +453,8 @@ fn air_eval_in_zk_dsl(table: T) -> String where T::ExtraData: Default, { - let (constraints, bus_flag, bus_data) = get_symbolic_constraints_and_bus_data_values::(&table); + let (constraints, bus_multiplicity, bus_discriminator, bus_data) = + get_symbolic_constraints_and_bus_data_values::(&table); let mut ctx = AirCodegenCtx::new(); let mut res = format!( @@ -474,24 +471,26 @@ where } // first: bus data - let flag = eval_air_constraint(bus_flag, None, &mut ctx, &mut res); + let multiplicity = eval_air_constraint(bus_multiplicity, None, &mut ctx, &mut res); res += &format!("\n buff = Array(DIM * {})", bus_data.len()); for (i, data) in bus_data.iter().enumerate() { let data_str = eval_air_constraint(*data, None, &mut ctx, &mut res); res += &format!("\n copy_5({}, buff + DIM * {})", data_str, i); } - // dot product: bus_res = sum(buff[i] * logup_alphas_eq_poly[i]) for i in 0..bus_data.len() + let discriminator_str = eval_air_constraint(bus_discriminator, None, &mut ctx, &mut res); + // bus_res = sum(buff[i] * logup_alphas_eq_poly[i]) + disc * logup_alphas_eq_poly.last() res += "\n bus_res_init = Array(DIM)"; res += &format!( "\n dot_product_ee(buff, logup_alphas_eq_poly, bus_res_init, {})", bus_data.len() ); res += &format!( - "\n bus_res: Mut = add_extension_ret(mul_base_extension_ret(LOGUP_PRECOMPILE_DOMAINSEP, logup_alphas_eq_poly + {} * DIM), bus_res_init)", - max_bus_width_including_domainsep().next_power_of_two() - 1 + "\n bus_res: Mut = add_extension_ret(mul_extension_ret({}, logup_alphas_eq_poly + {} * DIM), bus_res_init)", + discriminator_str, + max_bus_width_including_bytecode().next_power_of_two() - 1 ); res += "\n bus_res = mul_extension_ret(bus_res, bus_beta)"; - res += &format!("\n sum: Mut = add_extension_ret(bus_res, {})", flag); + res += &format!("\n sum: Mut = add_extension_ret(bus_res, {})", multiplicity); // Batch constraint weighting: single dot_product_ee(alpha_powers, constraints_buf, result, n_constraints) res += "\n weighted_constraints = Array(DIM)"; diff --git a/crates/rec_aggregation/zkdsl_implem/recursion.py b/crates/rec_aggregation/zkdsl_implem/recursion.py index f11b6716d..8f5b303b5 100644 --- a/crates/rec_aggregation/zkdsl_implem/recursion.py +++ b/crates/rec_aggregation/zkdsl_implem/recursion.py @@ -14,9 +14,8 @@ MAX_BUS_WIDTH = MAX_BUS_WIDTH_PLACEHOLDER MAX_NUM_AIR_CONSTRAINTS = MAX_NUM_AIR_CONSTRAINTS_PLACEHOLDER -LOGUP_MEMORY_DOMAINSEP = LOGUP_MEMORY_DOMAINSEP_PLACEHOLDER -LOGUP_PRECOMPILE_DOMAINSEP = LOGUP_PRECOMPILE_DOMAINSEP_PLACEHOLDER -LOGUP_BYTECODE_DOMAINSEP = LOGUP_BYTECODE_DOMAINSEP_PLACEHOLDER +LOGUP_MEMORY_DISCRIMINATOR = LOGUP_MEMORY_DISCRIMINATOR_PLACEHOLDER +LOGUP_BYTECODE_DISCRIMINATOR = LOGUP_BYTECODE_DISCRIMINATOR_PLACEHOLDER EXECUTION_TABLE_INDEX = EXECUTION_TABLE_INDEX_PLACEHOLDER LOOKUPS_INDEXES = LOOKUPS_INDEXES_PLACEHOLDER # [[_; ?]; N_TABLES] @@ -115,7 +114,7 @@ def recursion(inner_public_memory, bytecode_hash_domsep): retrieved_numerators_value: Mut = opposite_extension_ret(mul_extension_ret(memory_and_acc_prefix, value_acc)) value_index = mle_of_01234567_etc(point_gkr + (n_vars_logup_gkr - log_memory) * DIM, log_memory) - fingerprint_memory = fingerprint_2(LOGUP_MEMORY_DOMAINSEP, value_memory, value_index, logup_alphas_eq_poly) + fingerprint_memory = fingerprint_2(LOGUP_MEMORY_DISCRIMINATOR, value_memory, value_index, logup_alphas_eq_poly) retrieved_denominators_value: Mut = mul_extension_ret(memory_and_acc_prefix, sub_extension_ret(logup_c, fingerprint_memory)) offset: Mut = two_exp(log_memory) @@ -159,7 +158,7 @@ def recursion(inner_public_memory, bytecode_hash_domsep): bytecode_value_corrected, add_extension_ret( mul_extension_ret(bytecode_index_value, logup_alphas_eq_poly + N_INSTRUCTION_COLUMNS * DIM), - mul_base_extension_ret(LOGUP_BYTECODE_DOMAINSEP, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM), + mul_base_extension_ret(LOGUP_BYTECODE_DISCRIMINATOR, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM), ), ), ), @@ -356,7 +355,7 @@ def continue_recursion_ordered( pref = multilinear_location_prefix(offset / n_rows, n_vars_logup_gkr - log_n_rows, point_gkr) # TODO there is some duplication here retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, pref) fingerp = fingerprint_2( - LOGUP_MEMORY_DOMAINSEP, + LOGUP_MEMORY_DISCRIMINATOR, value_eval, add_base_extension_ret(i, index_eval), logup_alphas_eq_poly, @@ -705,7 +704,7 @@ def fingerprint_bytecode(instr_evals, eval_on_pc, logup_alphas_eq_poly): res = add_extension_ret(res, mul_extension_ret(eval_on_pc, logup_alphas_eq_poly + N_INSTRUCTION_COLUMNS * DIM)) res = add_extension_ret( res, - mul_base_extension_ret(LOGUP_BYTECODE_DOMAINSEP, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM), + mul_base_extension_ret(LOGUP_BYTECODE_DISCRIMINATOR, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM), ) return res diff --git a/crates/sub_protocols/src/logup.rs b/crates/sub_protocols/src/logup.rs index 32b988799..3817a9cb8 100644 --- a/crates/sub_protocols/src/logup.rs +++ b/crates/sub_protocols/src/logup.rs @@ -54,9 +54,9 @@ pub fn prove_generic_logup( let c_packed = EFPacking::::from(c); let alphas_packed: Vec> = alphas_eq_poly.iter().map(|a| EFPacking::::from(*a)).collect(); let alpha_last = *alphas_eq_poly.last().unwrap(); - let memory_contrib = EFPacking::::from(alpha_last * F::from_usize(LOGUP_MEMORY_DOMAINSEP)); - let bytecode_contrib = EFPacking::::from(alpha_last * F::from_usize(LOGUP_BYTECODE_DOMAINSEP)); - let precompile_contrib = EFPacking::::from(alpha_last * F::from_usize(LOGUP_PRECOMPILE_DOMAINSEP)); + let memory_contrib = EFPacking::::from(alpha_last * F::from_usize(LOGUP_MEMORY_DISCRIMINATOR)); + let bytecode_contrib = EFPacking::::from(alpha_last * F::from_usize(LOGUP_BYTECODE_DISCRIMINATOR)); + let alpha_last_packed = EFPacking::::from(alpha_last); let min_section_log = log_bytecode.min(tables_log_heights_sorted.last().unwrap().1); if min_section_log < ENDIANNESS_PIVOT_GKR { @@ -152,9 +152,9 @@ pub fn prove_generic_logup( // I] Bus let bus = table.bus(); - let selector = &trace.columns[bus.selector]; + let multiplicity = &trace.columns[bus.multiplicity]; let pull = matches!(bus.direction, BusDirection::Pull); - fill_num_from(&mut numerators[offset..][..1 << log_n_rows], selector, pull); + fill_num_from(&mut numerators[offset..][..1 << log_n_rows], multiplicity, pull); let bus_data_entries = &bus.data; fill_denoms(&mut denominators[offset / width..][..(1 << log_n_rows) / width], |p| { let mut bus_data = [PFPacking::::ZERO; MAX_PRECOMPILE_BUS_WIDTH]; @@ -164,8 +164,16 @@ pub fn prove_generic_logup( BusData::Constant(val) => PFPacking::::from(F::from_usize(*val)), }; } + let discriminator = match bus.discriminator { + BusData::Column(col) => PFPacking::::from_fn(|w| trace.columns[col][src_idx(p, w)]), + BusData::Constant(val) => PFPacking::::from(F::from_usize(val)), + }; c_packed - + finger_print_packed::(precompile_contrib, &bus_data[..bus_data_entries.len()], &alphas_packed) + + finger_print_packed::( + alpha_last_packed * discriminator, + &bus_data[..bus_data_entries.len()], + &alphas_packed, + ) }); offset += 1 << log_n_rows; @@ -268,8 +276,9 @@ pub fn prove_generic_logup( } let bus = table.bus(); - let eval_on_selector = trace.columns[bus.selector].evaluate(&inner_point) * bus.direction.to_field_flag(); - prover_state.add_extension_scalar(eval_on_selector); + let eval_on_multiplicity = + trace.columns[bus.multiplicity].evaluate(&inner_point) * bus.direction.to_field_flag(); + prover_state.add_extension_scalar(eval_on_multiplicity); let bus_data_evals: Vec = bus .data @@ -279,14 +288,14 @@ pub fn prove_generic_logup( BusData::Constant(val) => EF::from(F::from_usize(*val)), }) .collect(); - let eval_on_data = c + finger_print::( - F::from_usize(LOGUP_PRECOMPILE_DOMAINSEP), - &bus_data_evals, - alphas_eq_poly, - ); + let bus_discriminator_eval: EF = match bus.discriminator { + BusData::Column(col) => trace.columns[col].evaluate(&inner_point), + BusData::Constant(val) => EF::from(F::from_usize(val)), + }; + let eval_on_data = c + finger_print(bus_discriminator_eval, &bus_data_evals, alphas_eq_poly); prover_state.add_extension_scalar(eval_on_data); - bus_numerators_values.insert(*table, eval_on_selector); + bus_numerators_values.insert(*table, eval_on_multiplicity); bus_denominators_values.insert(*table, eval_on_data); // II] Lookup into memory @@ -365,7 +374,7 @@ pub fn verify_generic_logup( let value_index = mle_of_01234567_etc(&memory_and_acc_point); retrieved_denominators_value += pref * (c - finger_print( - F::from_usize(LOGUP_MEMORY_DOMAINSEP), + F::from_usize(LOGUP_MEMORY_DISCRIMINATOR), &[value_memory, value_index], alphas_eq_poly, )); @@ -394,7 +403,7 @@ pub fn verify_generic_logup( retrieved_denominators_value += pref * (c - (bytecode_value_corrected + bytecode_index_value * alphas_eq_poly[N_INSTRUCTION_COLUMNS] - + *alphas_eq_poly.last().unwrap() * F::from_usize(LOGUP_BYTECODE_DOMAINSEP))); + + *alphas_eq_poly.last().unwrap() * F::from_usize(LOGUP_BYTECODE_DISCRIMINATOR))); // Padding for bytecode retrieved_denominators_value += pref_padded * mle_of_zeros_then_ones(1 << log_bytecode, from_end(&point_gkr, log_bytecode_padded)); @@ -421,7 +430,7 @@ pub fn verify_generic_logup( retrieved_numerators_value += pref; // numerator is 1 retrieved_denominators_value += pref * (c - finger_print( - F::from_usize(LOGUP_BYTECODE_DOMAINSEP), + F::from_usize(LOGUP_BYTECODE_DISCRIMINATOR), &[instr_evals, vec![eval_on_pc]].concat(), alphas_eq_poly, )); @@ -430,14 +439,14 @@ pub fn verify_generic_logup( } // I] Bus (data flow between tables) - let eval_on_selector = verifier_state.next_extension_scalar()?; + let eval_on_multiplicity = verifier_state.next_extension_scalar()?; let pref = pref_at(offset, log_n_rows); - retrieved_numerators_value += pref * eval_on_selector; + retrieved_numerators_value += pref * eval_on_multiplicity; let eval_on_data = verifier_state.next_extension_scalar()?; retrieved_denominators_value += pref * eval_on_data; - bus_numerators_values.insert(table, eval_on_selector); + bus_numerators_values.insert(table, eval_on_multiplicity); bus_denominators_values.insert(table, eval_on_data); offset += 1 << log_n_rows; @@ -457,7 +466,7 @@ pub fn verify_generic_logup( retrieved_numerators_value += pref; // numerator is 1 retrieved_denominators_value += pref * (c - finger_print( - F::from_usize(LOGUP_MEMORY_DOMAINSEP), + F::from_usize(LOGUP_MEMORY_DISCRIMINATOR), &[value_eval, index_eval + F::from_usize(i)], alphas_eq_poly, )); diff --git a/crates/sub_protocols/tests/prove_poseidon_16.rs b/crates/sub_protocols/tests/prove_poseidon_16.rs index 941993662..e9ce9a81c 100644 --- a/crates/sub_protocols/tests/prove_poseidon_16.rs +++ b/crates/sub_protocols/tests/prove_poseidon_16.rs @@ -3,7 +3,7 @@ use std::time::Instant; use backend::*; use lean_vm::{ EF, ExtraDataForBuses, F, POSEIDON_16_COL_EFFECTIVE_INDEX_LEFT_FIRST, POSEIDON_16_COL_EFFECTIVE_INDEX_LEFT_SECOND, - POSEIDON_16_COL_FLAG, POSEIDON_16_COL_INPUT_START, Poseidon16Precompile, fill_trace_poseidon_16, + POSEIDON_16_COL_INPUT_START, POSEIDON_16_COL_MULTIPLICITY, Poseidon16Precompile, fill_trace_poseidon_16, num_cols_poseidon_16, }; use rand::{RngExt, SeedableRng, rngs::StdRng}; @@ -31,7 +31,7 @@ fn prove_air_poseidon_16(log_n_rows: usize) { for t in trace.iter_mut().skip(POSEIDON_16_COL_INPUT_START).take(WIDTH) { *t = (0..n_rows).map(|_| rng.random()).collect(); } - trace[POSEIDON_16_COL_FLAG] = vec![F::ONE; n_rows]; + trace[POSEIDON_16_COL_MULTIPLICITY] = vec![F::ONE; n_rows]; trace[POSEIDON_16_COL_EFFECTIVE_INDEX_LEFT_FIRST] = vec![F::ZERO; n_rows]; trace[POSEIDON_16_COL_EFFECTIVE_INDEX_LEFT_SECOND] = vec![F::from_usize(HALF_DIGEST_LEN); n_rows]; fill_trace_poseidon_16(&mut trace); diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 593e76800..18667903f 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -73,23 +73,19 @@ pub fn mle_of_01234567_etc(point: &[F]) -> F { } } -pub fn finger_print>, EF: ExtensionField + ExtensionField>( - table: F, - data: &[IF], - alphas_eq_poly: &[EF], -) -> EF { +pub fn finger_print>(discriminator: DS, data: &[EF], alphas_eq_poly: &[EF]) -> EF { assert!(alphas_eq_poly.len() > data.len()); dot_product::(alphas_eq_poly.iter().copied(), data.iter().copied()) - + *alphas_eq_poly.last().unwrap() * table + + *alphas_eq_poly.last().unwrap() * discriminator } #[inline(always)] pub fn finger_print_packed>>( - table_contrib: EFPacking, + discriminator_contrib: EFPacking, data: &[PFPacking], alphas_packed: &[EFPacking], ) -> EFPacking { - let mut result = table_contrib; + let mut result = discriminator_contrib; for (alpha, d) in alphas_packed.iter().zip(data) { result += *alpha * *d; } From b01b1997aec3129ac70ac54617ba06edc1228383 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 19 May 2026 17:16:47 +0100 Subject: [PATCH 16/69] add a test to validate logup soundness bits >= SECURITY_LEVEL --- Cargo.lock | 1 + crates/backend/fiat-shamir/src/errors.rs | 11 ++++++++++ crates/lean_compiler/src/c_compile_final.rs | 4 ++++ crates/lean_prover/src/verify_execution.rs | 6 ++++++ crates/lean_vm/Cargo.toml | 2 +- crates/lean_vm/src/core/constants.rs | 1 + crates/sub_protocols/Cargo.toml | 2 +- crates/sub_protocols/src/lib.rs | 1 + crates/sub_protocols/src/logup.rs | 17 ++++++++++----- crates/sub_protocols/tests/soundness_logup.rs | 21 +++++++++++++++++++ 10 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 crates/sub_protocols/tests/soundness_logup.rs diff --git a/Cargo.lock b/Cargo.lock index d938586b8..51d9fdfb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1094,6 +1094,7 @@ name = "sub_protocols" version = "0.1.0" dependencies = [ "backend", + "lean_prover", "lean_vm", "rand", "tracing", diff --git a/crates/backend/fiat-shamir/src/errors.rs b/crates/backend/fiat-shamir/src/errors.rs index c720e6058..d2515b5f1 100644 --- a/crates/backend/fiat-shamir/src/errors.rs +++ b/crates/backend/fiat-shamir/src/errors.rs @@ -29,6 +29,10 @@ pub enum ProofError { InvalidPadding, InvalidRate, TooBigTable(TooBigTableError), + TooBigBytecode { + current_log_size: usize, + max_log_size: usize, + }, } impl From for ProofError { @@ -71,6 +75,13 @@ impl Display for ProofError { "LeanVM supports rate 1/2, 1/4, 1/8 and 1/16 (log_inv_rate in {{1, 2, 3, 4}})" ), Self::TooBigTable(e) => write!(f, "{}", e), + Self::TooBigBytecode { + current_log_size, + max_log_size, + } => write!( + f, + "Bytecode too big: current=2^{current_log_size}, max=2^{max_log_size}" + ), } } } diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index 5ff50f14b..42abf465d 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -167,6 +167,10 @@ pub fn compile_to_low_level_bytecode( .collect(); assert!(hints.is_empty()); + if log2_ceil_usize(code.len()) > MAX_BYTECODE_LOG_SIZE { + return Err("Bytecode too large".to_string()); + } + Ok(Bytecode { code, instructions_multilinear, diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index b563fc2f1..8781c6e7f 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -16,6 +16,12 @@ pub fn verify_execution( public_input: &[F], proof: Proof, ) -> Result<(ProofVerificationDetails, RawProof), ProofError> { + if bytecode.log_size() > MAX_BYTECODE_LOG_SIZE { + return Err(ProofError::TooBigBytecode { + current_log_size: bytecode.log_size(), + max_log_size: MAX_BYTECODE_LOG_SIZE, + }); + } let mut verifier_state = VerifierState::::new(proof, get_poseidon16().clone())?; verifier_state.observe_scalars(public_input); verifier_state.observe_scalars(&poseidon16_compress_pair(&bytecode.hash, &SNARK_DOMAIN_SEP)); diff --git a/crates/lean_vm/Cargo.toml b/crates/lean_vm/Cargo.toml index 32e50f199..2b5d1832e 100644 --- a/crates/lean_vm/Cargo.toml +++ b/crates/lean_vm/Cargo.toml @@ -14,4 +14,4 @@ xmss.workspace = true rand.workspace = true tracing.workspace = true backend.workspace = true -itertools.workspace = true +itertools.workspace = true \ No newline at end of file diff --git a/crates/lean_vm/src/core/constants.rs b/crates/lean_vm/src/core/constants.rs index 6873255d4..8f8477635 100644 --- a/crates/lean_vm/src/core/constants.rs +++ b/crates/lean_vm/src/core/constants.rs @@ -17,6 +17,7 @@ pub const MIN_LOG_MEMORY_SIZE: usize = 16; pub const MAX_LOG_MEMORY_SIZE: usize = 26; pub const MIN_BYTECODE_LOG_SIZE: usize = 8; +pub const MAX_BYTECODE_LOG_SIZE: usize = 22; /// Minimum and maximum number of rows per table (as powers of two), both inclusive pub const MIN_LOG_N_ROWS_PER_TABLE: usize = 8; // Zero padding will be added to each at least, if this minimum is not reached, (ensuring AIR / GKR work fine, with SIMD, without too much edge cases). Long term, we should find a more elegant solution. diff --git a/crates/sub_protocols/Cargo.toml b/crates/sub_protocols/Cargo.toml index 491ef4ce3..38c8663a6 100644 --- a/crates/sub_protocols/Cargo.toml +++ b/crates/sub_protocols/Cargo.toml @@ -14,6 +14,6 @@ lean_vm.workspace = true backend.workspace = true [dev-dependencies] - rand.workspace = true +lean_prover.workspace = true diff --git a/crates/sub_protocols/src/lib.rs b/crates/sub_protocols/src/lib.rs index 4921c7734..356bbba8d 100644 --- a/crates/sub_protocols/src/lib.rs +++ b/crates/sub_protocols/src/lib.rs @@ -1,4 +1,5 @@ mod air_sumcheck; + pub use air_sumcheck::*; mod logup; diff --git a/crates/sub_protocols/src/logup.rs b/crates/sub_protocols/src/logup.rs index 3817a9cb8..bdfa62a3e 100644 --- a/crates/sub_protocols/src/logup.rs +++ b/crates/sub_protocols/src/logup.rs @@ -343,11 +343,7 @@ pub fn verify_generic_logup( ) -> ProofResult { let tables_heights_sorted = sort_tables_by_height(table_log_n_rows); let log_bytecode = log2_strict_usize(bytecode_multilinear.len() / N_INSTRUCTION_COLUMNS.next_power_of_two()); - let total_gkr_n_vars = log2_ceil_usize(compute_total_active_len( - log_memory, - log_bytecode, - &tables_heights_sorted, - )); + let total_gkr_n_vars = compute_total_logup_log_size(log_memory, log_bytecode, &tables_heights_sorted); let (sum, point_gkr, numerators_value, denominators_value) = verify_gkr_quotient(verifier_state, total_gkr_n_vars)?; @@ -506,6 +502,17 @@ fn offset_for_table(table: &Table, log_n_rows: usize) -> usize { num_cols << log_n_rows } +pub fn compute_total_logup_log_size( + log_memory: usize, + log_bytecode: usize, + tables_heights_sorted: &[(Table, VarCount)], +) -> VarCount { + log2_ceil_usize(compute_total_active_len( + log_memory, + log_bytecode, + tables_heights_sorted, + )) +} fn compute_total_active_len( log_memory: usize, log_bytecode: usize, diff --git a/crates/sub_protocols/tests/soundness_logup.rs b/crates/sub_protocols/tests/soundness_logup.rs new file mode 100644 index 000000000..885a5f20d --- /dev/null +++ b/crates/sub_protocols/tests/soundness_logup.rs @@ -0,0 +1,21 @@ +use backend::{Field, log2_ceil_usize}; +use lean_prover::SECURITY_BITS; +use lean_vm::{ + EF, MAX_BYTECODE_LOG_SIZE, MAX_LOG_MEMORY_SIZE, MAX_LOG_N_ROWS_PER_TABLE, max_bus_width_including_bytecode, + sort_tables_by_height, +}; +use std::collections::BTreeMap; +use sub_protocols::compute_total_logup_log_size; + +#[test] +fn ensure_logup_soundness_is_suffisant() { + let max_logup_n_vars = compute_total_logup_log_size( + MAX_LOG_MEMORY_SIZE, + MAX_BYTECODE_LOG_SIZE, + &sort_tables_by_height(&BTreeMap::from(MAX_LOG_N_ROWS_PER_TABLE)), + ); + dbg!(max_logup_n_vars, EF::bits()); + // TODO explain formula + let logup_error_bits = max_logup_n_vars + log2_ceil_usize(log2_ceil_usize(max_bus_width_including_bytecode())); + assert!(SECURITY_BITS + logup_error_bits <= EF::bits()); +} From 6f6ca0a56a5f32d9e066b975a1da046dfb28396b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 19 May 2026 17:48:51 +0100 Subject: [PATCH 17/69] 1000 xmss --- crates/lean_prover/tests/dump_zkvm_vector.rs | 11 ++++++----- crates/lean_prover/verifier.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/lean_prover/tests/dump_zkvm_vector.rs b/crates/lean_prover/tests/dump_zkvm_vector.rs index aeb1fb17c..89c884660 100644 --- a/crates/lean_prover/tests/dump_zkvm_vector.rs +++ b/crates/lean_prover/tests/dump_zkvm_vector.rs @@ -1,5 +1,5 @@ -//! Single end-to-end test vector for the Python verifier: aggregate one XMSS -//! signature using rec-aggregation, then dump the resulting bytecode, public +//! Single end-to-end test vector for the Python verifier: aggregate 1000 XMSS +//! signatures using rec-aggregation, then dump the resulting bytecode, public //! input, table metadata, and proof. //! //! Run: @@ -125,12 +125,13 @@ fn dump_zkvm_vector() { init_aggregation_bytecode(); let bytecode = get_aggregation_bytecode(); - // Aggregate one raw XMSS signature into a TypeOneMultiSignature. + // Aggregate 1000 raw XMSS signatures into a TypeOneMultiSignature. + const N_SIGNATURES: usize = 1000; let sig = { - let (pk, xmss_sig) = get_benchmark_signatures()[0].clone(); + let raw_xmss = get_benchmark_signatures()[..N_SIGNATURES].to_vec(); aggregate_type_1( &[], - vec![(pk, xmss_sig)], + raw_xmss, message_for_benchmark(), BENCHMARK_SLOT, /* log_inv_rate = */ 1, diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index ce7b24663..ba43e1b81 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1,7 +1,7 @@ """Pure-Python verifier for leanVM execution proofs. -Single end-to-end test vector — a rec-aggregation proof over one XMSS -signature — is generated by the Rust side and stored under `target/`. +Single end-to-end test vector — a rec-aggregation proof over 1000 XMSS +signatures — is generated by the Rust side and stored under `target/`. Run this script to verify it. Setup (one-time): From 38c3d112c804e6982add32de6dded2d6814722ba Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 19 May 2026 17:59:25 +0100 Subject: [PATCH 18/69] wip --- crates/lean_prover/verifier.py | 228 ++++++++++++++------------------- 1 file changed, 97 insertions(+), 131 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index ba43e1b81..b19294200 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -60,17 +60,9 @@ def __init__(self, coeffs: Sequence[Fp]): assert len(coeffs) == 5 self.c = tuple(coeffs) - @staticmethod - def zero() -> "EF": - return EF([Fp(0)] * 5) - - @staticmethod - def one() -> "EF": - return EF([Fp(1)] + [Fp(0)] * 4) - @staticmethod def from_base(x: Fp) -> "EF": - return EF([x] + [Fp(0)] * 4) + return EF([x, Fp(0), Fp(0), Fp(0), Fp(0)]) def __add__(self, o): if isinstance(o, Fp): @@ -113,7 +105,7 @@ def __repr__(self): return f"EF({[int(x.value) for x in self.c]})" def inv(self) -> "EF": - result, base, n = EF.one(), self, P**5 - 2 + result, base, n = ONE, self, P**5 - 2 while n > 0: if n & 1: result = result * base @@ -122,6 +114,28 @@ def inv(self) -> "EF": return result +ZERO = EF([Fp(0)] * 5) +ONE = EF.from_base(Fp(1)) + + +def fb(v: int) -> EF: + """`EF` lift of an integer base-field element.""" + return EF.from_base(Fp(v)) + + +def ef_sum(terms) -> EF: + """Sum of an iterable of `EF` (empty -> `ZERO`).""" + return sum(terms, ZERO) + + +def ef_prod(factors) -> EF: + """Product of an iterable of `EF` (empty -> `ONE`).""" + acc = ONE + for f in factors: + acc = acc * f + return acc + + _POSEIDON16 = Poseidon1(PARAMS_16) @@ -299,31 +313,21 @@ def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: """`Π (1 − a_i − b_i + 2·a_i·b_i)`.""" assert len(a) == len(b) - one, acc = EF.one(), EF.one() - for x, y in zip(a, b): - xy = x * y - acc = acc * (one + xy + xy - x - y) - return acc + return ef_prod(ONE + x * y + x * y - x - y for x, y in zip(a, b)) def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: """Multilinear extension of `y = x + 1` (big-endian, mod `2^n`).""" assert len(x) == len(y) - one = EF.one() n = len(x) - eq_prefix = [one] + eq_prefix = [ONE] for i in range(n): - eq_prefix.append(eq_prefix[i] * (x[i] * y[i] + (one - x[i]) * (one - y[i]))) - low_suffix = [one] * (n + 1) + eq_prefix.append(eq_prefix[i] * (x[i] * y[i] + (ONE - x[i]) * (ONE - y[i]))) + low_suffix = [ONE] * (n + 1) for i in range(n - 1, -1, -1): - low_suffix[i] = low_suffix[i + 1] * x[i] * (one - y[i]) - s = EF.zero() - for i in range(n): - s = s + eq_prefix[i] * (one - x[i]) * y[i] * low_suffix[i + 1] - prod = one - for v in list(x) + list(y): - prod = prod * v - return s + prod + low_suffix[i] = low_suffix[i + 1] * x[i] * (ONE - y[i]) + s = ef_sum(eq_prefix[i] * (ONE - x[i]) * y[i] * low_suffix[i + 1] for i in range(n)) + return s + ef_prod([*x, *y]) def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: @@ -475,7 +479,7 @@ def oods_constraints(self) -> list[SparseStatement]: def _eval_univariate(coeffs: list[EF], x: EF) -> EF: - acc = EF.zero() + acc = ZERO for c in reversed(coeffs): acc = acc * x + c return acc @@ -488,9 +492,7 @@ def verify_sumcheck( point: list[EF] = [] for _ in range(n_vars): coeffs = state.next_extension_scalars_vec(degree + 1) - s = coeffs[0] - for c in coeffs: - s = s + c + s = coeffs[0] + ef_sum(coeffs) if s != target: raise ProofError("Sumcheck identity failed: h(0) + h(1) != target") state.check_pow_grinding(pow_bits) @@ -502,7 +504,7 @@ def verify_sumcheck( def combine_constraints(state: VerifierState, target: EF, constraints: list[SparseStatement]) -> tuple[EF, list[EF]]: gamma: EF = state.sample() - combo = [EF.one()] + combo = [ONE] for smt in constraints: for v in smt.values: target = target + combo[-1] * v[1] @@ -538,7 +540,7 @@ def pack_answers(leaf: list[Fp]) -> list[EF]: if not merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path): raise ProofError("Merkle verification failed") fold = eval_multilinear_evals(pack_answers(op.leaf_data), folding_randomness) - ef_pt = EF.from_base(Fp(pow(int(gen.value), idx, P))) + ef_pt = fb(pow(int(gen.value), idx, P)) constraints.append(SparseStatement.dense(expand_from_univariate(ef_pt, num_variables), fold)) return constraints @@ -554,7 +556,7 @@ def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> b def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatement]]], point: list[EF]) -> EF: - value = EF.zero() + value = ZERO pt = list(point) for round_idx, (randomness, smts) in enumerate(constraints): if round_idx > 0: @@ -565,10 +567,7 @@ def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatement common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly_outside(smt.point, inner_pt) sel_n = smt.selector_num_variables for v in smt.values: - lagrange = EF.one() - for j in range(sel_n): - bit = (v[0] >> (sel_n - 1 - j)) & 1 - lagrange = lagrange * (pt[j] if bit else (EF.one() - pt[j])) + lagrange = ef_prod(pt[j] if (v[0] >> (sel_n - 1 - j)) & 1 else ONE - pt[j] for j in range(sel_n)) value = value + lagrange * common * randomness[i] i += 1 assert i == len(randomness) @@ -587,7 +586,7 @@ def whir_verify( n_rounds, final_sumcheck_rounds = whir_n_rounds_and_final_sumcheck(cfg["num_variables"]) round_constraints: list[tuple[list[EF], list[SparseStatement]]] = [] round_folding: list[list[EF]] = [] - target = EF.zero() + target = ZERO def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None: nonlocal target @@ -707,11 +706,7 @@ def values_at(d: dict[int, EF], n_vars: int) -> list[tuple[int, EF]]: if name == "execution": # PC column: pin first row to starting_pc, last row to ending_pc. for idx, pc in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: - out.append( - SparseStatement.unique_value( - stacked_n_vars, offset + (col_pc << n_vars) + idx, EF.from_base(Fp(pc)) - ) - ) + out.append(SparseStatement.unique_value(stacked_n_vars, offset + (col_pc << n_vars) + idx, fb(pc))) for point, eq_values, next_values in committed_statements[name]: if next_values: @@ -732,9 +727,7 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] nums = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) dens = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) - quotient = EF.zero() - for n, d in zip(nums, dens): - quotient = quotient + n * d.inv() + quotient = ef_sum(n * d.inv() for n, d in zip(nums, dens)) point = state.sample_vec(N_VARS_TO_SEND_GKR_COEFFS) claim_num = eval_multilinear_evals(nums, point) @@ -749,7 +742,7 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] if sc_value != eq_poly_outside(point, sc_point) * (alpha * dl * dr + nl * dr + nr * dl): raise ProofError("GKR step: postponed value mismatch") beta = state.sample() - one_minus = EF.one() - beta + one_minus = ONE - beta claim_num = one_minus * nl + beta * nr claim_den = one_minus * dl + beta * dr point = sc_point + [beta] @@ -758,7 +751,7 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: - return [EF.one() if (value >> (bit_count - 1 - i)) & 1 else EF.zero() for i in range(bit_count)] + return [ONE if (value >> (bit_count - 1 - i)) & 1 else ZERO for i in range(bit_count)] def from_end(seq: Sequence, n: int) -> list: @@ -768,9 +761,9 @@ def from_end(seq: Sequence, n: int) -> list: def mle_of_01234567_etc(point: Sequence[EF]) -> EF: """MLE of `f(i) = i` (big-endian) at `point`.""" if not point: - return EF.zero() + return ZERO e = mle_of_01234567_etc(point[1:]) - return e + point[0] * EF.from_base(Fp(1 << (len(point) - 1))) + return e + point[0] * fb(1 << (len(point) - 1)) def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: @@ -778,21 +771,19 @@ def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: n_values = 1 << len(point) assert n_zeros <= n_values if n_zeros == 0: - return EF.one() + return ONE if n_zeros == n_values: - return EF.zero() + return ZERO half, tail = n_values >> 1, point[1:] if n_zeros < half: - return (EF.one() - point[0]) * mle_of_zeros_then_ones(n_zeros, tail) + point[0] + return (ONE - point[0]) * mle_of_zeros_then_ones(n_zeros, tail) + point[0] return point[0] * mle_of_zeros_then_ones(n_zeros - half, tail) def finger_print(discriminator: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: """`Σᵢ αᵢ · dataᵢ + α_last · discriminator`.""" assert len(alphas_eq_poly) > len(data) - acc = EF.zero() - for a, d in zip(alphas_eq_poly, data): - acc = acc + a * d + acc = ef_sum(a * d for a, d in zip(alphas_eq_poly, data)) return acc + alphas_eq_poly[-1] * EF.from_base(discriminator) @@ -803,9 +794,9 @@ def sort_tables_by_height(table_log_heights: dict[str, int]) -> list[tuple[str, def eval_eq(point: Sequence[EF]) -> list[EF]: """Length-`2^n` evaluation table of `eq(point, ·)`.""" - out = [EF.one()] + out = [ONE] for p in point: - out = [w for v in out for w in (v * (EF.one() - p), v * p)] + out = [w for v in out for w in (v * (ONE - p), v * p)] return out @@ -842,10 +833,10 @@ def verify_generic_logup( total_gkr_n_vars = log2_ceil_usize(total_active_len) quotient, point_gkr, claim_num, claim_den = verify_gkr_quotient(state, total_gkr_n_vars) - if quotient != EF.zero(): + if quotient != ZERO: raise ProofError("logup: GKR sum != 0") - num, den = EF.zero(), EF.zero() + num, den = ZERO, ZERO def pref_at(offset: int, log_height: int) -> EF: n_missing = total_gkr_n_vars - log_height @@ -869,9 +860,7 @@ def pref_at(offset: int, log_height: int) -> EF: pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() bytecode_value = eval_mle_base_at_ef(bytecode_multilinear, list(byte_pt) + list(from_end(alphas, log_instr))) - correction = EF.one() - for a in alphas[: len(alphas) - log_instr]: - correction = correction * (EF.one() - a) + correction = ef_prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( bytecode_value * correction + mle_of_01234567_etc(byte_pt) * alphas_eq_poly[n_instr_cols] @@ -924,7 +913,7 @@ def pref_at(offset: int, log_height: int) -> EF: pref = pref_at(offset, log_n_rows) fp = finger_print( disc_mem, - [value_eval, index_eval + EF.from_base(Fp(i))], + [value_eval, index_eval + fb(i)], alphas_eq_poly, ) num = num + pref @@ -960,7 +949,7 @@ def __init__(self, flat: Sequence[EF], shift: Sequence[EF], alpha_powers: Sequen list(shift), list(alpha_powers), ) - self.accumulator: EF = EF.zero() + self.accumulator: EF = ZERO self.i = 0 def assert_zero(self, x: EF) -> None: @@ -971,7 +960,7 @@ def assert_eq(self, x: EF, y: EF) -> None: self.assert_zero(x - y) def assert_bool(self, x: EF) -> None: - self.assert_zero(x * (EF.one() - x)) + self.assert_zero(x * (ONE - x)) def _eval_virtual_bus_column(extra_data: dict, multiplicity: EF, discriminator: EF, data: Sequence[EF]) -> EF: @@ -982,10 +971,7 @@ def _eval_virtual_bus_column(extra_data: dict, multiplicity: EF, discriminator: alphas: list[EF] = extra_data["logup_alphas_eq_poly"] bus_beta: EF = extra_data["bus_beta"] assert len(data) < len(alphas) - inner = EF.zero() - for a, d in zip(alphas, data): - inner = inner + a * d - inner = inner + alphas[-1] * discriminator + inner = ef_sum(a * d for a, d in zip(alphas, data)) + alphas[-1] * discriminator return inner * bus_beta + multiplicity @@ -1011,20 +997,19 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: flag_ab_fp, mul, jump, aux, discriminator) = folder.flat[:20] # fmt: on pc_shift, fp_shift = folder.shift[0], folder.shift[1] - one = EF.one() # nu_x = flag·operand + (1 − flag − flag_ab_fp)·value + flag_ab_fp·(fp + operand) - nfa = -(flag_a + flag_ab_fp - one) - nfb = -(flag_b + flag_ab_fp - one) - nfc = -(flag_c + flag_c_fp - one) + nfa = -(flag_a + flag_ab_fp - ONE) + nfb = -(flag_b + flag_ab_fp - ONE) + nfc = -(flag_c + flag_c_fp - ONE) nu_a = flag_a * operand_a + nfa * value_a + flag_ab_fp * (fp + operand_a) nu_b = flag_b * operand_b + nfb * value_b + flag_ab_fp * (fp + operand_b) nu_c = flag_c * operand_c + nfc * value_c + flag_c_fp * (fp + operand_c) # aux ∈ {0,1,2}: 0=nothing, 1=add, 2=deref. - add = aux * EF.from_base(Fp(2)) - aux * aux - deref = aux * (aux - one) * EF.from_base(_INV_TWO) - is_precompile = -(add + mul + deref + jump - one) + add = aux * fb(2) - aux * aux + deref = aux * (aux - ONE) * EF.from_base(_INV_TWO) + is_precompile = -(add + mul + deref + jump - ONE) az = folder.assert_zero az(_eval_virtual_bus_column(extra_data, is_precompile, discriminator, [nu_a, nu_b, nu_c])) @@ -1036,11 +1021,11 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: az(deref * (addr_b - (value_a + operand_b))) az(deref * (value_b - nu_c)) jc = jump * nu_a - az(jc * (nu_a - one)) + az(jc * (nu_a - ONE)) az(jc * (pc_shift - nu_b)) az(jc * (fp_shift - nu_c)) - not_jc = -(jc - one) - az(not_jc * (pc_shift - (pc + one))) + not_jc = -(jc - ONE) + az(not_jc * (pc_shift - (pc + ONE))) az(not_jc * (fp_shift - fp)) @@ -1065,10 +1050,7 @@ def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: ] def dot(row: list[EF]) -> EF: - acc = a[0] * row[0] - for i in range(1, 5): - acc = acc + a[i] * row[i] - return acc + return ef_sum(a[i] * row[i] for i in range(5)) return [dot(row) for row in rows] @@ -1083,15 +1065,13 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat s = folder.shift is_be_sh, start_sh, len_sh, flag_add_sh, flag_mul_sh, flag_poly_eq_sh, idx_a_sh, idx_b_sh = s[:8] comp_sh = s[8:13] - one, zero = EF.one(), EF.zero() - Fb = lambda v: EF.from_base(Fp(v)) aux = ( - is_be * Fb(_EXT_OP_FLAG_IS_BE) - + flag_add * Fb(_EXT_OP_FLAG_ADD) - + flag_mul * Fb(_EXT_OP_FLAG_MUL) - + flag_poly_eq * Fb(_EXT_OP_FLAG_POLY_EQ) - + len_col * Fb(_EXT_OP_LEN_MULTIPLIER) + is_be * fb(_EXT_OP_FLAG_IS_BE) + + flag_add * fb(_EXT_OP_FLAG_ADD) + + flag_mul * fb(_EXT_OP_FLAG_MUL) + + flag_poly_eq * fb(_EXT_OP_FLAG_POLY_EQ) + + len_col * fb(_EXT_OP_LEN_MULTIPLIER) ) folder.assert_zero( _eval_virtual_bus_column(extra_data, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res]) @@ -1100,7 +1080,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat for x in (is_be, start, flag_add, flag_mul, flag_poly_eq): folder.assert_bool(x) - is_ee, not_start_sh = -(is_be - one), -(start_sh - one) + is_ee, not_start_sh = -(is_be - ONE), -(start_sh - ONE) va_x = [va[0]] + [va[k] * is_ee for k in range(1, 5)] comp_tail = [comp_sh[k] * not_start_sh for k in range(5)] va_vb = _quintic_mul_ef(va_x, vb) @@ -1111,7 +1091,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat folder.assert_zero((comp[k] - (va_vb[k] + comp_tail[k])) * flag_mul) # poly_eq: comp ← (2·va·vb − va − vb + 1) · comp_sh_or_one. - poly_eq_val = [va_vb[k] + va_vb[k] - va_x[k] - vb[k] + (one if k == 0 else zero) for k in range(5)] + poly_eq_val = [va_vb[k] + va_vb[k] - va_x[k] - vb[k] + (ONE if k == 0 else ZERO) for k in range(5)] comp_sh_or_one = [comp_sh[0] * not_start_sh + start_sh] + [comp_sh[k] * not_start_sh for k in range(1, 5)] poly_eq_result = _quintic_mul_ef(poly_eq_val, comp_sh_or_one) for k in range(5): @@ -1120,7 +1100,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat folder.assert_zero((comp[k] - vres[k]) * start) for x, y in [ - (len_col, len_sh + one), + (len_col, len_sh + ONE), (is_be, is_be_sh), (flag_add, flag_add_sh), (flag_mul, flag_mul_sh), @@ -1128,9 +1108,9 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat ]: folder.assert_zero(not_start_sh * (x - y)) - folder.assert_zero(not_start_sh * (idx_a_sh - idx_a - (is_be + is_ee * Fb(5)))) - folder.assert_zero(not_start_sh * (idx_b_sh - idx_b - Fb(5))) - folder.assert_zero(start_sh * (len_col - one)) + folder.assert_zero(not_start_sh * (idx_a_sh - idx_a - (is_be + is_ee * fb(5)))) + folder.assert_zero(not_start_sh * (idx_b_sh - idx_b - fb(5))) + folder.assert_zero(start_sh * (len_col - ONE)) @functools.cache @@ -1166,21 +1146,13 @@ def _p1c() -> dict: def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: - out: list[EF] = [] - for row in mat: - acc = EF.zero() - for s, m in zip(state, row): - acc = acc + s * m - out.append(acc) - return out + return [ef_sum(s * m for s, m in zip(state, row)) for row in mat] def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: for rc in (rc1, rc2): - state = _matvec_kb( - _p1c()["mds_dense"], - [(s + EF.from_base(c)) * (s + EF.from_base(c)) * (s + EF.from_base(c)) for s, c in zip(state, rc)], - ) + sbox = [(t := s + c) * t * t for s, c in zip(state, rc)] + state = _matvec_kb(_p1c()["mds_dense"], sbox) return state @@ -1198,7 +1170,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) - folder.assert_eq(state[i], post) state[i] = post - state = [s + EF.from_base(c) for s, c in zip(state, const["sparse_first_rc"])] + state = [s + c for s, c in zip(state, const["sparse_first_rc"])] state = _matvec_kb(const["sparse_m_i"], state) n_partial = const["partial_rounds"] @@ -1208,10 +1180,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) - if r < n_partial - 1: state[0] = state[0] + const["sparse_scalar_rc"][r] old_s0 = state[0] - new_s0 = EF.zero() - for j in range(_POSEIDON_WIDTH): - new_s0 = new_s0 + state[j] * const["sparse_first_row"][r][j] - state[0] = new_s0 + state[0] = ef_sum(state[j] * const["sparse_first_row"][r][j] for j in range(_POSEIDON_WIDTH)) for i in range(1, _POSEIDON_WIDTH): state[i] = state[i] + old_s0 * const["sparse_v"][r][i - 1] @@ -1226,7 +1195,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) - last = 2 * (half_final - 1) state = _full_round(state, const["final_constants"][last], const["final_constants"][last + 1]) flag_permute = cols["flag_permute"] - not_permute = EF.one() - flag_permute + not_permute = ONE - flag_permute compression_last4 = not_permute - cols["flag_half_output"] for i in range(_POSEIDON_WIDTH // 2): gate = not_permute if i < _HALF_DIGEST_LEN else compression_last4 @@ -1237,9 +1206,8 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) - def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: const = _p1c() - flat, one, W = folder.flat, EF.one(), _POSEIDON_WIDTH + flat, W = folder.flat, _POSEIDON_WIDTH half_initial = half_final = const["half_full_rounds"] // 2 - Fb = lambda v: EF.from_base(Fp(v)) o = 0 @@ -1259,14 +1227,14 @@ def take(n: int) -> list[EF]: outputs_left, outputs_right = take(W // 2), take(W // 2) discriminator = ( - Fb(_POSEIDON_DISCRIMINATOR_BASE) - + flag_permute * Fb(_POSEIDON_PERMUTE_SHIFT) - + flag_half_output * Fb(_POSEIDON_HALF_OUTPUT_SHIFT) - + flag_hardcoded_left * Fb(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) - + flag_hardcoded_left * offset_hardcoded_left * Fb(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT) + fb(_POSEIDON_DISCRIMINATOR_BASE) + + flag_permute * fb(_POSEIDON_PERMUTE_SHIFT) + + flag_half_output * fb(_POSEIDON_HALF_OUTPUT_SHIFT) + + flag_hardcoded_left * fb(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) + + flag_hardcoded_left * offset_hardcoded_left * fb(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT) ) - not_hcl = one - flag_hardcoded_left - index_a = eff_idx_left_second - not_hcl * Fb(_HALF_DIGEST_LEN) + not_hcl = ONE - flag_hardcoded_left + index_a = eff_idx_left_second - not_hcl * fb(_HALF_DIGEST_LEN) folder.assert_zero(_eval_virtual_bus_column(extra_data, multiplicity, discriminator, [index_a, index_b, index_res])) for f in (multiplicity, flag_half_output, flag_hardcoded_left, flag_permute): @@ -1374,7 +1342,7 @@ def verify_execution( eta = state.sample() def powers(x: EF, n: int) -> list[EF]: - out, cur = [], EF.one() + out, cur = [], ONE for _ in range(n): out.append(cur) cur = cur * x @@ -1385,9 +1353,9 @@ def powers(x: EF, n: int) -> list[EF]: extra_data = {"logup_alphas_eq_poly": logup_alphas_eq, "bus_beta": bus_beta, "c": logup_c} # Initial AIR sum: Σ η^t · (bus_num · sign + β · (bus_den − c)). - initial_sum = EF.zero() + initial_sum = ZERO for (name, _), eta_pow in zip(tables_sorted, eta_powers): - sign = -EF.one() if tables_by_name[name].bus_direction == "Pull" else EF.one() + sign = -ONE if tables_by_name[name].bus_direction == "Pull" else ONE initial_sum = initial_sum + eta_pow * ( logup["bus_num"][name] * sign + bus_beta * (logup["bus_den"][name] - logup_c) ) @@ -1400,16 +1368,14 @@ def powers(x: EF, n: int) -> list[EF]: name: [(list(from_end(gkr_point, table_log_heights[name])), dict(logup["columns_values"][name]), {})] for name in tables_by_name } - my_air_final = EF.zero() + my_air_final = ZERO for (name, log_n_rows), eta_pow in zip(tables_sorted, eta_powers): meta, n_shift = tables_by_name[name], _TABLE_SPECS[name]["n_shift"] col_evals = state.next_extension_scalars_vec(meta.n_columns + n_shift) constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] - k_t = EF.one() - for x in sc_point[: n_max - log_n_rows]: - k_t = k_t * x + k_t = ef_prod(sc_point[: n_max - log_n_rows]) my_air_final = ( my_air_final + eta_pow * k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval From d7375deed12db03fe00cfb655998fda2b4789b5a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 19 May 2026 18:14:00 +0100 Subject: [PATCH 19/69] wip --- crates/lean_prover/verifier.py | 79 +++++++++++++++------------------- 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index b19294200..cb87b8116 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -82,16 +82,7 @@ def __neg__(self): def __mul__(self, o): if isinstance(o, Fp): return EF([a * o for a in self.c]) - a, b = self.c, o.c - prod = [Fp(0)] * 9 - for i in range(5): - for j in range(5): - prod[i + j] = prod[i + j] + a[i] * b[j] - for k in range(8, 4, -1): # X^k = X^(k-5)·(1 − X²) for k ≥ 5. - coef = prod[k] - prod[k - 5] = prod[k - 5] + coef - prod[k - 3] = prod[k - 3] - coef - return EF(prod[:5]) + return EF(_quintic_mul(self.c, o.c, Fp(0))) __rmul__ = __mul__ @@ -136,6 +127,19 @@ def ef_prod(factors) -> EF: return acc +def _quintic_mul(a, b, zero): + """Schoolbook product in `Fp[X]/(X⁵+X²−1)`. `a`, `b` and the result are length-5 + coefficient lists over any ring sharing the additive identity `zero`.""" + prod = [zero] * 9 + for i in range(5): + for j in range(5): + prod[i + j] = prod[i + j] + a[i] * b[j] + for k in range(8, 4, -1): # X^k = X^(k−5)·(1 − X²) for k ≥ 5. + prod[k - 5] = prod[k - 5] + prod[k] + prod[k - 3] = prod[k - 3] - prod[k] + return prod[:5] + + _POSEIDON16 = Poseidon1(PARAMS_16) @@ -503,13 +507,15 @@ def verify_sumcheck( def combine_constraints(state: VerifierState, target: EF, constraints: list[SparseStatement]) -> tuple[EF, list[EF]]: + """Fold all constraint values into `target` via powers of γ; return those γ-power weights.""" gamma: EF = state.sample() - combo = [ONE] + combo: list[EF] = [] + g = ONE for smt in constraints: - for v in smt.values: - target = target + combo[-1] * v[1] - combo.append(combo[-1] * gamma) - combo.pop() + for _, value in smt.values: + target = target + g * value + combo.append(g) + g = g * gamma return target, combo @@ -755,7 +761,8 @@ def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: def from_end(seq: Sequence, n: int) -> list: - return list(seq[len(seq) - n :]) if n else [] + """The last `n` elements of `seq` (empty list when `n == 0`).""" + return list(seq[-n:]) if n else [] def mle_of_01234567_etc(point: Sequence[EF]) -> EF: @@ -982,11 +989,7 @@ def air_constraint_eval( extra_data: dict, ) -> EF: folder = ConstraintFolder(col_evals[: table.n_columns], col_evals[table.n_columns :], alpha_powers) - { - "execution": _eval_air_execution, - "extension_op": _eval_air_extension_op, - "poseidon16_compress": _eval_air_poseidon16, - }[table.name](folder, table, extra_data) + _TABLE_SPECS[table.name]["air"](folder, table, extra_data) return folder.accumulator @@ -999,9 +1002,9 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: pc_shift, fp_shift = folder.shift[0], folder.shift[1] # nu_x = flag·operand + (1 − flag − flag_ab_fp)·value + flag_ab_fp·(fp + operand) - nfa = -(flag_a + flag_ab_fp - ONE) - nfb = -(flag_b + flag_ab_fp - ONE) - nfc = -(flag_c + flag_c_fp - ONE) + nfa = ONE - flag_a - flag_ab_fp + nfb = ONE - flag_b - flag_ab_fp + nfc = ONE - flag_c - flag_c_fp nu_a = flag_a * operand_a + nfa * value_a + flag_ab_fp * (fp + operand_a) nu_b = flag_b * operand_b + nfb * value_b + flag_ab_fp * (fp + operand_b) nu_c = flag_c * operand_c + nfc * value_c + flag_c_fp * (fp + operand_c) @@ -1009,7 +1012,7 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: # aux ∈ {0,1,2}: 0=nothing, 1=add, 2=deref. add = aux * fb(2) - aux * aux deref = aux * (aux - ONE) * EF.from_base(_INV_TWO) - is_precompile = -(add + mul + deref + jump - ONE) + is_precompile = ONE - add - mul - deref - jump az = folder.assert_zero az(_eval_virtual_bus_column(extra_data, is_precompile, discriminator, [nu_a, nu_b, nu_c])) @@ -1024,7 +1027,7 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: az(jc * (nu_a - ONE)) az(jc * (pc_shift - nu_b)) az(jc * (fp_shift - nu_c)) - not_jc = -(jc - ONE) + not_jc = ONE - jc az(not_jc * (pc_shift - (pc + ONE))) az(not_jc * (fp_shift - fp)) @@ -1038,21 +1041,9 @@ def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: - """Port of `quintic_mul` (multiplication of two EF⁵ as quintic-extension elements).""" + """Multiply two quintic-extension elements whose coefficients are themselves `EF`.""" assert len(a) == 5 and len(b) == 5 - b0m3, b1m4, b4m2 = b[0] - b[3], b[1] - b[4], b[4] - b[2] - rows = [ - [b[0], b[4], b[3], b[2], b1m4], - [b[1], b[0], b[4], b[3], b[2]], - [b[2], b1m4, b0m3, b4m2, b[3] - b1m4], - [b[3], b[2], b1m4, b0m3, b4m2], - [b[4], b[3], b[2], b1m4, b0m3], - ] - - def dot(row: list[EF]) -> EF: - return ef_sum(a[i] * row[i] for i in range(5)) - - return [dot(row) for row in rows] + return _quintic_mul(a, b, ZERO) def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: @@ -1080,7 +1071,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_dat for x in (is_be, start, flag_add, flag_mul, flag_poly_eq): folder.assert_bool(x) - is_ee, not_start_sh = -(is_be - ONE), -(start_sh - ONE) + is_ee, not_start_sh = ONE - is_be, ONE - start_sh va_x = [va[0]] + [va[k] * is_ee for k in range(1, 5)] comp_tail = [comp_sh[k] * not_start_sh for k in range(5)] va_vb = _quintic_mul_ef(va_x, vb) @@ -1260,9 +1251,9 @@ def take(n: int) -> list[EF]: _TABLE_SPECS: dict[str, dict] = { - "execution": {"degree": 5, "n_constraints": 13, "n_shift": 2}, - "extension_op": {"degree": 4, "n_constraints": 33, "n_shift": 13}, - "poseidon16_compress": {"degree": 10, "n_constraints": 99, "n_shift": 0}, + "execution": {"degree": 5, "n_constraints": 13, "n_shift": 2, "air": _eval_air_execution}, + "extension_op": {"degree": 4, "n_constraints": 33, "n_shift": 13, "air": _eval_air_extension_op}, + "poseidon16_compress": {"degree": 10, "n_constraints": 99, "n_shift": 0, "air": _eval_air_poseidon16}, } From 25115f1025f2ba0e0037336ee9ebdc85e67277c4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 20 May 2026 22:28:43 +0100 Subject: [PATCH 20/69] make it work --- crates/lean_prover/tests/dump_zkvm_vector.rs | 58 +------ crates/lean_prover/verifier.py | 158 +++++++++++-------- 2 files changed, 97 insertions(+), 119 deletions(-) diff --git a/crates/lean_prover/tests/dump_zkvm_vector.rs b/crates/lean_prover/tests/dump_zkvm_vector.rs index 89c884660..32af4c14d 100644 --- a/crates/lean_prover/tests/dump_zkvm_vector.rs +++ b/crates/lean_prover/tests/dump_zkvm_vector.rs @@ -41,33 +41,10 @@ struct RawProofJson { merkle_openings: Vec, } -#[derive(Serialize)] -#[serde(tag = "kind", content = "value")] -enum BusDataJson { - Column(usize), - Constant(usize), -} - -#[derive(Serialize)] -struct BusJson { - direction: &'static str, - multiplicity: usize, - discriminator: BusDataJson, - data: Vec, -} - -#[derive(Serialize)] -struct LookupJson { - index: usize, - values: Vec, -} - #[derive(Serialize)] struct TableInfoJson { name: &'static str, n_columns: usize, - bus: BusJson, - lookups: Vec, } #[derive(Serialize)] @@ -75,9 +52,9 @@ struct ConstantsJson { n_instruction_columns: usize, n_runtime_columns: usize, col_pc: usize, - logup_memory_discriminator: usize, - logup_bytecode_discriminator: usize, - max_precompile_bus_width: usize, + logup_memory_domainsep: usize, + logup_bytecode_domainsep: usize, + log_max_bus_width: usize, starting_pc: usize, ending_pc: usize, } @@ -147,34 +124,11 @@ fn dump_zkvm_vector() { let public_input = verified.input_data_hash; let raw_proof = verified.raw_proof; - let convert_bus_data = |d: BusData| match d { - BusData::Column(c) => BusDataJson::Column(c), - BusData::Constant(v) => BusDataJson::Constant(v), - }; - let convert_bus = |bus: Bus| BusJson { - direction: match bus.direction { - BusDirection::Pull => "Pull", - BusDirection::Push => "Push", - }, - multiplicity: bus.multiplicity, - discriminator: convert_bus_data(bus.discriminator), - data: bus.data.into_iter().map(convert_bus_data).collect(), - }; - let table_infos: Vec = ALL_TABLES .iter() .map(|t| TableInfoJson { name: t.name(), n_columns:
::n_columns(t), - bus: convert_bus(t.bus()), - lookups: t - .lookups() - .into_iter() - .map(|l| LookupJson { - index: l.index, - values: l.values, - }) - .collect(), }) .collect(); @@ -200,9 +154,9 @@ fn dump_zkvm_vector() { n_instruction_columns: N_INSTRUCTION_COLUMNS, n_runtime_columns: N_RUNTIME_COLUMNS, col_pc: COL_PC, - logup_memory_discriminator: LOGUP_MEMORY_DISCRIMINATOR, - logup_bytecode_discriminator: LOGUP_BYTECODE_DISCRIMINATOR, - max_precompile_bus_width: MAX_PRECOMPILE_BUS_WIDTH, + logup_memory_domainsep: LOGUP_MEMORY_DOMAINSEP, + logup_bytecode_domainsep: LOGUP_BYTECODE_DOMAINSEP, + log_max_bus_width: LOG_MAX_BUS_WIDTH, starting_pc: STARTING_PC, ending_pc: bytecode.ending_pc, }, diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index cb87b8116..f8863859d 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -666,23 +666,43 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None return folding_flat +def _table_buses(name: str, n_columns: int) -> tuple: + if name == "execution": + return ( + ("col_mult", "Push"), + ("byte_lookup",), + ("mem_group", 2, 5, 1), # addr_a, value_a + ("mem_group", 3, 6, 1), # addr_b, value_b + ("mem_group", 4, 7, 1), # addr_c, value_c + ) + if name == "extension_op": + return ( + ("col_mult", "Pull"), + ("mem_group", 6, 14, 5), # idx_a, va + ("mem_group", 7, 19, 5), # idx_b, vb + ("mem_group", 13, 24, 5), # idx_res, vres + ) + if name == "poseidon16_compress": + return ( + ("col_mult", "Pull"), + ("mem_group", 6, 9, 4), + ("mem_group", 7, 13, 4), + ("mem_group", 1, 17, 8), + ("mem_group", 2, n_columns - 16, 16), + ) + raise ProofError(f"unknown table: {name}") + @dataclass(frozen=True) class TableMeta: name: str n_columns: int - bus_direction: str # "Pull" or "Push" - lookups: tuple[tuple[int, tuple[int, ...]], ...] # (index_col, value_cols) + buses: tuple def tables_from_json(obj: list[dict]) -> list[TableMeta]: return [ - TableMeta( - name=t["name"], - n_columns=int(t["n_columns"]), - bus_direction=t["bus"]["direction"], - lookups=tuple((int(l["index"]), tuple(int(v) for v in l["values"])) for l in t["lookups"]), - ) + TableMeta(name=t["name"], n_columns=int(t["n_columns"]), buses=_table_buses(t["name"], int(t["n_columns"]))) for t in obj ] @@ -710,7 +730,7 @@ def values_at(d: dict[int, EF], n_vars: int) -> list[tuple[int, EF]]: for name, n_vars in tables_sorted: if name == "execution": - # PC column: pin first row to starting_pc, last row to ending_pc. + # PC column: pin first row to STARTING_PC, last row to ending_pc. for idx, pc in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: out.append(SparseStatement.unique_value(stacked_n_vars, offset + (col_pc << n_vars) + idx, fb(pc))) @@ -822,20 +842,21 @@ def verify_generic_logup( n_instr_cols = constants["n_instruction_columns"] n_runtime_cols = constants["n_runtime_columns"] col_pc = constants["col_pc"] - disc_mem = Fp(constants["logup_memory_discriminator"]) - disc_byte = Fp(constants["logup_bytecode_discriminator"]) + ds_mem = Fp(constants["logup_memory_domainsep"]) + ds_byte = Fp(constants["logup_bytecode_domainsep"]) tables_sorted = sort_tables_by_height(table_log_heights) log_bytecode = log2_strict_usize(len(bytecode_multilinear) // (1 << log2_ceil_usize(n_instr_cols))) log_instr = log2_ceil_usize(n_instr_cols) - log_n_cycles = table_log_heights["execution"] - table_cols = lambda n: sum(len(vs) for _, vs in tables[n].lookups) + 1 + def n_buses(name: str) -> int: + # mem_group entries expand to `n` individual buses. + return sum(b[3] if b[0] == "mem_group" else 1 for b in tables[name].buses) + total_active_len = ( (1 << log_memory) + max(1 << log_bytecode, 1 << tables_sorted[0][1]) - + (1 << log_n_cycles) - + sum(table_cols(n) << h for n, h in tables_sorted) + + sum(n_buses(n) << h for n, h in tables_sorted) ) total_gkr_n_vars = log2_ceil_usize(total_active_len) @@ -850,13 +871,13 @@ def pref_at(offset: int, log_height: int) -> EF: bits = to_big_endian_in_field(offset >> log_height, n_missing) return eq_poly_outside(bits, point_gkr[:n_missing]) - # Memory. + # Memory (data order: [value_index, value_memory] mirrors `crates/sub_protocols/src/logup.rs`). mem_pt = from_end(point_gkr, log_memory) pref = pref_at(0, log_memory) value_memory_acc = state.next_extension_scalar() - value_memory = state.next_extension_scalar() - fp_mem = finger_print(disc_mem, [value_memory, mle_of_01234567_etc(mem_pt)], alphas_eq_poly) num = num - pref * value_memory_acc + value_memory = state.next_extension_scalar() + fp_mem = finger_print(ds_mem, [mle_of_01234567_etc(mem_pt), value_memory], alphas_eq_poly) den = den + pref * (c - fp_mem) offset = 1 << log_memory @@ -871,7 +892,7 @@ def pref_at(offset: int, log_height: int) -> EF: fp_byte = ( bytecode_value * correction + mle_of_01234567_etc(byte_pt) * alphas_eq_poly[n_instr_cols] - + alphas_eq_poly[-1] * EF.from_base(disc_byte) + + alphas_eq_poly[-1] * EF.from_base(ds_byte) ) num = num - pref * value_bytecode_acc den = ( @@ -881,51 +902,54 @@ def pref_at(offset: int, log_height: int) -> EF: ) offset += 1 << log_byte_pad - # Per-table: execution bytecode lookup + bus column + per-lookup memory reads. + # Per-table: walk the bus spec in the same order as the Rust prover. The prover + # writes col_evals for new (uncached) columns in `bus.data` order via a single + # `add_extension_scalars` chunk per bus — the verifier must read in the same chunks. bus_num_vals: dict[str, EF] = {} bus_den_vals: dict[str, EF] = {} columns_values: dict[str, dict[int, EF]] = {} + for name, log_n_rows in tables_sorted: meta = tables[name] table_values: dict[int, EF] = {} + row_stride = 1 << log_n_rows - if name == "execution": - eval_on_pc = state.next_extension_scalar() - instr_evals = state.next_extension_scalars_vec(n_instr_cols) - table_values[col_pc] = eval_on_pc - table_values.update({n_runtime_cols + i: e for i, e in enumerate(instr_evals)}) + for bus in meta.buses: pref = pref_at(offset, log_n_rows) - fp = finger_print(disc_byte, list(instr_evals) + [eval_on_pc], alphas_eq_poly) - num = num + pref - den = den + pref * (c - fp) - offset += 1 << log_n_rows - - eval_on_multiplicity = state.next_extension_scalar() - eval_on_data = state.next_extension_scalar() - pref = pref_at(offset, log_n_rows) - num = num + pref * eval_on_multiplicity - den = den + pref * eval_on_data - bus_num_vals[name] = eval_on_multiplicity - bus_den_vals[name] = eval_on_data - offset += 1 << log_n_rows - - for index_col, value_cols in meta.lookups: - index_eval = state.next_extension_scalar() - assert index_col not in table_values - table_values[index_col] = index_eval - for i, col_index in enumerate(value_cols): - value_eval = state.next_extension_scalar() - assert col_index not in table_values - table_values[col_index] = value_eval - pref = pref_at(offset, log_n_rows) - fp = finger_print( - disc_mem, - [value_eval, index_eval + fb(i)], - alphas_eq_poly, - ) - num = num + pref - den = den + pref * (c - fp) - offset += 1 << log_n_rows + match bus: + case ("col_mult", _direction): + bus_num_vals[name] = state.next_extension_scalar() + bus_den_vals[name] = state.next_extension_scalar() + num = num + pref * bus_num_vals[name] + den = den + pref * bus_den_vals[name] + offset += row_stride + case ("byte_lookup",): + cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [col_pc] + evals = state.next_extension_scalars_vec(len(cols)) + for c_idx, e in zip(cols, evals): + table_values[c_idx] = e + num = num + pref # Push direction + den = den + pref * (c - finger_print(ds_byte, evals, alphas_eq_poly)) + offset += row_stride + case ("mem_group", idx_col, vals_start, n): + # One bus per row in the group; first sees idx_col fresh, the rest + # see only val_col fresh (mirrors the Rust prover's dedup logic). + for i in range(n): + val_col = vals_start + i + idx_fresh = idx_col not in table_values + val_fresh = val_col not in table_values + evals = iter(state.next_extension_scalars_vec(idx_fresh + val_fresh)) + if idx_fresh: + table_values[idx_col] = next(evals) + if val_fresh: + table_values[val_col] = next(evals) + pref = pref_at(offset, log_n_rows) + fp = finger_print(ds_mem, [table_values[idx_col] + fb(i), table_values[val_col]], alphas_eq_poly) + num = num + pref # Push direction + den = den + pref * (c - fp) + offset += row_stride + case _: + raise ProofError(f"unknown bus kind: {bus[0]}") columns_values[name] = table_values @@ -993,7 +1017,7 @@ def air_constraint_eval( return folder.accumulator -def _eval_air_execution(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: +def _eval_air_execution(folder: ConstraintFolder, _table: TableMeta, extra_data: dict) -> None: # fmt: off (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, @@ -1046,7 +1070,7 @@ def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: return _quintic_mul(a, b, ZERO) -def _eval_air_extension_op(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: +def _eval_air_extension_op(folder: ConstraintFolder, _table: TableMeta, extra_data: dict) -> None: # Layout: shift columns 0..13 = (is_be, start, len, flag_{add,mul,poly_eq}, # idx_{a,b}, comp[0..5]); then idx_res, va, vb, vres (5 each). f = folder.flat @@ -1147,7 +1171,7 @@ def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: return state -def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) -> None: +def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, _extra_data: dict) -> None: """AIR for Poseidon1-16. Each `post` column commits an intermediate state, which we constrain against the local computation, then adopt to bound polynomial degree.""" const = _p1c() @@ -1195,7 +1219,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, extra_data: dict) - folder.assert_zero(flag_permute * (state[i + _POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) -def _eval_air_poseidon16(folder: ConstraintFolder, table: TableMeta, extra_data: dict) -> None: +def _eval_air_poseidon16(folder: ConstraintFolder, _table: TableMeta, extra_data: dict) -> None: const = _p1c() flat, W = folder.flat, _POSEIDON_WIDTH half_initial = half_final = const["half_full_rounds"] // 2 @@ -1252,8 +1276,8 @@ def take(n: int) -> list[EF]: _TABLE_SPECS: dict[str, dict] = { "execution": {"degree": 5, "n_constraints": 13, "n_shift": 2, "air": _eval_air_execution}, - "extension_op": {"degree": 4, "n_constraints": 33, "n_shift": 13, "air": _eval_air_extension_op}, - "poseidon16_compress": {"degree": 10, "n_constraints": 99, "n_shift": 0, "air": _eval_air_poseidon16}, + "extension_op": {"degree": 6, "n_constraints": 33, "n_shift": 13, "air": _eval_air_extension_op}, + "poseidon16_compress": {"degree": 10, "n_constraints": 100, "n_shift": 0, "air": _eval_air_poseidon16}, } @@ -1310,8 +1334,7 @@ def verify_execution( logup_c = state.sample() state.duplex() - max_bus_width = 1 + max(constants["max_precompile_bus_width"], constants["n_instruction_columns"]) - logup_alphas = state.sample_vec(log2_ceil_usize(max_bus_width)) + logup_alphas = state.sample_vec(constants["log_max_bus_width"]) logup_alphas_eq = eval_eq(logup_alphas) logup = verify_generic_logup( state, @@ -1343,12 +1366,13 @@ def powers(x: EF, n: int) -> list[EF]: eta_powers = powers(eta, len(tables_sorted)) extra_data = {"logup_alphas_eq_poly": logup_alphas_eq, "bus_beta": bus_beta, "c": logup_c} - # Initial AIR sum: Σ η^t · (bus_num · sign + β · (bus_den − c)). + # Initial AIR sum: Σ η^t · (bus_num · sign + β · (c − bus_den)). The sign is the + # direction of each table's unique Column-multiplicity bus (always `buses[0]`). initial_sum = ZERO for (name, _), eta_pow in zip(tables_sorted, eta_powers): - sign = -ONE if tables_by_name[name].bus_direction == "Pull" else ONE + sign = -ONE if tables_by_name[name].buses[0][1] == "Pull" else ONE initial_sum = initial_sum + eta_pow * ( - logup["bus_num"][name] * sign + bus_beta * (logup["bus_den"][name] - logup_c) + logup["bus_num"][name] * sign + bus_beta * (logup_c - logup["bus_den"][name]) ) n_max = tables_sorted[0][1] sc_point, sc_value = verify_sumcheck( From e8cc7b57983c59a2660c7e5a2a44a7dbb6323ea0 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 21 May 2026 00:56:06 +0200 Subject: [PATCH 21/69] wip --- crates/lean_prover/verifier.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index f8863859d..458b48ab3 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -43,6 +43,8 @@ MIN_WHIR_LOG_INV_RATE, MAX_WHIR_LOG_INV_RATE = 1, 4 MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, BASE_TWO_ADICITY = 8, 8, 24 +MAX_BYTECODE_LOG_SIZE = 22 +MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension_op": 21, "poseidon16_compress": 21} WHIR_CONFIGS_PATH = "whir_configs.json" @@ -1302,12 +1304,18 @@ def verify_execution( raise ProofError("InvalidRate") if any(h < MIN_LOG_N_ROWS_PER_TABLE for h in table_log_n_rows): raise ProofError("InvalidProof: table too small") + for t, h in zip(tables, table_log_n_rows): + limit = MAX_LOG_N_ROWS_PER_TABLE.get(t.name) + if limit is None: + raise ProofError(f"InvalidProof: unknown table {t.name}") + if h > limit: + raise ProofError(f"InvalidProof: table {t.name} too large (log_n_rows={h} > {limit})") if log_memory < max(max(table_log_n_rows, default=0), bytecode_log_size): raise ProofError("InvalidProof: memory smaller than tables/bytecode") if not MIN_LOG_MEMORY_SIZE <= log_memory <= MAX_LOG_MEMORY_SIZE: raise ProofError("InvalidProof: log_memory out of range") - if bytecode_log_size < MIN_BYTECODE_LOG_SIZE: - raise ProofError("InvalidProof: bytecode too small") + if not MIN_BYTECODE_LOG_SIZE <= bytecode_log_size <= MAX_BYTECODE_LOG_SIZE: + raise ProofError("InvalidProof: bytecode log_size out of range") table_log_heights = {t.name: h for t, h in zip(tables, table_log_n_rows)} tables_by_name = {t.name: t for t in tables} @@ -1427,6 +1435,15 @@ def powers(x: EF, n: int) -> list[EF]: ) whir_verify(state, cfg, parsed_commitment, global_statements) + if state.offset != len(state.transcript): + raise ProofError( + f"InvalidProof: transcript not fully consumed ({state.offset}/{len(state.transcript)} scalars read)" + ) + if state.open_idx != len(state.openings): + raise ProofError( + f"InvalidProof: Merkle openings not fully consumed ({state.open_idx}/{len(state.openings)} openings used)" + ) + return {"log_inv_rate": log_inv_rate, "log_memory": log_memory, "stacked_n_vars": stacked_n_vars} From 4b6aa431948f4dae48e79d2e2db7fd271436a54a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 21 May 2026 22:09:43 +0100 Subject: [PATCH 22/69] wip --- crates/lean_prover/verifier.py | 60 ++++++++++++++++------------------ 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 458b48ab3..2ef9dd4b1 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -996,16 +996,12 @@ def assert_bool(self, x: EF) -> None: self.assert_zero(x * (ONE - x)) -def _eval_virtual_bus_column(extra_data: dict, multiplicity: EF, discriminator: EF, data: Sequence[EF]) -> EF: - """`(Σ αᵢ·dataᵢ + α_last·discriminator)·β + multiplicity`. - - The per-bus `discriminator` keeps the three precompile buses disjoint from each - other and from the memory/bytecode lookups (reserved discriminators 1 and 2).""" +def _eval_bus_virtual(folder: "ConstraintFolder", extra_data: dict, multiplicity: EF, discriminator: EF, data: Sequence[EF]) -> None: alphas: list[EF] = extra_data["logup_alphas_eq_poly"] - bus_beta: EF = extra_data["bus_beta"] assert len(data) < len(alphas) - inner = ef_sum(a * d for a, d in zip(alphas, data)) + alphas[-1] * discriminator - return inner * bus_beta + multiplicity + folder.assert_zero(multiplicity) + encoded = ef_sum(a * d for a, d in zip(alphas, data)) + alphas[-1] * discriminator + folder.assert_zero(encoded) def air_constraint_eval( @@ -1041,7 +1037,7 @@ def _eval_air_execution(folder: ConstraintFolder, _table: TableMeta, extra_data: is_precompile = ONE - add - mul - deref - jump az = folder.assert_zero - az(_eval_virtual_bus_column(extra_data, is_precompile, discriminator, [nu_a, nu_b, nu_c])) + _eval_bus_virtual(folder, extra_data, is_precompile, discriminator, [nu_a, nu_b, nu_c]) az(nfa * (addr_a - (fp + operand_a))) az(nfb * (addr_b - (fp + operand_b))) az(nfc * (addr_c - (fp + operand_c))) @@ -1090,9 +1086,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, _table: TableMeta, extra_da + flag_poly_eq * fb(_EXT_OP_FLAG_POLY_EQ) + len_col * fb(_EXT_OP_LEN_MULTIPLIER) ) - folder.assert_zero( - _eval_virtual_bus_column(extra_data, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res]) - ) + _eval_bus_virtual(folder, extra_data, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res]) for x in (is_be, start, flag_add, flag_mul, flag_poly_eq): folder.assert_bool(x) @@ -1253,7 +1247,7 @@ def take(n: int) -> list[EF]: not_hcl = ONE - flag_hardcoded_left index_a = eff_idx_left_second - not_hcl * fb(_HALF_DIGEST_LEN) - folder.assert_zero(_eval_virtual_bus_column(extra_data, multiplicity, discriminator, [index_a, index_b, index_res])) + _eval_bus_virtual(folder, extra_data, multiplicity, discriminator, [index_a, index_b, index_res]) for f in (multiplicity, flag_half_output, flag_hardcoded_left, flag_permute): folder.assert_bool(f) folder.assert_zero(flag_permute * (flag_half_output + flag_hardcoded_left)) @@ -1277,9 +1271,9 @@ def take(n: int) -> list[EF]: _TABLE_SPECS: dict[str, dict] = { - "execution": {"degree": 5, "n_constraints": 13, "n_shift": 2, "air": _eval_air_execution}, - "extension_op": {"degree": 6, "n_constraints": 33, "n_shift": 13, "air": _eval_air_extension_op}, - "poseidon16_compress": {"degree": 10, "n_constraints": 100, "n_shift": 0, "air": _eval_air_poseidon16}, + "execution": {"degree": 5, "n_constraints": 14, "n_shift": 2, "air": _eval_air_execution}, + "extension_op": {"degree": 6, "n_constraints": 35, "n_shift": 13, "air": _eval_air_extension_op}, + "poseidon16_compress": {"degree": 10, "n_constraints": 101, "n_shift": 0, "air": _eval_air_poseidon16}, } @@ -1357,11 +1351,7 @@ def verify_execution( ) gkr_point = logup["gkr_point"] - bus_beta = state.sample() - state.duplex() air_alpha = state.sample() - state.duplex() - eta = state.sample() def powers(x: EF, n: int) -> list[EF]: out, cur = [], ONE @@ -1370,18 +1360,23 @@ def powers(x: EF, n: int) -> list[EF]: cur = cur * x return out - alpha_powers = powers(air_alpha, max(_TABLE_SPECS[n]["n_constraints"] for n in tables_by_name) + 1) - eta_powers = powers(eta, len(tables_sorted)) - extra_data = {"logup_alphas_eq_poly": logup_alphas_eq, "bus_beta": bus_beta, "c": logup_c} + total_air_constraints = sum(_TABLE_SPECS[n]["n_constraints"] for n, _ in tables_sorted) + alpha_powers = powers(air_alpha, total_air_constraints) + alpha_offsets: list[int] = [] + cumulative = 0 + for name, _ in tables_sorted: + alpha_offsets.append(cumulative) + cumulative += _TABLE_SPECS[name]["n_constraints"] + + extra_data = {"logup_alphas_eq_poly": logup_alphas_eq} - # Initial AIR sum: Σ η^t · (bus_num · sign + β · (c − bus_den)). The sign is the - # direction of each table's unique Column-multiplicity bus (always `buses[0]`). + # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (c − bus_den)). The + # sign is the direction of each table's unique Column-multiplicity bus. initial_sum = ZERO - for (name, _), eta_pow in zip(tables_sorted, eta_powers): + for (name, _), offset in zip(tables_sorted, alpha_offsets): sign = -ONE if tables_by_name[name].buses[0][1] == "Pull" else ONE - initial_sum = initial_sum + eta_pow * ( - logup["bus_num"][name] * sign + bus_beta * (logup_c - logup["bus_den"][name]) - ) + initial_sum = initial_sum + alpha_powers[offset] * (logup["bus_num"][name] * sign) + initial_sum = initial_sum + alpha_powers[offset + 1] * (logup_c - logup["bus_den"][name]) n_max = tables_sorted[0][1] sc_point, sc_value = verify_sumcheck( state, initial_sum, n_max, max(_TABLE_SPECS[n]["degree"] + 1 for n, _ in tables_sorted) @@ -1392,16 +1387,17 @@ def powers(x: EF, n: int) -> list[EF]: for name in tables_by_name } my_air_final = ZERO - for (name, log_n_rows), eta_pow in zip(tables_sorted, eta_powers): + for (name, log_n_rows), offset in zip(tables_sorted, alpha_offsets): meta, n_shift = tables_by_name[name], _TABLE_SPECS[name]["n_shift"] col_evals = state.next_extension_scalars_vec(meta.n_columns + n_shift) - constraint_eval = air_constraint_eval(meta, col_evals, alpha_powers, extra_data) + alpha_slice = alpha_powers[offset : offset + _TABLE_SPECS[name]["n_constraints"]] + constraint_eval = air_constraint_eval(meta, col_evals, alpha_slice, extra_data) natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = ef_prod(sc_point[: n_max - log_n_rows]) my_air_final = ( my_air_final - + eta_pow * k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval + + k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval ) eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} From 96a117783ef153d7832653d2e880e4620017afff Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 24 May 2026 00:52:23 +0400 Subject: [PATCH 23/69] w --- crates/lean_prover/verifier.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 2ef9dd4b1..18a56b68f 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -6,6 +6,7 @@ Setup (one-time): uv venv .venv --python 3.12 + source .venv/bin/activate VIRTUAL_ENV=.venv uv pip install "git+https://github.com/leanEthereum/leanSpec.git" cargo test --release -p lean_prover --test dump_whir_configs -- --nocapture cargo test --release -p lean_prover --test dump_poseidon1_constants -- --nocapture @@ -32,6 +33,8 @@ RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 RATE, WIDTH, DIGEST_ELEMS = 8, 16, 8 CAPACITY = WIDTH - RATE +PUBLIC_INPUT_SIZE = DIGEST_ELEMS + # fmt: off SNARK_DOMAIN_SEP = [Fp(v) for v in ( @@ -162,6 +165,14 @@ def hash_slice(data: Sequence[Fp]) -> list[Fp]: return state[:DIGEST_ELEMS] +def fiat_shamir_domain_sep(bytecode_hash: Sequence[Fp], public_input_size: int) -> list[Fp]: + """Domain-separator absorbed before the proof. Mixes the bytecode hash and the + bytecode's declared `public_input_size` (mirrors `lean_prover::fiat_shamir_domain_sep`).""" + tail = [Fp(public_input_size)] + [Fp(0)] * (RATE - 1) + extended = poseidon16_compress(SNARK_DOMAIN_SEP, tail) + return poseidon16_compress(bytecode_hash, extended) + + class Challenger: """Duplex-sponge Fiat-Shamir. `observe(chunk)` permutes `state[:CAPACITY] || chunk`; `_sample_rate()` reads `state[CAPACITY:]` once per `duplex()`.""" @@ -1286,16 +1297,19 @@ def verify_execution( constants: dict, bytecode_multilinear: list[int], ) -> dict: + if len(public_input) != PUBLIC_INPUT_SIZE: + raise ProofError("InvalidProof: public_input length mismatch") + state = VerifierState(proof) state.observe_scalars(list(public_input)) - state.observe_scalars(poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP)) + state.observe_scalars(fiat_shamir_domain_sep(bytecode_hash, PUBLIC_INPUT_SIZE)) - dims = [int(x.value) for x in state.next_base_scalars_vec(3 + len(tables))] - log_inv_rate, log_memory, public_input_len, *table_log_n_rows = dims - if public_input_len != len(public_input): - raise ProofError("InvalidProof: public_input length mismatch") + dims = [int(x.value) for x in state.next_base_scalars_vec(2 + len(tables))] + log_inv_rate, log_memory, *table_log_n_rows = dims if not MIN_WHIR_LOG_INV_RATE <= log_inv_rate <= MAX_WHIR_LOG_INV_RATE: raise ProofError("InvalidRate") + if log_memory < log2_strict_usize(PUBLIC_INPUT_SIZE): + raise ProofError("InvalidProof: memory smaller than public_input_size") if any(h < MIN_LOG_N_ROWS_PER_TABLE for h in table_log_n_rows): raise ProofError("InvalidProof: table too small") for t, h in zip(tables, table_log_n_rows): From a3def88b9c56e800ee013d2cbe3b43d25210fe46 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 24 May 2026 01:42:03 +0400 Subject: [PATCH 24/69] wip --- crates/lean_prover/poseidon1_constants.json | 1 - .../tests/check_poseidon1_constants.rs | 129 ++ .../lean_prover/tests/check_whir_configs.rs | 79 + .../tests/dump_poseidon1_constants.rs | 94 -- crates/lean_prover/tests/dump_whir_configs.rs | 84 - crates/lean_prover/verifier.py | 61 +- crates/lean_prover/whir_configs.json | 1478 ----------------- 7 files changed, 241 insertions(+), 1685 deletions(-) delete mode 100644 crates/lean_prover/poseidon1_constants.json create mode 100644 crates/lean_prover/tests/check_poseidon1_constants.rs create mode 100644 crates/lean_prover/tests/check_whir_configs.rs delete mode 100644 crates/lean_prover/tests/dump_poseidon1_constants.rs delete mode 100644 crates/lean_prover/tests/dump_whir_configs.rs delete mode 100644 crates/lean_prover/whir_configs.json diff --git a/crates/lean_prover/poseidon1_constants.json b/crates/lean_prover/poseidon1_constants.json deleted file mode 100644 index e5e1af7fb..000000000 --- a/crates/lean_prover/poseidon1_constants.json +++ /dev/null @@ -1 +0,0 @@ -{"half_full_rounds":4,"partial_rounds":20,"initial_constants":[[2128964168,288780357,316938561,2126233899,426817493,1714118888,1045008582,1738510837,889721787,8866516,681576474,419059826,1596305521,1583176088,1584387047,1529751136],[1863858111,1072044075,517831365,1464274176,1138001621,428001039,245709561,1641420379,1365482496,770454828,693167409,757905735,136670447,436275702,525466355,1559174242],[1030087950,869864998,322787870,267688717,948964561,740478015,679816114,113662466,2066544572,1744924186,367094720,1380455578,1842483872,416711434,1342291586,1692058446],[1493348999,1113949088,210900530,1071655077,610242121,1136339326,2020858841,1019840479,678147278,1678413261,1361743414,61132629,1209546658,64412292,1936878279,1980661727]],"final_constants":[[1983525157,1330885184,414710339,733907571,479859442,1064293389,236801732,325174861,162067568,64109120,278581904,683867016,996448498,1960361559,1782740946,415413204],[1649591052,130819424,547348827,1386569644,1307680439,38932758,1581338609,1020895732,5942549,665140992,1924917707,1910029693,1100265370,1223195250,859919676,1674792874],[321520099,942924505,1232236036,88692728,2071051492,1945027965,1433294131,531185630,879398056,291692510,1546702888,155861652,810736858,932742296,1374710679,1703184249],[1973006548,1131403964,1724233597,1086876318,669451611,1829624280,2119538869,441255155,1580936135,1396398895,1043570981,1716351438,942566442,616885102,334644983,132306927]],"sparse_m_i":[[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1176991763,1962798433,507789489,1019168605,1163325691,466620818,1131708271,931504963,918112312,86863075,882630651,84434949,754655560,375632733,210588963],[0,1406869940,217296974,97037986,2020988961,1368157387,446815816,456620646,1350101418,1922416357,1227469637,603478726,1537295456,873165878,155811605,375632733],[0,682820181,2031016045,138039228,846585925,558910395,9722937,1543529703,2088599457,1481139936,255018864,2130530098,43680256,864171667,873165878,754655560],[0,110418656,1501074676,1412834556,1032465671,563855872,962367231,788585369,1597452496,62007254,389404591,904725063,1698425244,43680256,1537295456,84434949],[0,1538375796,2102000169,1333501812,530151948,2053218304,1744692061,1352986051,701513153,1663428696,1849567553,1774105504,904725063,2130530098,603478726,882630651],[0,807210699,1559543298,304640754,399071438,1521605122,2097677068,1930489690,1512116835,467964189,1473717591,1849567553,389404591,255018864,1227469637,86863075],[0,604440680,1962894682,695994228,2105212903,935308582,1889173744,983368822,39843520,446408074,467964189,1663428696,62007254,1481139936,1922416357,918112312],[0,956061053,360112848,1035074790,1610007096,1698268692,93985685,1442713600,622937787,39843520,1512116835,701513153,1597452496,2088599457,1350101418,931504963],[0,774245966,1379974664,604491366,1621008618,166130994,1057741505,706931411,1442713600,983368822,1930489690,1352986051,788585369,1543529703,456620646,1131708271],[0,1049574470,1831059707,1284527617,1297275196,751089896,981821717,1057741505,93985685,1889173744,2097677068,1744692061,962367231,9722937,446815816,466620818],[0,639454493,1368214806,1169707859,1849562776,1603581590,751089896,166130994,1698268692,935308582,1521605122,2053218304,563855872,558910395,1368157387,1163325691],[0,1332049785,2018469344,1406223611,1533175366,1849562776,1297275196,1621008618,1610007096,2105212903,399071438,530151948,1032465671,846585925,2020988961,1019168605],[0,67923442,549746182,1490248217,1406223611,1169707859,1284527617,604491366,1035074790,695994228,304640754,1333501812,1412834556,138039228,97037986,507789489],[0,1092915095,1728246700,549746182,2018469344,1368214806,1831059707,1379974664,360112848,1962894682,1559543298,2102000169,1501074676,2031016045,217296974,1962798433],[0,1102759352,1092915095,67923442,1332049785,639454493,1049574470,774245966,956061053,604440680,807210699,1538375796,110418656,682820181,1406869940,1176991763]],"sparse_first_row":[[1,1044617752,1481433387,1878444588,2104235304,3722907,640029121,1328464283,527881075,2001559077,689032166,1880575107,358872577,108364736,1332772919,2129539744],[1,914163922,1285456931,2020639520,1453855633,1477444027,1339193063,328589713,208931151,1850882938,1462363792,869657005,805767435,796387373,400960806,755656571],[1,1787518610,1878065122,1211179805,1623392502,596876727,487353310,948630619,46137575,2011272885,785962300,141492211,527311230,1677138244,308914786,646273371],[1,475985225,1276143107,1696071196,32589944,1946884834,790695552,1297018938,1247695977,1441442697,348598749,1532810014,420302089,1768470437,2025744598,502837865],[1,189252144,933340760,1196177692,1207326122,1310289919,1018859884,1931151148,306468194,2080590861,1011822907,870184312,1169054295,299157995,550624518,761217428],[1,530026590,1465459236,1537941855,311984892,1766685979,254904495,1314612604,654252150,1786383982,886092250,951000927,1492154688,2015327114,1795152847,886334479],[1,1412702485,1137887454,520203158,1812492367,789833688,1233938788,1819934176,1614801759,746470909,1336417528,680335909,313757646,841134444,1641869241,998121965],[1,1839450507,1514473471,1397874495,427026633,825206836,1881998988,798695984,1518245734,137171104,1985410295,1204805986,708385656,135671564,181727188,1904989502],[1,1624250506,1276553090,1125040530,732235550,770503435,1098559359,1897139531,1957393256,2066469648,356890796,430889358,1662727935,736846479,86159423,2117864164],[1,1716262826,1219251453,406649991,956336998,891847704,1340399588,2120454448,1963372981,1580211744,1080244840,443212987,97408192,1276609344,864015922,791499252],[1,1506258844,428092642,616592092,491052976,1058721642,1154122014,1063147280,1468054399,738561812,458551485,1134033275,798609536,1652845715,1626650240,1834902174],[1,607388252,1341920224,661794417,844415991,1742333960,710739800,111776808,1680513373,1739278776,1261371867,485363479,9207629,1858910799,2063484755,1896740071],[1,837837165,218556391,599284291,302320589,605756188,1423640541,982100365,1395306646,2054696424,1124172688,311709517,1483301282,2057326379,468011828,195504483],[1,1289414867,961998037,641842254,1998469649,519613099,1247429126,607576114,76055325,48127247,1837975498,79401479,1108712765,543571094,1463931705,1750978183],[1,32307344,407129084,1819638694,777354771,1232160074,2126730873,1007018661,1966216114,644324697,1374455617,1280692573,1485221466,2092259084,2005955566,843003152],[1,1799257672,1148378128,1707844443,119809600,1464022250,1463207203,1189831139,80446531,29416071,995912922,1867752521,1481174533,1072217556,801048591,1832269882],[1,1030517273,477905567,1805133547,728218728,691695658,1764920569,1697028861,581984142,1322354059,843428748,903794926,1401335111,1906908186,1236851451,1854676428],[1,375781491,663573853,204251191,1828817902,317340619,1771861371,1841750301,1008632801,1793041736,369201095,488809041,46558970,875712544,1922589546,1760372266],[1,483733172,606096455,106009622,441040436,1929468092,1672504038,1906451897,986604790,1050370358,429434801,335400069,1143011095,1303702871,733751510,1165784837],[1,1108808405,2090426960,155082568,702347681,919398936,1226339182,1901110596,1230360372,1088093666,1713572740,675635302,759294455,895266739,255605669,1282509143]],"sparse_v":[[815798082,1599417173,2019487682,1495563308,1429225500,462208417,1706939096,1929713759,2037985010,1993489272,146269421,1370491063,1457031915,1571606227,442112630,0],[1672393568,1841009674,1550920329,1779211568,1449479676,1961293578,1174765549,738863811,358643257,820352444,1150799707,619173188,922229211,1138887134,409392716,0],[115734840,580126996,1525976646,1239851818,2073245456,1030589628,1377558295,494197709,238790464,719384642,134484029,231324069,639578566,636120851,568223911,0],[1300554587,1450500786,38201558,1838005083,1019142646,576859025,592297447,460824075,1486889364,199901131,793972955,1649041905,1287870494,344387188,1436973230,0],[1379067459,1918333570,591694540,245256148,1209106504,89299776,769713898,422208520,319592660,1799482307,665955244,433129386,733120015,95130246,1380689525,0],[2043404085,177610011,776806663,1124577123,324662120,532004834,57207205,807037515,1322129569,535602285,1823856965,687338970,1150883563,1938629528,1135982477,0],[125487871,771378267,895416365,1835469214,1441346099,1574070991,1051852536,1802482042,424344011,439065759,635684069,172075871,2054384866,792486292,1785646874,0],[1951012288,581143389,1449982847,2034951834,1296305314,1253043123,437690613,1533604834,1062523959,460834088,2028092965,112092247,480561016,29047112,918330564,0],[1321780723,906545729,598089205,1961814902,59242599,1763880479,1227717469,421528848,858340345,1469534917,1284739390,1593161876,70443154,1376173926,950943549,0],[111983004,815998134,480506885,2051984157,1295771849,472501509,1228066101,982351516,1168152195,2065242773,1539603922,494115877,630184518,1875163552,1430833335,0],[223627613,90799236,405797050,756408252,269447004,368791260,977004868,2000904592,658368457,581053670,1971660486,1301775976,1711019833,8812901,370847939,0],[1677802579,1959347885,1379609505,1200496457,441395130,1651239120,388457220,600596382,1851813934,1099854908,1253845511,1066124698,1415589924,943395525,1201570139,0],[1742026459,398996820,1559417452,1869434180,1650939527,1678406146,697527412,1329042656,1590738739,840532121,1919639745,1493325582,385219255,1321483334,74724398,0],[370629703,968406604,283063044,1421912803,1218525950,235983381,2097999101,1417290051,89846708,1755258584,1614636443,1923339542,1443080738,1287589955,1628527076,0],[1404617516,1387461349,960446077,316686774,1493143485,1135010996,1364787501,1366151319,1429025689,560429732,1992657421,86028332,16393928,1587924775,1099758468,0],[710831155,1269946944,1906631355,1017976477,466873715,61539759,17059176,1278714883,2061644815,240339454,235970441,1012156003,1873469407,1611775578,1163822633,0],[1188745580,1055003602,785416201,868051025,1135832507,1004853599,904741729,809824679,980810992,1178194302,1159788697,949043013,1001466621,1011628637,924759953,0],[2475856,3337618,4161263,3129126,2071505,3373463,2975691,1742470,2828204,3695590,1809935,2316312,3448583,2986173,2518923,0],[3415,9781,5292,4288,7724,13016,3835,5807,4933,8577,13125,16823,3127,8363,15859,0],[3,13,22,67,2,15,63,101,1,2,17,11,1,51,1,0]],"sparse_first_round_constants":[1423960925,886776133,1838900201,1725134361,1970838154,1349502123,1632425298,1452136978,1500653880,1694910225,1895400154,783177966,1170207886,1249553016,1486169768,387169126],"sparse_scalar_round_constants":[1358473177,1095637505,293175207,73153213,86260038,722710190,2089335770,1280052251,576313228,265102820,1685441472,670793739,1640841922,1549535807,1957713140,1556154273,1103412295,2118144716,20933114],"mds_dense":[[1,1,51,1,11,17,2,1,101,63,15,2,67,22,13,3],[3,1,1,51,1,11,17,2,1,101,63,15,2,67,22,13],[13,3,1,1,51,1,11,17,2,1,101,63,15,2,67,22],[22,13,3,1,1,51,1,11,17,2,1,101,63,15,2,67],[67,22,13,3,1,1,51,1,11,17,2,1,101,63,15,2],[2,67,22,13,3,1,1,51,1,11,17,2,1,101,63,15],[15,2,67,22,13,3,1,1,51,1,11,17,2,1,101,63],[63,15,2,67,22,13,3,1,1,51,1,11,17,2,1,101],[101,63,15,2,67,22,13,3,1,1,51,1,11,17,2,1],[1,101,63,15,2,67,22,13,3,1,1,51,1,11,17,2],[2,1,101,63,15,2,67,22,13,3,1,1,51,1,11,17],[17,2,1,101,63,15,2,67,22,13,3,1,1,51,1,11],[11,17,2,1,101,63,15,2,67,22,13,3,1,1,51,1],[1,11,17,2,1,101,63,15,2,67,22,13,3,1,1,51],[51,1,11,17,2,1,101,63,15,2,67,22,13,3,1,1],[1,51,1,11,17,2,1,101,63,15,2,67,22,13,3,1]]} \ No newline at end of file diff --git a/crates/lean_prover/tests/check_poseidon1_constants.rs b/crates/lean_prover/tests/check_poseidon1_constants.rs new file mode 100644 index 000000000..8077f9028 --- /dev/null +++ b/crates/lean_prover/tests/check_poseidon1_constants.rs @@ -0,0 +1,129 @@ +//! Ensure the Poseidon1 (width-16) constants hardcoded in +//! `crates/lean_prover/verifier.py` match the Rust constants used by the AIR. +//! The test prints the expected line (so you can paste it back if anything +//! drifts) and asserts that `verifier.py` contains that exact string up to +//! whitespace. +//! +//! Run: +//! cargo test -p lean_prover --test check_poseidon1_constants -- --nocapture + +use std::fmt::Write as _; +use std::fs; +use std::path::PathBuf; + +use backend::{ + KoalaBear, POSEIDON1_HALF_FULL_ROUNDS, POSEIDON1_PARTIAL_ROUNDS, PrimeCharacteristicRing, PrimeField32, + mds_circ_16, poseidon1_final_constants, poseidon1_initial_constants, poseidon1_sparse_first_round_constants, + poseidon1_sparse_first_row, poseidon1_sparse_m_i, poseidon1_sparse_scalar_round_constants, poseidon1_sparse_v, +}; + +fn k(x: KoalaBear) -> u32 { + x.as_canonical_u32() +} + +/// Reconstruct the dense MDS matrix the way `mds_dense_16` does in +/// `lean_vm::tables::poseidon_16::mod.rs` — run `mds_circ_16` on each standard +/// basis vector and stack the columns into a row-major matrix. +fn dense_mds_matrix() -> [[KoalaBear; 16]; 16] { + let mut cols = [[KoalaBear::ZERO; 16]; 16]; + for j in 0..16 { + let mut e = [KoalaBear::ZERO; 16]; + e[j] = KoalaBear::ONE; + mds_circ_16::(&mut e); + cols[j] = e; + } + let mut rows = [[KoalaBear::ZERO; 16]; 16]; + for i in 0..16 { + for j in 0..16 { + rows[i][j] = cols[j][i]; + } + } + rows +} + +fn fmt_vec(v: &[KoalaBear]) -> String { + let mut s = String::from("("); + for (i, &x) in v.iter().enumerate() { + if i > 0 { + s.push(','); + } + write!(s, "{}", k(x)).unwrap(); + } + s.push(')'); + s +} + +fn fmt_mat>(rows: &[R]) -> String { + let mut s = String::from("("); + for (i, row) in rows.iter().enumerate() { + if i > 0 { + s.push(','); + } + s.push_str(&fmt_vec(row.as_ref())); + } + s.push(')'); + s +} + +fn expected_poseidon1_constants_line() -> String { + let initial = poseidon1_initial_constants(); + let final_ = poseidon1_final_constants(); + let m_i = poseidon1_sparse_m_i(); + let first_row = poseidon1_sparse_first_row(); + let sparse_v = poseidon1_sparse_v(); + let first_rc = poseidon1_sparse_first_round_constants(); + let scalar_rc = poseidon1_sparse_scalar_round_constants(); + let mds = dense_mds_matrix(); + + let initial: Vec> = initial.iter().map(|r| r.to_vec()).collect(); + let final_: Vec> = final_.iter().map(|r| r.to_vec()).collect(); + let m_i: Vec> = m_i.iter().map(|r| r.to_vec()).collect(); + let first_row: Vec> = first_row.iter().map(|r| r.to_vec()).collect(); + let sparse_v: Vec> = sparse_v.iter().map(|r| r.to_vec()).collect(); + let mds: Vec> = mds.iter().map(|r| r.to_vec()).collect(); + + format!( + "POSEIDON1_CONSTANTS = {{\ +'half_full_rounds':{hf},\ +'partial_rounds':{pr},\ +'initial_constants':{ic},\ +'final_constants':{fc},\ +'sparse_m_i':{smi},\ +'sparse_first_row':{sfr},\ +'sparse_v':{sv},\ +'sparse_first_round_constants':{sfrc},\ +'sparse_scalar_round_constants':{ssrc},\ +'mds_dense':{mds}\ +}}", + hf = POSEIDON1_HALF_FULL_ROUNDS, + pr = POSEIDON1_PARTIAL_ROUNDS, + ic = fmt_mat(&initial), + fc = fmt_mat(&final_), + smi = fmt_mat(&m_i), + sfr = fmt_mat(&first_row), + sv = fmt_mat(&sparse_v), + sfrc = fmt_vec(first_rc), + ssrc = fmt_vec(scalar_rc), + mds = fmt_mat(&mds), + ) +} + +fn strip_ws(s: &str) -> String { + s.chars().filter(|c| !c.is_whitespace()).collect() +} + +#[test] +fn check_poseidon1_constants() { + let expected = expected_poseidon1_constants_line(); + println!("{expected}"); + + let verifier_py = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("verifier.py"); + let src = + fs::read_to_string(&verifier_py).unwrap_or_else(|e| panic!("failed to read {}: {e}", verifier_py.display())); + + assert!( + strip_ws(&src).contains(&strip_ws(&expected)), + "POSEIDON1_CONSTANTS in {} is out of sync with Rust. Replace the line with the one printed above.", + verifier_py.display(), + ); +} diff --git a/crates/lean_prover/tests/check_whir_configs.rs b/crates/lean_prover/tests/check_whir_configs.rs new file mode 100644 index 000000000..7009cc904 --- /dev/null +++ b/crates/lean_prover/tests/check_whir_configs.rs @@ -0,0 +1,79 @@ +//! Ensure the WHIR parameter table hardcoded in `crates/lean_prover/verifier.py` +//! matches what the Rust prover would compute from `default_whir_config`. The +//! test prints the expected line (so you can paste it back if it drifts) and +//! asserts that `verifier.py` contains that exact string. +//! +//! Run: +//! cargo test -p lean_prover --test check_whir_configs -- --nocapture + +use std::fmt::Write as _; +use std::fs; +use std::path::PathBuf; + +use backend::{TwoAdicField, WhirConfig}; +use lean_prover::default_whir_config; +use lean_vm::{EF, F, MAX_WHIR_LOG_INV_RATE, MIN_WHIR_LOG_INV_RATE}; + +fn expected_whir_configs_line() -> String { + let mut entries: Vec = Vec::new(); + + for log_inv_rate in MIN_WHIR_LOG_INV_RATE..=MAX_WHIR_LOG_INV_RATE { + let builder = default_whir_config(log_inv_rate); + let first_ff = builder.folding_factor.at_round(0); + let max_nv = F::TWO_ADICITY + first_ff - log_inv_rate; + + for num_variables in first_ff..=max_nv { + let cfg: WhirConfig = WhirConfig::new(&builder, num_variables); + + let mut rounds = String::from("("); + for (i, r) in cfg.round_parameters.iter().enumerate() { + if i > 0 { + rounds.push(','); + } + write!( + rounds, + "({},{},{},{})", + r.num_queries, r.ood_samples, r.query_pow_bits, r.folding_pow_bits + ) + .unwrap(); + } + if cfg.round_parameters.len() == 1 { + rounds.push(','); + } + rounds.push(')'); + + entries.push(format!( + "({},{},{},{},{},{},{})", + log_inv_rate, + num_variables, + cfg.commitment_ood_samples, + cfg.starting_folding_pow_bits, + cfg.final_queries, + cfg.final_query_pow_bits, + rounds, + )); + } + } + + format!("WHIR_CONFIGS = ({})", entries.join(",")) +} + +fn strip_ws(s: &str) -> String { + s.chars().filter(|c| !c.is_whitespace()).collect() +} + +#[test] +fn check_whir_configs() { + let expected = expected_whir_configs_line(); + println!("{expected}"); + + let verifier_py = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("verifier.py"); + let src = + fs::read_to_string(&verifier_py).unwrap_or_else(|e| panic!("failed to read {}: {e}", verifier_py.display())); + + assert!( + strip_ws(&src).contains(&strip_ws(&expected)), + "WHIR_CONFIGS in {} is out of sync with Rust `default_whir_config`. Replace the line with the one printed above.", + verifier_py.display(), + ); +} diff --git a/crates/lean_prover/tests/dump_poseidon1_constants.rs b/crates/lean_prover/tests/dump_poseidon1_constants.rs deleted file mode 100644 index c85d19ff1..000000000 --- a/crates/lean_prover/tests/dump_poseidon1_constants.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Dumps all Poseidon1 round constants + matrices used by the leanVM AIR -//! into `crates/lean_prover/poseidon1_constants.json`, so the Python verifier -//! doesn't need to embed thousands of field literals. -//! -//! Run: -//! cargo test --release -p lean_prover --test dump_poseidon1_constants -- --nocapture - -use std::fs; -use std::path::PathBuf; - -use backend::{ - KoalaBear, POSEIDON1_HALF_FULL_ROUNDS, POSEIDON1_PARTIAL_ROUNDS, PrimeField32, poseidon1_final_constants, - poseidon1_initial_constants, poseidon1_sparse_first_round_constants, poseidon1_sparse_first_row, - poseidon1_sparse_m_i, poseidon1_sparse_scalar_round_constants, poseidon1_sparse_v, -}; -use serde::Serialize; - -fn k(x: KoalaBear) -> u32 { - x.as_canonical_u32() -} - -#[derive(Serialize)] -struct Out { - half_full_rounds: usize, - partial_rounds: usize, - initial_constants: Vec>, - final_constants: Vec>, - sparse_m_i: Vec>, - sparse_first_row: Vec>, - sparse_v: Vec>, - sparse_first_round_constants: Vec, - sparse_scalar_round_constants: Vec, - mds_dense: Vec>, -} - -/// Reconstruct the dense MDS matrix the way `mds_dense_16` does in -/// `lean_vm::tables::poseidon_16::mod.rs` — by running `mds_circ_16` on each -/// standard basis vector and stacking the columns into a row-major matrix. -/// We avoid touching the private `mds_circ_16` directly by recreating its -/// effect via `mds_fft_16` (which is the same map for KoalaBear). -fn dense_mds_matrix() -> [[KoalaBear; 16]; 16] { - use backend::{PrimeCharacteristicRing, mds_circ_16}; - - let mut cols = [[KoalaBear::ZERO; 16]; 16]; - for j in 0..16 { - let mut e = [KoalaBear::ZERO; 16]; - e[j] = KoalaBear::ONE; - mds_circ_16::(&mut e); - cols[j] = e; - } - let mut rows = [[KoalaBear::ZERO; 16]; 16]; - for i in 0..16 { - for j in 0..16 { - rows[i][j] = cols[j][i]; - } - } - rows -} - -#[test] -fn dump_poseidon1_constants() { - let initial = poseidon1_initial_constants(); - let final_ = poseidon1_final_constants(); - let m_i = poseidon1_sparse_m_i(); - let first_row = poseidon1_sparse_first_row(); - let sparse_v = poseidon1_sparse_v(); - let first_rc = poseidon1_sparse_first_round_constants(); - let scalar_rc = poseidon1_sparse_scalar_round_constants(); - let mds = dense_mds_matrix(); - - let out = Out { - half_full_rounds: POSEIDON1_HALF_FULL_ROUNDS, - partial_rounds: POSEIDON1_PARTIAL_ROUNDS, - initial_constants: initial.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), - final_constants: final_.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), - sparse_m_i: m_i.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), - sparse_first_row: first_row - .iter() - .map(|row| row.iter().map(|&x| k(x)).collect()) - .collect(), - sparse_v: sparse_v.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), - sparse_first_round_constants: first_rc.iter().map(|&x| k(x)).collect(), - sparse_scalar_round_constants: scalar_rc.iter().map(|&x| k(x)).collect(), - mds_dense: mds.iter().map(|row| row.iter().map(|&x| k(x)).collect()).collect(), - }; - - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("poseidon1_constants.json"); - fs::write(&path, serde_json::to_string(&out).unwrap()).unwrap(); - println!( - "wrote poseidon1 constants ({:.1} KiB) to {}", - path.metadata().unwrap().len() as f64 / 1024.0, - path.display() - ); -} diff --git a/crates/lean_prover/tests/dump_whir_configs.rs b/crates/lean_prover/tests/dump_whir_configs.rs deleted file mode 100644 index 2e2e1a65f..000000000 --- a/crates/lean_prover/tests/dump_whir_configs.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Dump the float-derived parts of every reachable `WhirConfig` to -//! `crates/lean_prover/whir_configs.json` so the Python verifier doesn't have -//! to redo the soundness-formula computation in pure Python. -//! -//! Only the values that come from f64 math are dumped (query counts, OOD -//! samples, grinding bits). Everything else (per-round `num_variables`, -//! `domain_size`, `folding_factor`, `log_inv_rate`, `folded_domain_gen`, -//! `n_rounds`, `final_sumcheck_rounds`, `final_log_inv_rate`) is integer -//! arithmetic and is derived on the Python side. -//! -//! Run: -//! cargo test -p lean_prover --test dump_whir_configs -- --nocapture - -use std::fs; -use std::path::PathBuf; - -use backend::{TwoAdicField, WhirConfig}; -use lean_prover::default_whir_config; -use lean_vm::{EF, F, MAX_WHIR_LOG_INV_RATE, MIN_WHIR_LOG_INV_RATE}; -use serde::Serialize; - -#[derive(Serialize)] -struct Round { - num_queries: usize, - ood_samples: usize, - query_pow_bits: usize, - folding_pow_bits: usize, -} - -#[derive(Serialize)] -struct Config { - log_inv_rate: usize, - num_variables: usize, - commitment_ood_samples: usize, - starting_folding_pow_bits: usize, - final_queries: usize, - final_query_pow_bits: usize, - rounds: Vec, -} - -#[test] -fn dump_whir_configs() { - let mut configs = Vec::new(); - - for log_inv_rate in MIN_WHIR_LOG_INV_RATE..=MAX_WHIR_LOG_INV_RATE { - let builder = default_whir_config(log_inv_rate); - let first_ff = builder.folding_factor.at_round(0); - - let min_nv = first_ff; - let max_nv = F::TWO_ADICITY + first_ff - log_inv_rate; - - for num_variables in min_nv..=max_nv { - let cfg: WhirConfig = WhirConfig::new(&builder, num_variables); - - let rounds = cfg - .round_parameters - .iter() - .map(|r| Round { - num_queries: r.num_queries, - ood_samples: r.ood_samples, - query_pow_bits: r.query_pow_bits, - folding_pow_bits: r.folding_pow_bits, - }) - .collect(); - - configs.push(Config { - log_inv_rate, - num_variables, - commitment_ood_samples: cfg.commitment_ood_samples, - starting_folding_pow_bits: cfg.starting_folding_pow_bits, - final_queries: cfg.final_queries, - final_query_pow_bits: cfg.final_query_pow_bits, - rounds, - }); - } - } - - let json = serde_json::to_string_pretty(&configs).unwrap(); - let path = std::env::var("WHIR_DUMP_PATH") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("whir_configs.json")); - fs::write(&path, json).unwrap_or_else(|e| panic!("failed to write {}: {e}", path.display())); - println!("wrote {} WhirConfig entries to {}", configs.len(), path.display()); -} diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 18a56b68f..7a7b3da2f 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -8,9 +8,7 @@ uv venv .venv --python 3.12 source .venv/bin/activate VIRTUAL_ENV=.venv uv pip install "git+https://github.com/leanEthereum/leanSpec.git" - cargo test --release -p lean_prover --test dump_whir_configs -- --nocapture - cargo test --release -p lean_prover --test dump_poseidon1_constants -- --nocapture - cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture + cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture Run: .venv/bin/python crates/lean_prover/verifier.py @@ -48,7 +46,22 @@ MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, BASE_TWO_ADICITY = 8, 8, 24 MAX_BYTECODE_LOG_SIZE = 22 MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension_op": 21, "poseidon16_compress": 21} -WHIR_CONFIGS_PATH = "whir_configs.json" + +# WHIR per-(log_inv_rate, num_variables) parameters. Tuple-of-tuples to keep the table on one line: +# (log_inv_rate, num_variables, commitment_ood_samples, starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds) +# where `rounds = ((num_queries, ood_samples, query_pow_bits, folding_pow_bits), ...)`. Synced with the +# Rust `WhirConfig` defaults — `cargo test -p lean_prover --test check_whir_configs` enforces a match. +# fmt: off +WHIR_CONFIGS = ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # noqa: E501 + +# KoalaBear two-adic generators: index `b` is a primitive 2^b-th root of unity. +KB_TWO_ADIC_GENERATORS: list[int] = [0x1, 0x7F000000, 0x7E010002, 0x6832FE4A, 0x08DBD69C, 0x0A28F031, 0x5C4A5B99, 0x29B75A80, 0x17668B8A, 0x27AD539B, 0x334D48C7, 0x7744959C, 0x768FC6FA, 0x303964B2, 0x3E687D4D, 0x45A60E61, 0x6E2F4D7A, 0x163BD499, 0x6C4A8A45, 0x143EF899, 0x514DDCAD, 0x484EF19B, 0x205D63C3, 0x68E7DD49, 0x6AC49F88] # noqa: E501 + +# Poseidon2-KoalaBear (width 16) constants used by the AIR. Tuples-of-tuples to stay on one line; +# raw u32s in canonical form. Synced with the Rust constants — `cargo test -p lean_prover --test +# check_poseidon1_constants` enforces a match. +POSEIDON1_CONSTANTS = {'half_full_rounds':4,'partial_rounds':20,'initial_constants':((2128964168,288780357,316938561,2126233899,426817493,1714118888,1045008582,1738510837,889721787,8866516,681576474,419059826,1596305521,1583176088,1584387047,1529751136),(1863858111,1072044075,517831365,1464274176,1138001621,428001039,245709561,1641420379,1365482496,770454828,693167409,757905735,136670447,436275702,525466355,1559174242),(1030087950,869864998,322787870,267688717,948964561,740478015,679816114,113662466,2066544572,1744924186,367094720,1380455578,1842483872,416711434,1342291586,1692058446),(1493348999,1113949088,210900530,1071655077,610242121,1136339326,2020858841,1019840479,678147278,1678413261,1361743414,61132629,1209546658,64412292,1936878279,1980661727)),'final_constants':((1983525157,1330885184,414710339,733907571,479859442,1064293389,236801732,325174861,162067568,64109120,278581904,683867016,996448498,1960361559,1782740946,415413204),(1649591052,130819424,547348827,1386569644,1307680439,38932758,1581338609,1020895732,5942549,665140992,1924917707,1910029693,1100265370,1223195250,859919676,1674792874),(321520099,942924505,1232236036,88692728,2071051492,1945027965,1433294131,531185630,879398056,291692510,1546702888,155861652,810736858,932742296,1374710679,1703184249),(1973006548,1131403964,1724233597,1086876318,669451611,1829624280,2119538869,441255155,1580936135,1396398895,1043570981,1716351438,942566442,616885102,334644983,132306927)),'sparse_m_i':((1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),(0,1176991763,1962798433,507789489,1019168605,1163325691,466620818,1131708271,931504963,918112312,86863075,882630651,84434949,754655560,375632733,210588963),(0,1406869940,217296974,97037986,2020988961,1368157387,446815816,456620646,1350101418,1922416357,1227469637,603478726,1537295456,873165878,155811605,375632733),(0,682820181,2031016045,138039228,846585925,558910395,9722937,1543529703,2088599457,1481139936,255018864,2130530098,43680256,864171667,873165878,754655560),(0,110418656,1501074676,1412834556,1032465671,563855872,962367231,788585369,1597452496,62007254,389404591,904725063,1698425244,43680256,1537295456,84434949),(0,1538375796,2102000169,1333501812,530151948,2053218304,1744692061,1352986051,701513153,1663428696,1849567553,1774105504,904725063,2130530098,603478726,882630651),(0,807210699,1559543298,304640754,399071438,1521605122,2097677068,1930489690,1512116835,467964189,1473717591,1849567553,389404591,255018864,1227469637,86863075),(0,604440680,1962894682,695994228,2105212903,935308582,1889173744,983368822,39843520,446408074,467964189,1663428696,62007254,1481139936,1922416357,918112312),(0,956061053,360112848,1035074790,1610007096,1698268692,93985685,1442713600,622937787,39843520,1512116835,701513153,1597452496,2088599457,1350101418,931504963),(0,774245966,1379974664,604491366,1621008618,166130994,1057741505,706931411,1442713600,983368822,1930489690,1352986051,788585369,1543529703,456620646,1131708271),(0,1049574470,1831059707,1284527617,1297275196,751089896,981821717,1057741505,93985685,1889173744,2097677068,1744692061,962367231,9722937,446815816,466620818),(0,639454493,1368214806,1169707859,1849562776,1603581590,751089896,166130994,1698268692,935308582,1521605122,2053218304,563855872,558910395,1368157387,1163325691),(0,1332049785,2018469344,1406223611,1533175366,1849562776,1297275196,1621008618,1610007096,2105212903,399071438,530151948,1032465671,846585925,2020988961,1019168605),(0,67923442,549746182,1490248217,1406223611,1169707859,1284527617,604491366,1035074790,695994228,304640754,1333501812,1412834556,138039228,97037986,507789489),(0,1092915095,1728246700,549746182,2018469344,1368214806,1831059707,1379974664,360112848,1962894682,1559543298,2102000169,1501074676,2031016045,217296974,1962798433),(0,1102759352,1092915095,67923442,1332049785,639454493,1049574470,774245966,956061053,604440680,807210699,1538375796,110418656,682820181,1406869940,1176991763)),'sparse_first_row':((1,1044617752,1481433387,1878444588,2104235304,3722907,640029121,1328464283,527881075,2001559077,689032166,1880575107,358872577,108364736,1332772919,2129539744),(1,914163922,1285456931,2020639520,1453855633,1477444027,1339193063,328589713,208931151,1850882938,1462363792,869657005,805767435,796387373,400960806,755656571),(1,1787518610,1878065122,1211179805,1623392502,596876727,487353310,948630619,46137575,2011272885,785962300,141492211,527311230,1677138244,308914786,646273371),(1,475985225,1276143107,1696071196,32589944,1946884834,790695552,1297018938,1247695977,1441442697,348598749,1532810014,420302089,1768470437,2025744598,502837865),(1,189252144,933340760,1196177692,1207326122,1310289919,1018859884,1931151148,306468194,2080590861,1011822907,870184312,1169054295,299157995,550624518,761217428),(1,530026590,1465459236,1537941855,311984892,1766685979,254904495,1314612604,654252150,1786383982,886092250,951000927,1492154688,2015327114,1795152847,886334479),(1,1412702485,1137887454,520203158,1812492367,789833688,1233938788,1819934176,1614801759,746470909,1336417528,680335909,313757646,841134444,1641869241,998121965),(1,1839450507,1514473471,1397874495,427026633,825206836,1881998988,798695984,1518245734,137171104,1985410295,1204805986,708385656,135671564,181727188,1904989502),(1,1624250506,1276553090,1125040530,732235550,770503435,1098559359,1897139531,1957393256,2066469648,356890796,430889358,1662727935,736846479,86159423,2117864164),(1,1716262826,1219251453,406649991,956336998,891847704,1340399588,2120454448,1963372981,1580211744,1080244840,443212987,97408192,1276609344,864015922,791499252),(1,1506258844,428092642,616592092,491052976,1058721642,1154122014,1063147280,1468054399,738561812,458551485,1134033275,798609536,1652845715,1626650240,1834902174),(1,607388252,1341920224,661794417,844415991,1742333960,710739800,111776808,1680513373,1739278776,1261371867,485363479,9207629,1858910799,2063484755,1896740071),(1,837837165,218556391,599284291,302320589,605756188,1423640541,982100365,1395306646,2054696424,1124172688,311709517,1483301282,2057326379,468011828,195504483),(1,1289414867,961998037,641842254,1998469649,519613099,1247429126,607576114,76055325,48127247,1837975498,79401479,1108712765,543571094,1463931705,1750978183),(1,32307344,407129084,1819638694,777354771,1232160074,2126730873,1007018661,1966216114,644324697,1374455617,1280692573,1485221466,2092259084,2005955566,843003152),(1,1799257672,1148378128,1707844443,119809600,1464022250,1463207203,1189831139,80446531,29416071,995912922,1867752521,1481174533,1072217556,801048591,1832269882),(1,1030517273,477905567,1805133547,728218728,691695658,1764920569,1697028861,581984142,1322354059,843428748,903794926,1401335111,1906908186,1236851451,1854676428),(1,375781491,663573853,204251191,1828817902,317340619,1771861371,1841750301,1008632801,1793041736,369201095,488809041,46558970,875712544,1922589546,1760372266),(1,483733172,606096455,106009622,441040436,1929468092,1672504038,1906451897,986604790,1050370358,429434801,335400069,1143011095,1303702871,733751510,1165784837),(1,1108808405,2090426960,155082568,702347681,919398936,1226339182,1901110596,1230360372,1088093666,1713572740,675635302,759294455,895266739,255605669,1282509143)),'sparse_v':((815798082,1599417173,2019487682,1495563308,1429225500,462208417,1706939096,1929713759,2037985010,1993489272,146269421,1370491063,1457031915,1571606227,442112630,0),(1672393568,1841009674,1550920329,1779211568,1449479676,1961293578,1174765549,738863811,358643257,820352444,1150799707,619173188,922229211,1138887134,409392716,0),(115734840,580126996,1525976646,1239851818,2073245456,1030589628,1377558295,494197709,238790464,719384642,134484029,231324069,639578566,636120851,568223911,0),(1300554587,1450500786,38201558,1838005083,1019142646,576859025,592297447,460824075,1486889364,199901131,793972955,1649041905,1287870494,344387188,1436973230,0),(1379067459,1918333570,591694540,245256148,1209106504,89299776,769713898,422208520,319592660,1799482307,665955244,433129386,733120015,95130246,1380689525,0),(2043404085,177610011,776806663,1124577123,324662120,532004834,57207205,807037515,1322129569,535602285,1823856965,687338970,1150883563,1938629528,1135982477,0),(125487871,771378267,895416365,1835469214,1441346099,1574070991,1051852536,1802482042,424344011,439065759,635684069,172075871,2054384866,792486292,1785646874,0),(1951012288,581143389,1449982847,2034951834,1296305314,1253043123,437690613,1533604834,1062523959,460834088,2028092965,112092247,480561016,29047112,918330564,0),(1321780723,906545729,598089205,1961814902,59242599,1763880479,1227717469,421528848,858340345,1469534917,1284739390,1593161876,70443154,1376173926,950943549,0),(111983004,815998134,480506885,2051984157,1295771849,472501509,1228066101,982351516,1168152195,2065242773,1539603922,494115877,630184518,1875163552,1430833335,0),(223627613,90799236,405797050,756408252,269447004,368791260,977004868,2000904592,658368457,581053670,1971660486,1301775976,1711019833,8812901,370847939,0),(1677802579,1959347885,1379609505,1200496457,441395130,1651239120,388457220,600596382,1851813934,1099854908,1253845511,1066124698,1415589924,943395525,1201570139,0),(1742026459,398996820,1559417452,1869434180,1650939527,1678406146,697527412,1329042656,1590738739,840532121,1919639745,1493325582,385219255,1321483334,74724398,0),(370629703,968406604,283063044,1421912803,1218525950,235983381,2097999101,1417290051,89846708,1755258584,1614636443,1923339542,1443080738,1287589955,1628527076,0),(1404617516,1387461349,960446077,316686774,1493143485,1135010996,1364787501,1366151319,1429025689,560429732,1992657421,86028332,16393928,1587924775,1099758468,0),(710831155,1269946944,1906631355,1017976477,466873715,61539759,17059176,1278714883,2061644815,240339454,235970441,1012156003,1873469407,1611775578,1163822633,0),(1188745580,1055003602,785416201,868051025,1135832507,1004853599,904741729,809824679,980810992,1178194302,1159788697,949043013,1001466621,1011628637,924759953,0),(2475856,3337618,4161263,3129126,2071505,3373463,2975691,1742470,2828204,3695590,1809935,2316312,3448583,2986173,2518923,0),(3415,9781,5292,4288,7724,13016,3835,5807,4933,8577,13125,16823,3127,8363,15859,0),(3,13,22,67,2,15,63,101,1,2,17,11,1,51,1,0)),'sparse_first_round_constants':(1423960925,886776133,1838900201,1725134361,1970838154,1349502123,1632425298,1452136978,1500653880,1694910225,1895400154,783177966,1170207886,1249553016,1486169768,387169126),'sparse_scalar_round_constants':(1358473177,1095637505,293175207,73153213,86260038,722710190,2089335770,1280052251,576313228,265102820,1685441472,670793739,1640841922,1549535807,1957713140,1556154273,1103412295,2118144716,20933114),'mds_dense':((1,1,51,1,11,17,2,1,101,63,15,2,67,22,13,3),(3,1,1,51,1,11,17,2,1,101,63,15,2,67,22,13),(13,3,1,1,51,1,11,17,2,1,101,63,15,2,67,22),(22,13,3,1,1,51,1,11,17,2,1,101,63,15,2,67),(67,22,13,3,1,1,51,1,11,17,2,1,101,63,15,2),(2,67,22,13,3,1,1,51,1,11,17,2,1,101,63,15),(15,2,67,22,13,3,1,1,51,1,11,17,2,1,101,63),(63,15,2,67,22,13,3,1,1,51,1,11,17,2,1,101),(101,63,15,2,67,22,13,3,1,1,51,1,11,17,2,1),(1,101,63,15,2,67,22,13,3,1,1,51,1,11,17,2),(2,1,101,63,15,2,67,22,13,3,1,1,51,1,11,17),(17,2,1,101,63,15,2,67,22,13,3,1,1,51,1,11),(11,17,2,1,101,63,15,2,67,22,13,3,1,1,51,1),(1,11,17,2,1,101,63,15,2,67,22,13,3,1,1,51),(51,1,11,17,2,1,101,63,15,2,67,22,13,3,1,1),(1,51,1,11,17,2,1,101,63,15,2,67,22,13,3,1))} # noqa: E501 +# fmt: on class ProofError(Exception): @@ -453,32 +466,27 @@ def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: return num_variables + start_rate - (RS_DOMAIN_INITIAL_REDUCTION_FACTOR + r - 1 if r >= 1 else 0) -# fmt: off -KB_TWO_ADIC_GENERATORS: list[int] = [ - 0x1, 0x7F000000, 0x7E010002, 0x6832FE4A, 0x08DBD69C, 0x0A28F031, 0x5C4A5B99, - 0x29B75A80, 0x17668B8A, 0x27AD539B, 0x334D48C7, 0x7744959C, 0x768FC6FA, - 0x303964B2, 0x3E687D4D, 0x45A60E61, 0x6E2F4D7A, 0x163BD499, 0x6C4A8A45, - 0x143EF899, 0x514DDCAD, 0x484EF19B, 0x205D63C3, 0x68E7DD49, 0x6AC49F88, -] -# fmt: on - - def two_adic_generator(bits: int) -> Fp: return Fp(KB_TWO_ADIC_GENERATORS[bits]) @functools.cache def whir_config(log_inv_rate: int, num_variables: int) -> dict: - import json - from pathlib import Path - - for c in json.loads(Path(__file__).with_name(WHIR_CONFIGS_PATH).read_text()): - if (c["log_inv_rate"], c["num_variables"]) == (log_inv_rate, num_variables): - return c - raise KeyError( - f"No WHIR config for (log_inv_rate={log_inv_rate}, num_variables={num_variables}). " - "Regenerate with: cargo test -p lean_prover --test dump_whir_configs" - ) + for c in WHIR_CONFIGS: + if (c[0], c[1]) == (log_inv_rate, num_variables): + return { + "log_inv_rate": c[0], + "num_variables": c[1], + "commitment_ood_samples": c[2], + "starting_folding_pow_bits": c[3], + "final_queries": c[4], + "final_query_pow_bits": c[5], + "rounds": [ + {"num_queries": r[0], "ood_samples": r[1], "query_pow_bits": r[2], "folding_pow_bits": r[3]} + for r in c[6] + ], + } + raise KeyError(f"No WHIR config for (log_inv_rate={log_inv_rate}, num_variables={num_variables}).") @dataclass @@ -1137,10 +1145,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, _table: TableMeta, extra_da @functools.cache def _p1c() -> dict: - import json - from pathlib import Path - - raw = json.loads(Path(__file__).with_name("poseidon1_constants.json").read_text()) + raw = POSEIDON1_CONSTANTS fp_mat = lambda m: [[Fp(v) for v in row] for row in m] fp_vec = lambda v: [Fp(x) for x in v] return { diff --git a/crates/lean_prover/whir_configs.json b/crates/lean_prover/whir_configs.json deleted file mode 100644 index 306461ede..000000000 --- a/crates/lean_prover/whir_configs.json +++ /dev/null @@ -1,1478 +0,0 @@ -[ - { - "log_inv_rate": 1, - "num_variables": 7, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 10, - "final_queries": 220, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 8, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 11, - "final_queries": 220, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 9, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 12, - "final_queries": 220, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 10, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 13, - "final_queries": 220, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 11, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 14, - "final_queries": 220, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 12, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 15, - "final_queries": 220, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 13, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 220, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 14, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 15, - "final_queries": 221, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 15, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 221, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 1, - "num_variables": 16, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 222, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 11 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 17, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 223, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 18, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 224, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 13 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 19, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 225, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 14 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 20, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 227, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 15 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 21, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 32, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 229, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 73, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 9 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 22, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 32, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 230, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - }, - { - "num_queries": 74, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 10 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 23, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 32, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 234, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 13 - }, - { - "num_queries": 74, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 11 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 24, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 32, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 235, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 14 - }, - { - "num_queries": 74, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 25, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 32, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 241, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 74, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 13 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 26, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 21, - "final_query_pow_bits": 14, - "rounds": [ - { - "num_queries": 243, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 74, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - }, - { - "num_queries": 32, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 27, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 21, - "final_query_pow_bits": 14, - "rounds": [ - { - "num_queries": 248, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 75, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 32, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 28, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 21, - "final_query_pow_bits": 14, - "rounds": [ - { - "num_queries": 256, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 75, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 32, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 29, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 21, - "final_query_pow_bits": 14, - "rounds": [ - { - "num_queries": 262, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 76, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 12 - }, - { - "num_queries": 33, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 17 - } - ] - }, - { - "log_inv_rate": 1, - "num_variables": 30, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 21, - "final_query_pow_bits": 14, - "rounds": [ - { - "num_queries": 270, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 76, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 13 - }, - { - "num_queries": 33, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 18 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 7, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 13, - "final_queries": 109, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 8, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 14, - "final_queries": 109, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 9, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 15, - "final_queries": 109, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 10, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 109, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 11, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 12, - "final_queries": 110, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 12, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 13, - "final_queries": 110, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 13, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 14, - "final_queries": 110, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 14, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 15, - "final_queries": 110, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 15, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 110, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 2, - "num_variables": 16, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 14, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 111, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 10 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 17, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 15, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 111, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 11 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 18, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 111, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 19, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 15, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 112, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 13 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 20, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 112, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 14 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 21, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 28, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 113, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 55, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 10 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 22, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 28, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 114, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 55, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 11 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 23, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 28, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 114, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 13 - }, - { - "num_queries": 56, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 24, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 28, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 115, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 14 - }, - { - "num_queries": 56, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 13 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 25, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 28, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 118, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 56, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 26, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 19, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 118, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 56, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 28, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 17 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 27, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 19, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 119, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 13 - }, - { - "num_queries": 57, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 28, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 18 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 28, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 19, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 120, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - }, - { - "num_queries": 57, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - }, - { - "num_queries": 29, - "ood_samples": 2, - "query_pow_bits": 15, - "folding_pow_bits": 19 - } - ] - }, - { - "log_inv_rate": 2, - "num_variables": 29, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 19, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 123, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 57, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 29, - "ood_samples": 2, - "query_pow_bits": 15, - "folding_pow_bits": 20 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 7, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 9, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 8, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 10, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 9, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 11, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 10, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 12, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 11, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 13, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 12, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 14, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 13, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 15, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 14, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 73, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 15, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 12, - "final_queries": 74, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 3, - "num_variables": 16, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 13, - "final_queries": 44, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 74, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 11 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 17, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 14, - "final_queries": 44, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 74, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 18, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 44, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 74, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 13 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 19, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 44, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 74, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 14 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 20, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 44, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 75, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 15 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 21, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 25, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 75, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 44, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 11 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 22, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 25, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 76, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 11 - }, - { - "num_queries": 45, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 23, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 25, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 76, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - }, - { - "num_queries": 45, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 13 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 24, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 25, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 77, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 13 - }, - { - "num_queries": 45, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 25, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 25, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 78, - "ood_samples": 2, - "query_pow_bits": 15, - "folding_pow_bits": 14 - }, - { - "num_queries": 45, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 26, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 18, - "final_query_pow_bits": 12, - "rounds": [ - { - "num_queries": 79, - "ood_samples": 2, - "query_pow_bits": 15, - "folding_pow_bits": 15 - }, - { - "num_queries": 45, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 25, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 19 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 27, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 18, - "final_query_pow_bits": 12, - "rounds": [ - { - "num_queries": 80, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 45, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 26, - "ood_samples": 2, - "query_pow_bits": 13, - "folding_pow_bits": 20 - } - ] - }, - { - "log_inv_rate": 3, - "num_variables": 28, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 18, - "final_query_pow_bits": 12, - "rounds": [ - { - "num_queries": 82, - "ood_samples": 2, - "query_pow_bits": 15, - "folding_pow_bits": 15 - }, - { - "num_queries": 46, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 26, - "ood_samples": 2, - "query_pow_bits": 13, - "folding_pow_bits": 21 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 7, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 8, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 8, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 9, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 9, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 10, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 10, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 11, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 11, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 12, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 12, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 13, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 13, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 14, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 14, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 15, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 15, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 16, - "final_queries": 55, - "final_query_pow_bits": 16, - "rounds": [] - }, - { - "log_inv_rate": 4, - "num_variables": 16, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 13, - "final_queries": 37, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 56, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 9 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 17, - "commitment_ood_samples": 1, - "starting_folding_pow_bits": 14, - "final_queries": 37, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 56, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 10 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 18, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 37, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 56, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 11 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 19, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 37, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 56, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 12 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 20, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 13, - "final_queries": 37, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 57, - "ood_samples": 1, - "query_pow_bits": 16, - "folding_pow_bits": 13 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 21, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 14, - "final_queries": 23, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 57, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - }, - { - "num_queries": 37, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 12 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 22, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 23, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 57, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - }, - { - "num_queries": 37, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 13 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 23, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 23, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 57, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 37, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 24, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 23, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 58, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 13 - }, - { - "num_queries": 38, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 15 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 25, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 23, - "final_query_pow_bits": 15, - "rounds": [ - { - "num_queries": 58, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 14 - }, - { - "num_queries": 38, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 26, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 16, - "final_queries": 16, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 60, - "ood_samples": 2, - "query_pow_bits": 15, - "folding_pow_bits": 15 - }, - { - "num_queries": 38, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 17 - }, - { - "num_queries": 23, - "ood_samples": 2, - "query_pow_bits": 15, - "folding_pow_bits": 22 - } - ] - }, - { - "log_inv_rate": 4, - "num_variables": 27, - "commitment_ood_samples": 2, - "starting_folding_pow_bits": 15, - "final_queries": 16, - "final_query_pow_bits": 16, - "rounds": [ - { - "num_queries": 61, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 16 - }, - { - "num_queries": 38, - "ood_samples": 2, - "query_pow_bits": 16, - "folding_pow_bits": 18 - }, - { - "num_queries": 23, - "ood_samples": 2, - "query_pow_bits": 15, - "folding_pow_bits": 23 - } - ] - } -] \ No newline at end of file From 21f23f7e0ab8b06d65d29d444688f34c7fa1f558 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 24 May 2026 01:47:49 +0400 Subject: [PATCH 25/69] wip --- .../tests/check_poseidon1_constants.rs | 36 +++++++++++++------ crates/lean_prover/verifier.py | 10 ++++-- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/crates/lean_prover/tests/check_poseidon1_constants.rs b/crates/lean_prover/tests/check_poseidon1_constants.rs index 8077f9028..f119690ca 100644 --- a/crates/lean_prover/tests/check_poseidon1_constants.rs +++ b/crates/lean_prover/tests/check_poseidon1_constants.rs @@ -1,7 +1,7 @@ //! Ensure the Poseidon1 (width-16) constants hardcoded in //! `crates/lean_prover/verifier.py` match the Rust constants used by the AIR. -//! The test prints the expected line (so you can paste it back if anything -//! drifts) and asserts that `verifier.py` contains that exact string up to +//! The test prints the expected lines (so you can paste them back if anything +//! drifts) and asserts that `verifier.py` contains those exact strings up to //! whitespace. //! //! Run: @@ -21,6 +21,14 @@ fn k(x: KoalaBear) -> u32 { x.as_canonical_u32() } +/// Recover the 16 shifts of the circulant MDS by computing `M^T * e_0`, which +/// equals row 0 of `M` and therefore `[SHIFTS[0], SHIFTS[1], …, SHIFTS[15]]`. +/// We get row 0 from `dense_mds_matrix()` since `M[0][j] = SHIFTS[(j-0) % 16]`. +fn mds_circ_16_shifts() -> [KoalaBear; 16] { + let mds = dense_mds_matrix(); + mds[0] +} + /// Reconstruct the dense MDS matrix the way `mds_dense_16` does in /// `lean_vm::tables::poseidon_16::mod.rs` — run `mds_circ_16` on each standard /// basis vector and stack the columns into a row-major matrix. @@ -73,14 +81,12 @@ fn expected_poseidon1_constants_line() -> String { let sparse_v = poseidon1_sparse_v(); let first_rc = poseidon1_sparse_first_round_constants(); let scalar_rc = poseidon1_sparse_scalar_round_constants(); - let mds = dense_mds_matrix(); let initial: Vec> = initial.iter().map(|r| r.to_vec()).collect(); let final_: Vec> = final_.iter().map(|r| r.to_vec()).collect(); let m_i: Vec> = m_i.iter().map(|r| r.to_vec()).collect(); let first_row: Vec> = first_row.iter().map(|r| r.to_vec()).collect(); let sparse_v: Vec> = sparse_v.iter().map(|r| r.to_vec()).collect(); - let mds: Vec> = mds.iter().map(|r| r.to_vec()).collect(); format!( "POSEIDON1_CONSTANTS = {{\ @@ -92,8 +98,7 @@ fn expected_poseidon1_constants_line() -> String { 'sparse_first_row':{sfr},\ 'sparse_v':{sv},\ 'sparse_first_round_constants':{sfrc},\ -'sparse_scalar_round_constants':{ssrc},\ -'mds_dense':{mds}\ +'sparse_scalar_round_constants':{ssrc}\ }}", hf = POSEIDON1_HALF_FULL_ROUNDS, pr = POSEIDON1_PARTIAL_ROUNDS, @@ -104,25 +109,36 @@ fn expected_poseidon1_constants_line() -> String { sv = fmt_mat(&sparse_v), sfrc = fmt_vec(first_rc), ssrc = fmt_vec(scalar_rc), - mds = fmt_mat(&mds), ) } +fn expected_mds_shifts_line() -> String { + format!("MDS_CIRC_16_SHIFTS = {}", fmt_vec(&mds_circ_16_shifts())) +} + fn strip_ws(s: &str) -> String { s.chars().filter(|c| !c.is_whitespace()).collect() } #[test] fn check_poseidon1_constants() { - let expected = expected_poseidon1_constants_line(); - println!("{expected}"); + let expected_shifts = expected_mds_shifts_line(); + let expected_constants = expected_poseidon1_constants_line(); + println!("{expected_shifts}"); + println!("{expected_constants}"); let verifier_py = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("verifier.py"); let src = fs::read_to_string(&verifier_py).unwrap_or_else(|e| panic!("failed to read {}: {e}", verifier_py.display())); + let src_ws = strip_ws(&src); assert!( - strip_ws(&src).contains(&strip_ws(&expected)), + src_ws.contains(&strip_ws(&expected_shifts)), + "MDS_CIRC_16_SHIFTS in {} is out of sync with Rust. Replace the line with the one printed above.", + verifier_py.display(), + ); + assert!( + src_ws.contains(&strip_ws(&expected_constants)), "POSEIDON1_CONSTANTS in {} is out of sync with Rust. Replace the line with the one printed above.", verifier_py.display(), ); diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 7a7b3da2f..afcdbf7c6 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -57,10 +57,14 @@ # KoalaBear two-adic generators: index `b` is a primitive 2^b-th root of unity. KB_TWO_ADIC_GENERATORS: list[int] = [0x1, 0x7F000000, 0x7E010002, 0x6832FE4A, 0x08DBD69C, 0x0A28F031, 0x5C4A5B99, 0x29B75A80, 0x17668B8A, 0x27AD539B, 0x334D48C7, 0x7744959C, 0x768FC6FA, 0x303964B2, 0x3E687D4D, 0x45A60E61, 0x6E2F4D7A, 0x163BD499, 0x6C4A8A45, 0x143EF899, 0x514DDCAD, 0x484EF19B, 0x205D63C3, 0x68E7DD49, 0x6AC49F88] # noqa: E501 +# Circulant Poseidon1-KoalaBear (width 16) MDS matrix. Stored as the first row; +# the full 16×16 matrix is `M[i][j] = MDS_CIRC_16_SHIFTS[(j - i) % 16]`. +MDS_CIRC_16_SHIFTS = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) + # Poseidon2-KoalaBear (width 16) constants used by the AIR. Tuples-of-tuples to stay on one line; # raw u32s in canonical form. Synced with the Rust constants — `cargo test -p lean_prover --test # check_poseidon1_constants` enforces a match. -POSEIDON1_CONSTANTS = {'half_full_rounds':4,'partial_rounds':20,'initial_constants':((2128964168,288780357,316938561,2126233899,426817493,1714118888,1045008582,1738510837,889721787,8866516,681576474,419059826,1596305521,1583176088,1584387047,1529751136),(1863858111,1072044075,517831365,1464274176,1138001621,428001039,245709561,1641420379,1365482496,770454828,693167409,757905735,136670447,436275702,525466355,1559174242),(1030087950,869864998,322787870,267688717,948964561,740478015,679816114,113662466,2066544572,1744924186,367094720,1380455578,1842483872,416711434,1342291586,1692058446),(1493348999,1113949088,210900530,1071655077,610242121,1136339326,2020858841,1019840479,678147278,1678413261,1361743414,61132629,1209546658,64412292,1936878279,1980661727)),'final_constants':((1983525157,1330885184,414710339,733907571,479859442,1064293389,236801732,325174861,162067568,64109120,278581904,683867016,996448498,1960361559,1782740946,415413204),(1649591052,130819424,547348827,1386569644,1307680439,38932758,1581338609,1020895732,5942549,665140992,1924917707,1910029693,1100265370,1223195250,859919676,1674792874),(321520099,942924505,1232236036,88692728,2071051492,1945027965,1433294131,531185630,879398056,291692510,1546702888,155861652,810736858,932742296,1374710679,1703184249),(1973006548,1131403964,1724233597,1086876318,669451611,1829624280,2119538869,441255155,1580936135,1396398895,1043570981,1716351438,942566442,616885102,334644983,132306927)),'sparse_m_i':((1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),(0,1176991763,1962798433,507789489,1019168605,1163325691,466620818,1131708271,931504963,918112312,86863075,882630651,84434949,754655560,375632733,210588963),(0,1406869940,217296974,97037986,2020988961,1368157387,446815816,456620646,1350101418,1922416357,1227469637,603478726,1537295456,873165878,155811605,375632733),(0,682820181,2031016045,138039228,846585925,558910395,9722937,1543529703,2088599457,1481139936,255018864,2130530098,43680256,864171667,873165878,754655560),(0,110418656,1501074676,1412834556,1032465671,563855872,962367231,788585369,1597452496,62007254,389404591,904725063,1698425244,43680256,1537295456,84434949),(0,1538375796,2102000169,1333501812,530151948,2053218304,1744692061,1352986051,701513153,1663428696,1849567553,1774105504,904725063,2130530098,603478726,882630651),(0,807210699,1559543298,304640754,399071438,1521605122,2097677068,1930489690,1512116835,467964189,1473717591,1849567553,389404591,255018864,1227469637,86863075),(0,604440680,1962894682,695994228,2105212903,935308582,1889173744,983368822,39843520,446408074,467964189,1663428696,62007254,1481139936,1922416357,918112312),(0,956061053,360112848,1035074790,1610007096,1698268692,93985685,1442713600,622937787,39843520,1512116835,701513153,1597452496,2088599457,1350101418,931504963),(0,774245966,1379974664,604491366,1621008618,166130994,1057741505,706931411,1442713600,983368822,1930489690,1352986051,788585369,1543529703,456620646,1131708271),(0,1049574470,1831059707,1284527617,1297275196,751089896,981821717,1057741505,93985685,1889173744,2097677068,1744692061,962367231,9722937,446815816,466620818),(0,639454493,1368214806,1169707859,1849562776,1603581590,751089896,166130994,1698268692,935308582,1521605122,2053218304,563855872,558910395,1368157387,1163325691),(0,1332049785,2018469344,1406223611,1533175366,1849562776,1297275196,1621008618,1610007096,2105212903,399071438,530151948,1032465671,846585925,2020988961,1019168605),(0,67923442,549746182,1490248217,1406223611,1169707859,1284527617,604491366,1035074790,695994228,304640754,1333501812,1412834556,138039228,97037986,507789489),(0,1092915095,1728246700,549746182,2018469344,1368214806,1831059707,1379974664,360112848,1962894682,1559543298,2102000169,1501074676,2031016045,217296974,1962798433),(0,1102759352,1092915095,67923442,1332049785,639454493,1049574470,774245966,956061053,604440680,807210699,1538375796,110418656,682820181,1406869940,1176991763)),'sparse_first_row':((1,1044617752,1481433387,1878444588,2104235304,3722907,640029121,1328464283,527881075,2001559077,689032166,1880575107,358872577,108364736,1332772919,2129539744),(1,914163922,1285456931,2020639520,1453855633,1477444027,1339193063,328589713,208931151,1850882938,1462363792,869657005,805767435,796387373,400960806,755656571),(1,1787518610,1878065122,1211179805,1623392502,596876727,487353310,948630619,46137575,2011272885,785962300,141492211,527311230,1677138244,308914786,646273371),(1,475985225,1276143107,1696071196,32589944,1946884834,790695552,1297018938,1247695977,1441442697,348598749,1532810014,420302089,1768470437,2025744598,502837865),(1,189252144,933340760,1196177692,1207326122,1310289919,1018859884,1931151148,306468194,2080590861,1011822907,870184312,1169054295,299157995,550624518,761217428),(1,530026590,1465459236,1537941855,311984892,1766685979,254904495,1314612604,654252150,1786383982,886092250,951000927,1492154688,2015327114,1795152847,886334479),(1,1412702485,1137887454,520203158,1812492367,789833688,1233938788,1819934176,1614801759,746470909,1336417528,680335909,313757646,841134444,1641869241,998121965),(1,1839450507,1514473471,1397874495,427026633,825206836,1881998988,798695984,1518245734,137171104,1985410295,1204805986,708385656,135671564,181727188,1904989502),(1,1624250506,1276553090,1125040530,732235550,770503435,1098559359,1897139531,1957393256,2066469648,356890796,430889358,1662727935,736846479,86159423,2117864164),(1,1716262826,1219251453,406649991,956336998,891847704,1340399588,2120454448,1963372981,1580211744,1080244840,443212987,97408192,1276609344,864015922,791499252),(1,1506258844,428092642,616592092,491052976,1058721642,1154122014,1063147280,1468054399,738561812,458551485,1134033275,798609536,1652845715,1626650240,1834902174),(1,607388252,1341920224,661794417,844415991,1742333960,710739800,111776808,1680513373,1739278776,1261371867,485363479,9207629,1858910799,2063484755,1896740071),(1,837837165,218556391,599284291,302320589,605756188,1423640541,982100365,1395306646,2054696424,1124172688,311709517,1483301282,2057326379,468011828,195504483),(1,1289414867,961998037,641842254,1998469649,519613099,1247429126,607576114,76055325,48127247,1837975498,79401479,1108712765,543571094,1463931705,1750978183),(1,32307344,407129084,1819638694,777354771,1232160074,2126730873,1007018661,1966216114,644324697,1374455617,1280692573,1485221466,2092259084,2005955566,843003152),(1,1799257672,1148378128,1707844443,119809600,1464022250,1463207203,1189831139,80446531,29416071,995912922,1867752521,1481174533,1072217556,801048591,1832269882),(1,1030517273,477905567,1805133547,728218728,691695658,1764920569,1697028861,581984142,1322354059,843428748,903794926,1401335111,1906908186,1236851451,1854676428),(1,375781491,663573853,204251191,1828817902,317340619,1771861371,1841750301,1008632801,1793041736,369201095,488809041,46558970,875712544,1922589546,1760372266),(1,483733172,606096455,106009622,441040436,1929468092,1672504038,1906451897,986604790,1050370358,429434801,335400069,1143011095,1303702871,733751510,1165784837),(1,1108808405,2090426960,155082568,702347681,919398936,1226339182,1901110596,1230360372,1088093666,1713572740,675635302,759294455,895266739,255605669,1282509143)),'sparse_v':((815798082,1599417173,2019487682,1495563308,1429225500,462208417,1706939096,1929713759,2037985010,1993489272,146269421,1370491063,1457031915,1571606227,442112630,0),(1672393568,1841009674,1550920329,1779211568,1449479676,1961293578,1174765549,738863811,358643257,820352444,1150799707,619173188,922229211,1138887134,409392716,0),(115734840,580126996,1525976646,1239851818,2073245456,1030589628,1377558295,494197709,238790464,719384642,134484029,231324069,639578566,636120851,568223911,0),(1300554587,1450500786,38201558,1838005083,1019142646,576859025,592297447,460824075,1486889364,199901131,793972955,1649041905,1287870494,344387188,1436973230,0),(1379067459,1918333570,591694540,245256148,1209106504,89299776,769713898,422208520,319592660,1799482307,665955244,433129386,733120015,95130246,1380689525,0),(2043404085,177610011,776806663,1124577123,324662120,532004834,57207205,807037515,1322129569,535602285,1823856965,687338970,1150883563,1938629528,1135982477,0),(125487871,771378267,895416365,1835469214,1441346099,1574070991,1051852536,1802482042,424344011,439065759,635684069,172075871,2054384866,792486292,1785646874,0),(1951012288,581143389,1449982847,2034951834,1296305314,1253043123,437690613,1533604834,1062523959,460834088,2028092965,112092247,480561016,29047112,918330564,0),(1321780723,906545729,598089205,1961814902,59242599,1763880479,1227717469,421528848,858340345,1469534917,1284739390,1593161876,70443154,1376173926,950943549,0),(111983004,815998134,480506885,2051984157,1295771849,472501509,1228066101,982351516,1168152195,2065242773,1539603922,494115877,630184518,1875163552,1430833335,0),(223627613,90799236,405797050,756408252,269447004,368791260,977004868,2000904592,658368457,581053670,1971660486,1301775976,1711019833,8812901,370847939,0),(1677802579,1959347885,1379609505,1200496457,441395130,1651239120,388457220,600596382,1851813934,1099854908,1253845511,1066124698,1415589924,943395525,1201570139,0),(1742026459,398996820,1559417452,1869434180,1650939527,1678406146,697527412,1329042656,1590738739,840532121,1919639745,1493325582,385219255,1321483334,74724398,0),(370629703,968406604,283063044,1421912803,1218525950,235983381,2097999101,1417290051,89846708,1755258584,1614636443,1923339542,1443080738,1287589955,1628527076,0),(1404617516,1387461349,960446077,316686774,1493143485,1135010996,1364787501,1366151319,1429025689,560429732,1992657421,86028332,16393928,1587924775,1099758468,0),(710831155,1269946944,1906631355,1017976477,466873715,61539759,17059176,1278714883,2061644815,240339454,235970441,1012156003,1873469407,1611775578,1163822633,0),(1188745580,1055003602,785416201,868051025,1135832507,1004853599,904741729,809824679,980810992,1178194302,1159788697,949043013,1001466621,1011628637,924759953,0),(2475856,3337618,4161263,3129126,2071505,3373463,2975691,1742470,2828204,3695590,1809935,2316312,3448583,2986173,2518923,0),(3415,9781,5292,4288,7724,13016,3835,5807,4933,8577,13125,16823,3127,8363,15859,0),(3,13,22,67,2,15,63,101,1,2,17,11,1,51,1,0)),'sparse_first_round_constants':(1423960925,886776133,1838900201,1725134361,1970838154,1349502123,1632425298,1452136978,1500653880,1694910225,1895400154,783177966,1170207886,1249553016,1486169768,387169126),'sparse_scalar_round_constants':(1358473177,1095637505,293175207,73153213,86260038,722710190,2089335770,1280052251,576313228,265102820,1685441472,670793739,1640841922,1549535807,1957713140,1556154273,1103412295,2118144716,20933114),'mds_dense':((1,1,51,1,11,17,2,1,101,63,15,2,67,22,13,3),(3,1,1,51,1,11,17,2,1,101,63,15,2,67,22,13),(13,3,1,1,51,1,11,17,2,1,101,63,15,2,67,22),(22,13,3,1,1,51,1,11,17,2,1,101,63,15,2,67),(67,22,13,3,1,1,51,1,11,17,2,1,101,63,15,2),(2,67,22,13,3,1,1,51,1,11,17,2,1,101,63,15),(15,2,67,22,13,3,1,1,51,1,11,17,2,1,101,63),(63,15,2,67,22,13,3,1,1,51,1,11,17,2,1,101),(101,63,15,2,67,22,13,3,1,1,51,1,11,17,2,1),(1,101,63,15,2,67,22,13,3,1,1,51,1,11,17,2),(2,1,101,63,15,2,67,22,13,3,1,1,51,1,11,17),(17,2,1,101,63,15,2,67,22,13,3,1,1,51,1,11),(11,17,2,1,101,63,15,2,67,22,13,3,1,1,51,1),(1,11,17,2,1,101,63,15,2,67,22,13,3,1,1,51),(51,1,11,17,2,1,101,63,15,2,67,22,13,3,1,1),(1,51,1,11,17,2,1,101,63,15,2,67,22,13,3,1))} # noqa: E501 +POSEIDON1_CONSTANTS = {'half_full_rounds':4,'partial_rounds':20,'initial_constants':((2128964168,288780357,316938561,2126233899,426817493,1714118888,1045008582,1738510837,889721787,8866516,681576474,419059826,1596305521,1583176088,1584387047,1529751136),(1863858111,1072044075,517831365,1464274176,1138001621,428001039,245709561,1641420379,1365482496,770454828,693167409,757905735,136670447,436275702,525466355,1559174242),(1030087950,869864998,322787870,267688717,948964561,740478015,679816114,113662466,2066544572,1744924186,367094720,1380455578,1842483872,416711434,1342291586,1692058446),(1493348999,1113949088,210900530,1071655077,610242121,1136339326,2020858841,1019840479,678147278,1678413261,1361743414,61132629,1209546658,64412292,1936878279,1980661727)),'final_constants':((1983525157,1330885184,414710339,733907571,479859442,1064293389,236801732,325174861,162067568,64109120,278581904,683867016,996448498,1960361559,1782740946,415413204),(1649591052,130819424,547348827,1386569644,1307680439,38932758,1581338609,1020895732,5942549,665140992,1924917707,1910029693,1100265370,1223195250,859919676,1674792874),(321520099,942924505,1232236036,88692728,2071051492,1945027965,1433294131,531185630,879398056,291692510,1546702888,155861652,810736858,932742296,1374710679,1703184249),(1973006548,1131403964,1724233597,1086876318,669451611,1829624280,2119538869,441255155,1580936135,1396398895,1043570981,1716351438,942566442,616885102,334644983,132306927)),'sparse_m_i':((1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),(0,1176991763,1962798433,507789489,1019168605,1163325691,466620818,1131708271,931504963,918112312,86863075,882630651,84434949,754655560,375632733,210588963),(0,1406869940,217296974,97037986,2020988961,1368157387,446815816,456620646,1350101418,1922416357,1227469637,603478726,1537295456,873165878,155811605,375632733),(0,682820181,2031016045,138039228,846585925,558910395,9722937,1543529703,2088599457,1481139936,255018864,2130530098,43680256,864171667,873165878,754655560),(0,110418656,1501074676,1412834556,1032465671,563855872,962367231,788585369,1597452496,62007254,389404591,904725063,1698425244,43680256,1537295456,84434949),(0,1538375796,2102000169,1333501812,530151948,2053218304,1744692061,1352986051,701513153,1663428696,1849567553,1774105504,904725063,2130530098,603478726,882630651),(0,807210699,1559543298,304640754,399071438,1521605122,2097677068,1930489690,1512116835,467964189,1473717591,1849567553,389404591,255018864,1227469637,86863075),(0,604440680,1962894682,695994228,2105212903,935308582,1889173744,983368822,39843520,446408074,467964189,1663428696,62007254,1481139936,1922416357,918112312),(0,956061053,360112848,1035074790,1610007096,1698268692,93985685,1442713600,622937787,39843520,1512116835,701513153,1597452496,2088599457,1350101418,931504963),(0,774245966,1379974664,604491366,1621008618,166130994,1057741505,706931411,1442713600,983368822,1930489690,1352986051,788585369,1543529703,456620646,1131708271),(0,1049574470,1831059707,1284527617,1297275196,751089896,981821717,1057741505,93985685,1889173744,2097677068,1744692061,962367231,9722937,446815816,466620818),(0,639454493,1368214806,1169707859,1849562776,1603581590,751089896,166130994,1698268692,935308582,1521605122,2053218304,563855872,558910395,1368157387,1163325691),(0,1332049785,2018469344,1406223611,1533175366,1849562776,1297275196,1621008618,1610007096,2105212903,399071438,530151948,1032465671,846585925,2020988961,1019168605),(0,67923442,549746182,1490248217,1406223611,1169707859,1284527617,604491366,1035074790,695994228,304640754,1333501812,1412834556,138039228,97037986,507789489),(0,1092915095,1728246700,549746182,2018469344,1368214806,1831059707,1379974664,360112848,1962894682,1559543298,2102000169,1501074676,2031016045,217296974,1962798433),(0,1102759352,1092915095,67923442,1332049785,639454493,1049574470,774245966,956061053,604440680,807210699,1538375796,110418656,682820181,1406869940,1176991763)),'sparse_first_row':((1,1044617752,1481433387,1878444588,2104235304,3722907,640029121,1328464283,527881075,2001559077,689032166,1880575107,358872577,108364736,1332772919,2129539744),(1,914163922,1285456931,2020639520,1453855633,1477444027,1339193063,328589713,208931151,1850882938,1462363792,869657005,805767435,796387373,400960806,755656571),(1,1787518610,1878065122,1211179805,1623392502,596876727,487353310,948630619,46137575,2011272885,785962300,141492211,527311230,1677138244,308914786,646273371),(1,475985225,1276143107,1696071196,32589944,1946884834,790695552,1297018938,1247695977,1441442697,348598749,1532810014,420302089,1768470437,2025744598,502837865),(1,189252144,933340760,1196177692,1207326122,1310289919,1018859884,1931151148,306468194,2080590861,1011822907,870184312,1169054295,299157995,550624518,761217428),(1,530026590,1465459236,1537941855,311984892,1766685979,254904495,1314612604,654252150,1786383982,886092250,951000927,1492154688,2015327114,1795152847,886334479),(1,1412702485,1137887454,520203158,1812492367,789833688,1233938788,1819934176,1614801759,746470909,1336417528,680335909,313757646,841134444,1641869241,998121965),(1,1839450507,1514473471,1397874495,427026633,825206836,1881998988,798695984,1518245734,137171104,1985410295,1204805986,708385656,135671564,181727188,1904989502),(1,1624250506,1276553090,1125040530,732235550,770503435,1098559359,1897139531,1957393256,2066469648,356890796,430889358,1662727935,736846479,86159423,2117864164),(1,1716262826,1219251453,406649991,956336998,891847704,1340399588,2120454448,1963372981,1580211744,1080244840,443212987,97408192,1276609344,864015922,791499252),(1,1506258844,428092642,616592092,491052976,1058721642,1154122014,1063147280,1468054399,738561812,458551485,1134033275,798609536,1652845715,1626650240,1834902174),(1,607388252,1341920224,661794417,844415991,1742333960,710739800,111776808,1680513373,1739278776,1261371867,485363479,9207629,1858910799,2063484755,1896740071),(1,837837165,218556391,599284291,302320589,605756188,1423640541,982100365,1395306646,2054696424,1124172688,311709517,1483301282,2057326379,468011828,195504483),(1,1289414867,961998037,641842254,1998469649,519613099,1247429126,607576114,76055325,48127247,1837975498,79401479,1108712765,543571094,1463931705,1750978183),(1,32307344,407129084,1819638694,777354771,1232160074,2126730873,1007018661,1966216114,644324697,1374455617,1280692573,1485221466,2092259084,2005955566,843003152),(1,1799257672,1148378128,1707844443,119809600,1464022250,1463207203,1189831139,80446531,29416071,995912922,1867752521,1481174533,1072217556,801048591,1832269882),(1,1030517273,477905567,1805133547,728218728,691695658,1764920569,1697028861,581984142,1322354059,843428748,903794926,1401335111,1906908186,1236851451,1854676428),(1,375781491,663573853,204251191,1828817902,317340619,1771861371,1841750301,1008632801,1793041736,369201095,488809041,46558970,875712544,1922589546,1760372266),(1,483733172,606096455,106009622,441040436,1929468092,1672504038,1906451897,986604790,1050370358,429434801,335400069,1143011095,1303702871,733751510,1165784837),(1,1108808405,2090426960,155082568,702347681,919398936,1226339182,1901110596,1230360372,1088093666,1713572740,675635302,759294455,895266739,255605669,1282509143)),'sparse_v':((815798082,1599417173,2019487682,1495563308,1429225500,462208417,1706939096,1929713759,2037985010,1993489272,146269421,1370491063,1457031915,1571606227,442112630,0),(1672393568,1841009674,1550920329,1779211568,1449479676,1961293578,1174765549,738863811,358643257,820352444,1150799707,619173188,922229211,1138887134,409392716,0),(115734840,580126996,1525976646,1239851818,2073245456,1030589628,1377558295,494197709,238790464,719384642,134484029,231324069,639578566,636120851,568223911,0),(1300554587,1450500786,38201558,1838005083,1019142646,576859025,592297447,460824075,1486889364,199901131,793972955,1649041905,1287870494,344387188,1436973230,0),(1379067459,1918333570,591694540,245256148,1209106504,89299776,769713898,422208520,319592660,1799482307,665955244,433129386,733120015,95130246,1380689525,0),(2043404085,177610011,776806663,1124577123,324662120,532004834,57207205,807037515,1322129569,535602285,1823856965,687338970,1150883563,1938629528,1135982477,0),(125487871,771378267,895416365,1835469214,1441346099,1574070991,1051852536,1802482042,424344011,439065759,635684069,172075871,2054384866,792486292,1785646874,0),(1951012288,581143389,1449982847,2034951834,1296305314,1253043123,437690613,1533604834,1062523959,460834088,2028092965,112092247,480561016,29047112,918330564,0),(1321780723,906545729,598089205,1961814902,59242599,1763880479,1227717469,421528848,858340345,1469534917,1284739390,1593161876,70443154,1376173926,950943549,0),(111983004,815998134,480506885,2051984157,1295771849,472501509,1228066101,982351516,1168152195,2065242773,1539603922,494115877,630184518,1875163552,1430833335,0),(223627613,90799236,405797050,756408252,269447004,368791260,977004868,2000904592,658368457,581053670,1971660486,1301775976,1711019833,8812901,370847939,0),(1677802579,1959347885,1379609505,1200496457,441395130,1651239120,388457220,600596382,1851813934,1099854908,1253845511,1066124698,1415589924,943395525,1201570139,0),(1742026459,398996820,1559417452,1869434180,1650939527,1678406146,697527412,1329042656,1590738739,840532121,1919639745,1493325582,385219255,1321483334,74724398,0),(370629703,968406604,283063044,1421912803,1218525950,235983381,2097999101,1417290051,89846708,1755258584,1614636443,1923339542,1443080738,1287589955,1628527076,0),(1404617516,1387461349,960446077,316686774,1493143485,1135010996,1364787501,1366151319,1429025689,560429732,1992657421,86028332,16393928,1587924775,1099758468,0),(710831155,1269946944,1906631355,1017976477,466873715,61539759,17059176,1278714883,2061644815,240339454,235970441,1012156003,1873469407,1611775578,1163822633,0),(1188745580,1055003602,785416201,868051025,1135832507,1004853599,904741729,809824679,980810992,1178194302,1159788697,949043013,1001466621,1011628637,924759953,0),(2475856,3337618,4161263,3129126,2071505,3373463,2975691,1742470,2828204,3695590,1809935,2316312,3448583,2986173,2518923,0),(3415,9781,5292,4288,7724,13016,3835,5807,4933,8577,13125,16823,3127,8363,15859,0),(3,13,22,67,2,15,63,101,1,2,17,11,1,51,1,0)),'sparse_first_round_constants':(1423960925,886776133,1838900201,1725134361,1970838154,1349502123,1632425298,1452136978,1500653880,1694910225,1895400154,783177966,1170207886,1249553016,1486169768,387169126),'sparse_scalar_round_constants':(1358473177,1095637505,293175207,73153213,86260038,722710190,2089335770,1280052251,576313228,265102820,1685441472,670793739,1640841922,1549535807,1957713140,1556154273,1103412295,2118144716,20933114)} # noqa: E501 # fmt: on @@ -1148,6 +1152,8 @@ def _p1c() -> dict: raw = POSEIDON1_CONSTANTS fp_mat = lambda m: [[Fp(v) for v in row] for row in m] fp_vec = lambda v: [Fp(x) for x in v] + n = len(MDS_CIRC_16_SHIFTS) + mds_dense = [[Fp(MDS_CIRC_16_SHIFTS[(j - i) % n]) for j in range(n)] for i in range(n)] return { "half_full_rounds": raw["half_full_rounds"], "partial_rounds": raw["partial_rounds"], @@ -1158,7 +1164,7 @@ def _p1c() -> dict: "sparse_v": fp_mat(raw["sparse_v"]), "sparse_first_rc": fp_vec(raw["sparse_first_round_constants"]), "sparse_scalar_rc": fp_vec(raw["sparse_scalar_round_constants"]), - "mds_dense": fp_mat(raw["mds_dense"]), + "mds_dense": mds_dense, } From fd0d3fa016e574c8e42ee7cf8cf83d8be01fe81b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 24 May 2026 01:56:18 +0400 Subject: [PATCH 26/69] Date: Sun, 24 May 2026 02:27:42 +0400 Subject: [PATCH 27/69] wip --- crates/lean_prover/primitives.py | 385 ++++++++++++++++++ .../tests/check_poseidon1_constants.rs | 145 ------- crates/lean_prover/verifier.py | 134 +----- 3 files changed, 401 insertions(+), 263 deletions(-) create mode 100644 crates/lean_prover/primitives.py delete mode 100644 crates/lean_prover/tests/check_poseidon1_constants.rs diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py new file mode 100644 index 000000000..b41137238 --- /dev/null +++ b/crates/lean_prover/primitives.py @@ -0,0 +1,385 @@ +# extracted from: https://github.com/leanEthereum/leanSpec + +from __future__ import annotations + +from typing import Final, Sequence + +P: Final = 2**31 - 2**24 + 1 +"""KoalaBear prime: `2^31 - 2^24 + 1`.""" + + +class Fp: + """An element of the KoalaBear prime field `F_p`.""" + + __slots__ = ("value",) + + def __init__(self, value: int) -> None: + self.value = value % P + + def __add__(self, other: "Fp") -> "Fp": + return Fp(self.value + other.value) + + def __sub__(self, other: "Fp") -> "Fp": + return Fp(self.value - other.value) + + def __neg__(self) -> "Fp": + return Fp(-self.value) + + def __mul__(self, other: "Fp") -> "Fp": + return Fp(self.value * other.value) + + def __pow__(self, exponent: int) -> "Fp": + return Fp(pow(self.value, exponent, P)) + + def __eq__(self, other: object) -> bool: + return isinstance(other, Fp) and self.value == other.value + + def __hash__(self) -> int: + return hash(self.value) + + def __repr__(self) -> str: + return f"Fp(value={self.value})" + + +def quintic_mul(a, b, zero): + """Schoolbook product in `Fp[X]/(X⁵+X²−1)`. `a`, `b` and the result are length-5 + coefficient lists over any ring sharing the additive identity `zero`.""" + prod = [zero] * 9 + for i in range(5): + for j in range(5): + prod[i + j] = prod[i + j] + a[i] * b[j] + for k in range(8, 4, -1): # X^k = X^(k−5)·(1 − X²) for k ≥ 5. + prod[k - 5] = prod[k - 5] + prod[k] + prod[k - 3] = prod[k - 3] - prod[k] + return prod[:5] + + +class EF: + """Quintic extension `Fp[X] / (X⁵ + X² − 1)`.""" + + __slots__ = ("c",) + DIMENSION = 5 + + def __init__(self, coeffs: Sequence[Fp]): + assert len(coeffs) == 5 + self.c = tuple(coeffs) + + @staticmethod + def from_base(x: Fp) -> "EF": + return EF([x, Fp(0), Fp(0), Fp(0), Fp(0)]) + + def __add__(self, o): + if isinstance(o, Fp): + return EF([self.c[0] + o, *self.c[1:]]) + return EF([a + b for a, b in zip(self.c, o.c)]) + + def __sub__(self, o): + if isinstance(o, Fp): + return EF([self.c[0] - o, *self.c[1:]]) + return EF([a - b for a, b in zip(self.c, o.c)]) + + def __neg__(self): + return EF([-a for a in self.c]) + + __radd__ = __add__ + + def __mul__(self, o): + if isinstance(o, Fp): + return EF([a * o for a in self.c]) + return EF(quintic_mul(self.c, o.c, Fp(0))) + + __rmul__ = __mul__ + + def __eq__(self, o): + return isinstance(o, EF) and self.c == o.c + + def __hash__(self): + return hash(self.c) + + def __repr__(self): + return f"EF({[int(x.value) for x in self.c]})" + + def inv(self) -> "EF": + result, base, n = ONE, self, P**5 - 2 + while n > 0: + if n & 1: + result = result * base + base = base * base + n >>= 1 + return result + + +ZERO = EF([Fp(0)] * 5) +ONE = EF.from_base(Fp(1)) + + +def fb(v: int) -> EF: + """`EF` lift of an integer base-field element.""" + return EF.from_base(Fp(v)) + + +def ef_sum(terms) -> EF: + """Sum of an iterable of `EF` (empty -> `ZERO`).""" + return sum(terms, ZERO) + + +def ef_prod(factors) -> EF: + """Product of an iterable of `EF` (empty -> `ONE`).""" + acc = ONE + for f in factors: + acc = acc * f + return acc + + +# Plonky3 width-16 circulant MDS first row. +MDS_FIRST_ROW_16: Final = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) + + +# KoalaBear two-adic generators: index `b` is a primitive 2^b-th root of unity. +# Built by repeatedly squaring `g[24]` (the canonical generator of the order-2^24 +# subgroup): `g[b] = g[b+1]^2 mod P`. Yields `g[1] = -1` and `g[0] = 1`. +KB_TWO_ADIC_GENERATORS: list[int] = [1] * 25 +KB_TWO_ADIC_GENERATORS[24] = 0x6AC49F88 +for _b in range(23, -1, -1): + KB_TWO_ADIC_GENERATORS[_b] = pow(KB_TWO_ADIC_GENERATORS[_b + 1], 2, P) +del _b + + +# 448 raw Poseidon1-KoalaBear width-16 round constants generated by the Grain +# LFSR (Poseidon paper §5.3, parameters field_type=1, α=3, n=31, t=16, R_F=8, +# R_P=20). Reference: https://github.com/Plonky3/Plonky3/blob/main/poseidon1/generate_constants.py +# Layout: 4 initial-full rounds × 16 + 20 partial rounds × 16 + 4 terminal-full rounds × 16. +def _grain_lfsr_round_constants_16() -> tuple[int, ...]: + bits_msb = lambda v, w: [(v >> (w - 1 - i)) & 1 for i in range(w)] + state = bits_msb(1, 2) + bits_msb(0, 4) + bits_msb(31, 12) + bits_msb(16, 12) + bits_msb(8, 10) + bits_msb(20, 10) + [1] * 30 # fmt: skip + + def step() -> int: + nonlocal state + new = state[62] ^ state[51] ^ state[38] ^ state[23] ^ state[13] ^ state[0] + state = state[1:] + [new] + return new + + for _ in range(160): # spec-mandated warm-up + step() + + def next_bit() -> int: # self-shrinking generator: keep step()'s output only when the prior step was 1 + while True: + if step() == 1: + return step() + step() + + def next_fe() -> int: # rejection sampling into [0, P) + while True: + x = 0 + for _ in range(31): + x = (x << 1) | next_bit() + if x < P: + return x + + return tuple(next_fe() for _ in range((8 + 20) * 16)) + + +P1_ROUND_CONSTANTS_16: Final = _grain_lfsr_round_constants_16() + + +class Poseidon1Params: + """Parameters for a Poseidon1 instance.""" + + __slots__ = ("width", "rounds_f", "rounds_p", "mds_first_row", "round_constants") + + def __init__( + self, + width: int, + rounds_f: int, + rounds_p: int, + mds_first_row: Sequence[int], + round_constants: Sequence[int], + ) -> None: + assert len(mds_first_row) == width + assert len(round_constants) == (rounds_f + rounds_p) * width + self.width = width + self.rounds_f = rounds_f + self.rounds_p = rounds_p + self.mds_first_row = mds_first_row + self.round_constants = round_constants + + +class Poseidon1: + """Pure-Python Poseidon1 permutation (S-box: x → x^3; dense circulant MDS). + + Round structure: AddRoundConstants → S-box (full state for full rounds, only + position 0 for partial rounds) → MDS multiply. + """ + + __slots__ = ("_width", "_half_rounds_f", "_rounds_p", "_mds", "_rc") + + def __init__(self, params: Poseidon1Params) -> None: + self._width = params.width + self._half_rounds_f = params.rounds_f // 2 + self._rounds_p = params.rounds_p + n = params.width + # Build circulant MDS: M[i][j] = first_row[(j - i) mod n]. + self._mds = [[params.mds_first_row[(j - i) % n] for j in range(n)] for i in range(n)] + self._rc = list(params.round_constants) + + def permute(self, current_state: Sequence[Fp]) -> list[Fp]: + assert len(current_state) == self._width + s = [x.value for x in current_state] + w, p, mds, rc = self._width, P, self._mds, self._rc + idx = 0 + + def mds_mul() -> None: + new = [sum((mds[i][j] * s[j]) % p for j in range(w)) % p for i in range(w)] + s[:] = new + + for _ in range(self._half_rounds_f): + for i in range(w): + s[i] = (s[i] + rc[idx + i]) % p + idx += w + for i in range(w): + s[i] = (s[i] * s[i] % p) * s[i] % p + mds_mul() + for _ in range(self._rounds_p): + for i in range(w): + s[i] = (s[i] + rc[idx + i]) % p + idx += w + s[0] = (s[0] * s[0] % p) * s[0] % p + mds_mul() + for _ in range(self._half_rounds_f): + for i in range(w): + s[i] = (s[i] + rc[idx + i]) % p + idx += w + for i in range(w): + s[i] = (s[i] * s[i] % p) * s[i] % p + mds_mul() + + return [Fp(v) for v in s] + + +PARAMS_16 = Poseidon1Params( + width=16, + rounds_f=8, + rounds_p=20, + mds_first_row=MDS_FIRST_ROW_16, + round_constants=P1_ROUND_CONSTANTS_16, +) +"""Poseidon1 parameters for width-16 (8 full rounds, 20 partial).""" + + +# --------------------------------------------------------------------------- +# Plonky3 / HorizenLabs partial-round optimization for the AIR. +# +# The AIR circuit verifies the Poseidon1 permutation in a more compact form: for +# the 20 partial rounds, the dense MDS multiply is replaced by a single +# precomputed transition matrix `m_i` (applied once) plus, per round, a +# rank-1-style update parameterized by `sparse_first_row[r]` and `sparse_v[r]`. +# Round constants are similarly compressed into `sparse_first_round_constants` +# (16 elements, added once) and `sparse_scalar_round_constants` (R_P - 1 +# scalars, added to position 0 between rounds). +# +# Algorithm follows `crates/backend/koala-bear/src/poseidon1_koalabear_16.rs` +# (`compute_equivalent_matrices`, `equivalent_round_constants`). +# --------------------------------------------------------------------------- + + +def _mat_mul(a: list[list[int]], b: list[list[int]], n: int) -> list[list[int]]: + return [[sum(a[i][k] * b[k][j] for k in range(n)) % P for j in range(n)] for i in range(n)] + + +def _mat_vec(m: list[list[int]], v: Sequence[int], n: int) -> list[int]: + return [sum(m[i][j] * v[j] for j in range(n)) % P for i in range(n)] + + +def _mat_transpose(m: list[list[int]], n: int) -> list[list[int]]: + return [[m[j][i] for j in range(n)] for i in range(n)] + + +def _gauss_jordan_inv(m_in: list[list[int]], n: int) -> list[list[int]]: + aug = [row[:] for row in m_in] + inv = [[1 if i == j else 0 for j in range(n)] for i in range(n)] + for col in range(n): + pivot = next(r for r in range(col, n) if aug[r][col] != 0) + if pivot != col: + aug[col], aug[pivot] = aug[pivot], aug[col] + inv[col], inv[pivot] = inv[pivot], inv[col] + piv_inv = pow(aug[col][col], P - 2, P) + for j in range(n): + aug[col][j] = aug[col][j] * piv_inv % P + inv[col][j] = inv[col][j] * piv_inv % P + for i in range(n): + if i == col or aug[i][col] == 0: + continue + factor = aug[i][col] + for j in range(n): + aug[i][j] = (aug[i][j] - factor * aug[col][j]) % P + inv[i][j] = (inv[i][j] - factor * inv[col][j]) % P + return inv + + +def _compute_air_sparse_constants() -> dict: + w = PARAMS_16.width + hf = PARAMS_16.rounds_f // 2 + rp = PARAMS_16.rounds_p + rc = PARAMS_16.round_constants + + # Dense circulant MDS: M[i][j] = MDS_FIRST_ROW_16[(j - i) mod w]. + mds = [[MDS_FIRST_ROW_16[(j - i) % w] for j in range(w)] for i in range(w)] + mds_inv = _gauss_jordan_inv(mds, w) + partial_rc = [list(rc[(hf + i) * w : (hf + i + 1) * w]) for i in range(rp)] + + # --- Compress round constants via backward substitution through MDS^{-1}. --- + scalar_rc: list[int] = [0] * rp + tmp = list(partial_rc[rp - 1]) + for i in range(rp - 2, -1, -1): + inv_cip = _mat_vec(mds_inv, tmp, w) + scalar_rc[i + 1] = inv_cip[0] + tmp = list(partial_rc[i]) + for j in range(1, w): + tmp[j] = (tmp[j] + inv_cip[j]) % P + sparse_first_round_constants = tmp + sparse_scalar_round_constants = scalar_rc[1:] # length rp - 1 + + # --- Factor MDS into per-round sparse matrices. --- + mds_t = _mat_transpose(mds, w) + m_mul = [row[:] for row in mds_t] + v_collection: list[list[int]] = [] + w_hat_collection: list[list[int]] = [] + m_i = [[0] * w for _ in range(w)] + for _ in range(rp): + v_row = [m_mul[0][j + 1] if j < 15 else 0 for j in range(w)] + w_col = [m_mul[i + 1][0] for i in range(15)] + sub = [[m_mul[i + 1][j + 1] for j in range(15)] for i in range(15)] + m_hat_inv = _gauss_jordan_inv(sub, 15) + w_hat = [ + sum(m_hat_inv[i][k] * w_col[k] for k in range(15)) % P if i < 15 else 0 for i in range(w) + ] + v_collection.append(v_row) + w_hat_collection.append(w_hat) + m_i = [row[:] for row in m_mul] + m_i[0][0] = 1 + for i in range(1, w): + m_i[i][0] = 0 + for j in range(1, w): + m_i[0][j] = 0 + m_mul = _mat_mul(mds_t, m_i, w) + sparse_m_i = _mat_transpose(m_i, w) + v_collection.reverse() + w_hat_collection.reverse() + + # Pre-assemble full first rows: [mds[0][0], ŵ[0], ..., ŵ[14]]. + mds_0_0 = mds[0][0] + sparse_first_row = [[mds_0_0] + w_hat_collection[r][:15] for r in range(rp)] + + return { + "half_full_rounds": hf, + "partial_rounds": rp, + "sparse_m_i": sparse_m_i, + "sparse_first_row": sparse_first_row, + "sparse_v": v_collection, + "sparse_first_round_constants": sparse_first_round_constants, + "sparse_scalar_round_constants": sparse_scalar_round_constants, + } + + +# Precomputed once at module load. Consumed by `verifier._p1c()`. +POSEIDON1_AIR_CONSTANTS = _compute_air_sparse_constants() diff --git a/crates/lean_prover/tests/check_poseidon1_constants.rs b/crates/lean_prover/tests/check_poseidon1_constants.rs deleted file mode 100644 index f119690ca..000000000 --- a/crates/lean_prover/tests/check_poseidon1_constants.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Ensure the Poseidon1 (width-16) constants hardcoded in -//! `crates/lean_prover/verifier.py` match the Rust constants used by the AIR. -//! The test prints the expected lines (so you can paste them back if anything -//! drifts) and asserts that `verifier.py` contains those exact strings up to -//! whitespace. -//! -//! Run: -//! cargo test -p lean_prover --test check_poseidon1_constants -- --nocapture - -use std::fmt::Write as _; -use std::fs; -use std::path::PathBuf; - -use backend::{ - KoalaBear, POSEIDON1_HALF_FULL_ROUNDS, POSEIDON1_PARTIAL_ROUNDS, PrimeCharacteristicRing, PrimeField32, - mds_circ_16, poseidon1_final_constants, poseidon1_initial_constants, poseidon1_sparse_first_round_constants, - poseidon1_sparse_first_row, poseidon1_sparse_m_i, poseidon1_sparse_scalar_round_constants, poseidon1_sparse_v, -}; - -fn k(x: KoalaBear) -> u32 { - x.as_canonical_u32() -} - -/// Recover the 16 shifts of the circulant MDS by computing `M^T * e_0`, which -/// equals row 0 of `M` and therefore `[SHIFTS[0], SHIFTS[1], …, SHIFTS[15]]`. -/// We get row 0 from `dense_mds_matrix()` since `M[0][j] = SHIFTS[(j-0) % 16]`. -fn mds_circ_16_shifts() -> [KoalaBear; 16] { - let mds = dense_mds_matrix(); - mds[0] -} - -/// Reconstruct the dense MDS matrix the way `mds_dense_16` does in -/// `lean_vm::tables::poseidon_16::mod.rs` — run `mds_circ_16` on each standard -/// basis vector and stack the columns into a row-major matrix. -fn dense_mds_matrix() -> [[KoalaBear; 16]; 16] { - let mut cols = [[KoalaBear::ZERO; 16]; 16]; - for j in 0..16 { - let mut e = [KoalaBear::ZERO; 16]; - e[j] = KoalaBear::ONE; - mds_circ_16::(&mut e); - cols[j] = e; - } - let mut rows = [[KoalaBear::ZERO; 16]; 16]; - for i in 0..16 { - for j in 0..16 { - rows[i][j] = cols[j][i]; - } - } - rows -} - -fn fmt_vec(v: &[KoalaBear]) -> String { - let mut s = String::from("("); - for (i, &x) in v.iter().enumerate() { - if i > 0 { - s.push(','); - } - write!(s, "{}", k(x)).unwrap(); - } - s.push(')'); - s -} - -fn fmt_mat>(rows: &[R]) -> String { - let mut s = String::from("("); - for (i, row) in rows.iter().enumerate() { - if i > 0 { - s.push(','); - } - s.push_str(&fmt_vec(row.as_ref())); - } - s.push(')'); - s -} - -fn expected_poseidon1_constants_line() -> String { - let initial = poseidon1_initial_constants(); - let final_ = poseidon1_final_constants(); - let m_i = poseidon1_sparse_m_i(); - let first_row = poseidon1_sparse_first_row(); - let sparse_v = poseidon1_sparse_v(); - let first_rc = poseidon1_sparse_first_round_constants(); - let scalar_rc = poseidon1_sparse_scalar_round_constants(); - - let initial: Vec> = initial.iter().map(|r| r.to_vec()).collect(); - let final_: Vec> = final_.iter().map(|r| r.to_vec()).collect(); - let m_i: Vec> = m_i.iter().map(|r| r.to_vec()).collect(); - let first_row: Vec> = first_row.iter().map(|r| r.to_vec()).collect(); - let sparse_v: Vec> = sparse_v.iter().map(|r| r.to_vec()).collect(); - - format!( - "POSEIDON1_CONSTANTS = {{\ -'half_full_rounds':{hf},\ -'partial_rounds':{pr},\ -'initial_constants':{ic},\ -'final_constants':{fc},\ -'sparse_m_i':{smi},\ -'sparse_first_row':{sfr},\ -'sparse_v':{sv},\ -'sparse_first_round_constants':{sfrc},\ -'sparse_scalar_round_constants':{ssrc}\ -}}", - hf = POSEIDON1_HALF_FULL_ROUNDS, - pr = POSEIDON1_PARTIAL_ROUNDS, - ic = fmt_mat(&initial), - fc = fmt_mat(&final_), - smi = fmt_mat(&m_i), - sfr = fmt_mat(&first_row), - sv = fmt_mat(&sparse_v), - sfrc = fmt_vec(first_rc), - ssrc = fmt_vec(scalar_rc), - ) -} - -fn expected_mds_shifts_line() -> String { - format!("MDS_CIRC_16_SHIFTS = {}", fmt_vec(&mds_circ_16_shifts())) -} - -fn strip_ws(s: &str) -> String { - s.chars().filter(|c| !c.is_whitespace()).collect() -} - -#[test] -fn check_poseidon1_constants() { - let expected_shifts = expected_mds_shifts_line(); - let expected_constants = expected_poseidon1_constants_line(); - println!("{expected_shifts}"); - println!("{expected_constants}"); - - let verifier_py = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("verifier.py"); - let src = - fs::read_to_string(&verifier_py).unwrap_or_else(|e| panic!("failed to read {}: {e}", verifier_py.display())); - let src_ws = strip_ws(&src); - - assert!( - src_ws.contains(&strip_ws(&expected_shifts)), - "MDS_CIRC_16_SHIFTS in {} is out of sync with Rust. Replace the line with the one printed above.", - verifier_py.display(), - ); - assert!( - src_ws.contains(&strip_ws(&expected_constants)), - "POSEIDON1_CONSTANTS in {} is out of sync with Rust. Replace the line with the one printed above.", - verifier_py.display(), - ); -} diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index f6ca36528..b8eb6707b 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -5,16 +5,13 @@ Run this script to verify it. Setup (one-time): - uv venv .venv --python 3.12 - source .venv/bin/activate - VIRTUAL_ENV=.venv uv pip install "git+https://github.com/leanEthereum/leanSpec.git" cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture Run: - .venv/bin/python crates/lean_prover/verifier.py + python3 crates/lean_prover/verifier.py Format: - .venv/bin/ruff format --line-length 120 crates/lean_prover/verifier.py + ruff format --line-length 120 crates/lean_prover/verifier.py """ from __future__ import annotations @@ -23,8 +20,7 @@ from dataclasses import dataclass from typing import Sequence -from lean_spec.subspecs.koalabear import Fp, P -from lean_spec.subspecs.poseidon1 import PARAMS_16, Poseidon1 +from primitives import * # noqa: F403 WHIR_INITIAL_FOLDING_FACTOR, WHIR_SUBSEQUENT_FOLDING_FACTOR = 7, 5 MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 @@ -54,21 +50,6 @@ # fmt: off WHIR_CONFIGS = ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # noqa: E501 -# KoalaBear two-adic generators: index `b` is a primitive 2^b-th root of unity. -KB_TWO_ADIC_GENERATORS: list[int] = [0x1, 0x7F000000, 0x7E010002, 0x6832FE4A, 0x08DBD69C, 0x0A28F031, 0x5C4A5B99, 0x29B75A80, 0x17668B8A, 0x27AD539B, 0x334D48C7, 0x7744959C, 0x768FC6FA, 0x303964B2, 0x3E687D4D, 0x45A60E61, 0x6E2F4D7A, 0x163BD499, 0x6C4A8A45, 0x143EF899, 0x514DDCAD, 0x484EF19B, 0x205D63C3, 0x68E7DD49, 0x6AC49F88] # noqa: E501 - -# Circulant Poseidon1-KoalaBear (width 16) MDS matrix. Stored as the first row; -# the full 16×16 matrix is `M[i][j] = MDS_CIRC_16_SHIFTS[(j - i) % 16]`. -MDS_CIRC_16_SHIFTS = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) - -# Poseidon1-KoalaBear (width 16) constants used by the AIR. Tuples-of-tuples to stay on one line; -# raw u32s in canonical form. Synced with the Rust constants — `cargo test -p lean_prover --test -# check_poseidon1_constants` enforces a match. `initial_constants` and `final_constants` are the -# external (full) round constants generated by the Grain LFSR (Poseidon paper §5.3) with parameters -# field_type=1, α=3, n=31, t=16, R_F=8, R_P=20 — see -# https://github.com/Plonky3/Plonky3/blob/main/poseidon1/generate_constants.py. The `sparse_*` -# entries are the Plonky3/Horizen partial-round optimization derived from the partial RCs and MDS. -POSEIDON1_CONSTANTS = {'half_full_rounds':4,'partial_rounds':20,'initial_constants':((2128964168,288780357,316938561,2126233899,426817493,1714118888,1045008582,1738510837,889721787,8866516,681576474,419059826,1596305521,1583176088,1584387047,1529751136),(1863858111,1072044075,517831365,1464274176,1138001621,428001039,245709561,1641420379,1365482496,770454828,693167409,757905735,136670447,436275702,525466355,1559174242),(1030087950,869864998,322787870,267688717,948964561,740478015,679816114,113662466,2066544572,1744924186,367094720,1380455578,1842483872,416711434,1342291586,1692058446),(1493348999,1113949088,210900530,1071655077,610242121,1136339326,2020858841,1019840479,678147278,1678413261,1361743414,61132629,1209546658,64412292,1936878279,1980661727)),'final_constants':((1983525157,1330885184,414710339,733907571,479859442,1064293389,236801732,325174861,162067568,64109120,278581904,683867016,996448498,1960361559,1782740946,415413204),(1649591052,130819424,547348827,1386569644,1307680439,38932758,1581338609,1020895732,5942549,665140992,1924917707,1910029693,1100265370,1223195250,859919676,1674792874),(321520099,942924505,1232236036,88692728,2071051492,1945027965,1433294131,531185630,879398056,291692510,1546702888,155861652,810736858,932742296,1374710679,1703184249),(1973006548,1131403964,1724233597,1086876318,669451611,1829624280,2119538869,441255155,1580936135,1396398895,1043570981,1716351438,942566442,616885102,334644983,132306927)),'sparse_m_i':((1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),(0,1176991763,1962798433,507789489,1019168605,1163325691,466620818,1131708271,931504963,918112312,86863075,882630651,84434949,754655560,375632733,210588963),(0,1406869940,217296974,97037986,2020988961,1368157387,446815816,456620646,1350101418,1922416357,1227469637,603478726,1537295456,873165878,155811605,375632733),(0,682820181,2031016045,138039228,846585925,558910395,9722937,1543529703,2088599457,1481139936,255018864,2130530098,43680256,864171667,873165878,754655560),(0,110418656,1501074676,1412834556,1032465671,563855872,962367231,788585369,1597452496,62007254,389404591,904725063,1698425244,43680256,1537295456,84434949),(0,1538375796,2102000169,1333501812,530151948,2053218304,1744692061,1352986051,701513153,1663428696,1849567553,1774105504,904725063,2130530098,603478726,882630651),(0,807210699,1559543298,304640754,399071438,1521605122,2097677068,1930489690,1512116835,467964189,1473717591,1849567553,389404591,255018864,1227469637,86863075),(0,604440680,1962894682,695994228,2105212903,935308582,1889173744,983368822,39843520,446408074,467964189,1663428696,62007254,1481139936,1922416357,918112312),(0,956061053,360112848,1035074790,1610007096,1698268692,93985685,1442713600,622937787,39843520,1512116835,701513153,1597452496,2088599457,1350101418,931504963),(0,774245966,1379974664,604491366,1621008618,166130994,1057741505,706931411,1442713600,983368822,1930489690,1352986051,788585369,1543529703,456620646,1131708271),(0,1049574470,1831059707,1284527617,1297275196,751089896,981821717,1057741505,93985685,1889173744,2097677068,1744692061,962367231,9722937,446815816,466620818),(0,639454493,1368214806,1169707859,1849562776,1603581590,751089896,166130994,1698268692,935308582,1521605122,2053218304,563855872,558910395,1368157387,1163325691),(0,1332049785,2018469344,1406223611,1533175366,1849562776,1297275196,1621008618,1610007096,2105212903,399071438,530151948,1032465671,846585925,2020988961,1019168605),(0,67923442,549746182,1490248217,1406223611,1169707859,1284527617,604491366,1035074790,695994228,304640754,1333501812,1412834556,138039228,97037986,507789489),(0,1092915095,1728246700,549746182,2018469344,1368214806,1831059707,1379974664,360112848,1962894682,1559543298,2102000169,1501074676,2031016045,217296974,1962798433),(0,1102759352,1092915095,67923442,1332049785,639454493,1049574470,774245966,956061053,604440680,807210699,1538375796,110418656,682820181,1406869940,1176991763)),'sparse_first_row':((1,1044617752,1481433387,1878444588,2104235304,3722907,640029121,1328464283,527881075,2001559077,689032166,1880575107,358872577,108364736,1332772919,2129539744),(1,914163922,1285456931,2020639520,1453855633,1477444027,1339193063,328589713,208931151,1850882938,1462363792,869657005,805767435,796387373,400960806,755656571),(1,1787518610,1878065122,1211179805,1623392502,596876727,487353310,948630619,46137575,2011272885,785962300,141492211,527311230,1677138244,308914786,646273371),(1,475985225,1276143107,1696071196,32589944,1946884834,790695552,1297018938,1247695977,1441442697,348598749,1532810014,420302089,1768470437,2025744598,502837865),(1,189252144,933340760,1196177692,1207326122,1310289919,1018859884,1931151148,306468194,2080590861,1011822907,870184312,1169054295,299157995,550624518,761217428),(1,530026590,1465459236,1537941855,311984892,1766685979,254904495,1314612604,654252150,1786383982,886092250,951000927,1492154688,2015327114,1795152847,886334479),(1,1412702485,1137887454,520203158,1812492367,789833688,1233938788,1819934176,1614801759,746470909,1336417528,680335909,313757646,841134444,1641869241,998121965),(1,1839450507,1514473471,1397874495,427026633,825206836,1881998988,798695984,1518245734,137171104,1985410295,1204805986,708385656,135671564,181727188,1904989502),(1,1624250506,1276553090,1125040530,732235550,770503435,1098559359,1897139531,1957393256,2066469648,356890796,430889358,1662727935,736846479,86159423,2117864164),(1,1716262826,1219251453,406649991,956336998,891847704,1340399588,2120454448,1963372981,1580211744,1080244840,443212987,97408192,1276609344,864015922,791499252),(1,1506258844,428092642,616592092,491052976,1058721642,1154122014,1063147280,1468054399,738561812,458551485,1134033275,798609536,1652845715,1626650240,1834902174),(1,607388252,1341920224,661794417,844415991,1742333960,710739800,111776808,1680513373,1739278776,1261371867,485363479,9207629,1858910799,2063484755,1896740071),(1,837837165,218556391,599284291,302320589,605756188,1423640541,982100365,1395306646,2054696424,1124172688,311709517,1483301282,2057326379,468011828,195504483),(1,1289414867,961998037,641842254,1998469649,519613099,1247429126,607576114,76055325,48127247,1837975498,79401479,1108712765,543571094,1463931705,1750978183),(1,32307344,407129084,1819638694,777354771,1232160074,2126730873,1007018661,1966216114,644324697,1374455617,1280692573,1485221466,2092259084,2005955566,843003152),(1,1799257672,1148378128,1707844443,119809600,1464022250,1463207203,1189831139,80446531,29416071,995912922,1867752521,1481174533,1072217556,801048591,1832269882),(1,1030517273,477905567,1805133547,728218728,691695658,1764920569,1697028861,581984142,1322354059,843428748,903794926,1401335111,1906908186,1236851451,1854676428),(1,375781491,663573853,204251191,1828817902,317340619,1771861371,1841750301,1008632801,1793041736,369201095,488809041,46558970,875712544,1922589546,1760372266),(1,483733172,606096455,106009622,441040436,1929468092,1672504038,1906451897,986604790,1050370358,429434801,335400069,1143011095,1303702871,733751510,1165784837),(1,1108808405,2090426960,155082568,702347681,919398936,1226339182,1901110596,1230360372,1088093666,1713572740,675635302,759294455,895266739,255605669,1282509143)),'sparse_v':((815798082,1599417173,2019487682,1495563308,1429225500,462208417,1706939096,1929713759,2037985010,1993489272,146269421,1370491063,1457031915,1571606227,442112630,0),(1672393568,1841009674,1550920329,1779211568,1449479676,1961293578,1174765549,738863811,358643257,820352444,1150799707,619173188,922229211,1138887134,409392716,0),(115734840,580126996,1525976646,1239851818,2073245456,1030589628,1377558295,494197709,238790464,719384642,134484029,231324069,639578566,636120851,568223911,0),(1300554587,1450500786,38201558,1838005083,1019142646,576859025,592297447,460824075,1486889364,199901131,793972955,1649041905,1287870494,344387188,1436973230,0),(1379067459,1918333570,591694540,245256148,1209106504,89299776,769713898,422208520,319592660,1799482307,665955244,433129386,733120015,95130246,1380689525,0),(2043404085,177610011,776806663,1124577123,324662120,532004834,57207205,807037515,1322129569,535602285,1823856965,687338970,1150883563,1938629528,1135982477,0),(125487871,771378267,895416365,1835469214,1441346099,1574070991,1051852536,1802482042,424344011,439065759,635684069,172075871,2054384866,792486292,1785646874,0),(1951012288,581143389,1449982847,2034951834,1296305314,1253043123,437690613,1533604834,1062523959,460834088,2028092965,112092247,480561016,29047112,918330564,0),(1321780723,906545729,598089205,1961814902,59242599,1763880479,1227717469,421528848,858340345,1469534917,1284739390,1593161876,70443154,1376173926,950943549,0),(111983004,815998134,480506885,2051984157,1295771849,472501509,1228066101,982351516,1168152195,2065242773,1539603922,494115877,630184518,1875163552,1430833335,0),(223627613,90799236,405797050,756408252,269447004,368791260,977004868,2000904592,658368457,581053670,1971660486,1301775976,1711019833,8812901,370847939,0),(1677802579,1959347885,1379609505,1200496457,441395130,1651239120,388457220,600596382,1851813934,1099854908,1253845511,1066124698,1415589924,943395525,1201570139,0),(1742026459,398996820,1559417452,1869434180,1650939527,1678406146,697527412,1329042656,1590738739,840532121,1919639745,1493325582,385219255,1321483334,74724398,0),(370629703,968406604,283063044,1421912803,1218525950,235983381,2097999101,1417290051,89846708,1755258584,1614636443,1923339542,1443080738,1287589955,1628527076,0),(1404617516,1387461349,960446077,316686774,1493143485,1135010996,1364787501,1366151319,1429025689,560429732,1992657421,86028332,16393928,1587924775,1099758468,0),(710831155,1269946944,1906631355,1017976477,466873715,61539759,17059176,1278714883,2061644815,240339454,235970441,1012156003,1873469407,1611775578,1163822633,0),(1188745580,1055003602,785416201,868051025,1135832507,1004853599,904741729,809824679,980810992,1178194302,1159788697,949043013,1001466621,1011628637,924759953,0),(2475856,3337618,4161263,3129126,2071505,3373463,2975691,1742470,2828204,3695590,1809935,2316312,3448583,2986173,2518923,0),(3415,9781,5292,4288,7724,13016,3835,5807,4933,8577,13125,16823,3127,8363,15859,0),(3,13,22,67,2,15,63,101,1,2,17,11,1,51,1,0)),'sparse_first_round_constants':(1423960925,886776133,1838900201,1725134361,1970838154,1349502123,1632425298,1452136978,1500653880,1694910225,1895400154,783177966,1170207886,1249553016,1486169768,387169126),'sparse_scalar_round_constants':(1358473177,1095637505,293175207,73153213,86260038,722710190,2089335770,1280052251,576313228,265102820,1685441472,670793739,1640841922,1549535807,1957713140,1556154273,1103412295,2118144716,20933114)} # noqa: E501 # fmt: on @@ -76,96 +57,6 @@ class ProofError(Exception): pass -class EF: - """Quintic extension `Fp[X] / (X⁵ + X² − 1)`.""" - - __slots__ = ("c",) - DIMENSION = 5 - - def __init__(self, coeffs: Sequence[Fp]): - assert len(coeffs) == 5 - self.c = tuple(coeffs) - - @staticmethod - def from_base(x: Fp) -> "EF": - return EF([x, Fp(0), Fp(0), Fp(0), Fp(0)]) - - def __add__(self, o): - if isinstance(o, Fp): - return EF([self.c[0] + o, *self.c[1:]]) - return EF([a + b for a, b in zip(self.c, o.c)]) - - def __sub__(self, o): - if isinstance(o, Fp): - return EF([self.c[0] - o, *self.c[1:]]) - return EF([a - b for a, b in zip(self.c, o.c)]) - - def __neg__(self): - return EF([-a for a in self.c]) - - __radd__ = __add__ - - def __mul__(self, o): - if isinstance(o, Fp): - return EF([a * o for a in self.c]) - return EF(_quintic_mul(self.c, o.c, Fp(0))) - - __rmul__ = __mul__ - - def __eq__(self, o): - return isinstance(o, EF) and self.c == o.c - - def __hash__(self): - return hash(self.c) - - def __repr__(self): - return f"EF({[int(x.value) for x in self.c]})" - - def inv(self) -> "EF": - result, base, n = ONE, self, P**5 - 2 - while n > 0: - if n & 1: - result = result * base - base = base * base - n >>= 1 - return result - - -ZERO = EF([Fp(0)] * 5) -ONE = EF.from_base(Fp(1)) - - -def fb(v: int) -> EF: - """`EF` lift of an integer base-field element.""" - return EF.from_base(Fp(v)) - - -def ef_sum(terms) -> EF: - """Sum of an iterable of `EF` (empty -> `ZERO`).""" - return sum(terms, ZERO) - - -def ef_prod(factors) -> EF: - """Product of an iterable of `EF` (empty -> `ONE`).""" - acc = ONE - for f in factors: - acc = acc * f - return acc - - -def _quintic_mul(a, b, zero): - """Schoolbook product in `Fp[X]/(X⁵+X²−1)`. `a`, `b` and the result are length-5 - coefficient lists over any ring sharing the additive identity `zero`.""" - prod = [zero] * 9 - for i in range(5): - for j in range(5): - prod[i + j] = prod[i + j] + a[i] * b[j] - for k in range(8, 4, -1): # X^k = X^(k−5)·(1 − X²) for k ≥ 5. - prod[k - 5] = prod[k - 5] + prod[k] - prod[k - 3] = prod[k - 3] - prod[k] - return prod[:5] - - _POSEIDON16 = Poseidon1(PARAMS_16) @@ -1092,7 +983,7 @@ def _eval_air_execution(folder: ConstraintFolder, _table: TableMeta, extra_data: def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: """Multiply two quintic-extension elements whose coefficients are themselves `EF`.""" assert len(a) == 5 and len(b) == 5 - return _quintic_mul(a, b, ZERO) + return quintic_mul(a, b, ZERO) def _eval_air_extension_op(folder: ConstraintFolder, _table: TableMeta, extra_data: dict) -> None: @@ -1153,16 +1044,23 @@ def _eval_air_extension_op(folder: ConstraintFolder, _table: TableMeta, extra_da @functools.cache def _p1c() -> dict: - raw = POSEIDON1_CONSTANTS + raw = POSEIDON1_AIR_CONSTANTS fp_mat = lambda m: [[Fp(v) for v in row] for row in m] fp_vec = lambda v: [Fp(x) for x in v] - n = len(MDS_CIRC_16_SHIFTS) - mds_dense = [[Fp(MDS_CIRC_16_SHIFTS[(j - i) % n]) for j in range(n)] for i in range(n)] + n = len(MDS_FIRST_ROW_16) + mds_dense = [[Fp(MDS_FIRST_ROW_16[(j - i) % n]) for j in range(n)] for i in range(n)] + # External full-round RCs: first and last `half_full_rounds * width` entries of the + # raw 448-element round-constant table that drives the actual Poseidon permutation. + hf, t = raw["half_full_rounds"], WIDTH + rcs = PARAMS_16.round_constants + initial_constants = [[Fp(x) for x in rcs[i * t : (i + 1) * t]] for i in range(hf)] + tail_start = (hf + raw["partial_rounds"]) * t + final_constants = [[Fp(x) for x in rcs[tail_start + i * t : tail_start + (i + 1) * t]] for i in range(hf)] return { "half_full_rounds": raw["half_full_rounds"], "partial_rounds": raw["partial_rounds"], - "initial_constants": fp_mat(raw["initial_constants"]), - "final_constants": fp_mat(raw["final_constants"]), + "initial_constants": initial_constants, + "final_constants": final_constants, "sparse_m_i": fp_mat(raw["sparse_m_i"]), "sparse_first_row": fp_mat(raw["sparse_first_row"]), "sparse_v": fp_mat(raw["sparse_v"]), From e629aa3a0cbbbd87d171f65aaf4b1f2ecf16c1ae Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 24 May 2026 02:54:22 +0400 Subject: [PATCH 28/69] remove numpy dependency --- crates/lean_prover/verifier.py | 77 +++++++++++++++++----------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index b8eb6707b..6c8038d14 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -269,46 +269,47 @@ def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: - """numpy-backed fold for the 2²²-entry bytecode multilinear.""" - import numpy as np + """Evaluate a base-field multilinear in eval form at an EF point. + """ assert len(base_evals) == 1 << len(point) - pt = [tuple(int(ci.value) for ci in p.c) for p in point] - cur = np.asarray(base_evals, dtype=np.int64) % P - # First round: base → EF. a + (b-a)·r with r ∈ EF. - a, b = cur[0::2], cur[1::2] - d = (b - a) % P - r = pt[-1] - cur = np.stack( - [(a + d * r[0]) % P, *[(d * r[k]) % P for k in range(1, 5)]], - axis=1, - ) - # EF × EF rounds: schoolbook product reduced mod X⁵+X²−1. - for r0, r1, r2, r3, r4 in (pt[i] for i in range(len(pt) - 2, -1, -1)): - a, b = cur[0::2], cur[1::2] - d = (b - a) % P - d0, d1, d2, d3, d4 = d[:, 0], d[:, 1], d[:, 2], d[:, 3], d[:, 4] - m = lambda x, y: (x * y) % P - p0 = m(d0, r0) - p1 = (m(d0, r1) + m(d1, r0)) % P - p2 = (m(d0, r2) + m(d1, r1) + m(d2, r0)) % P - p3 = (m(d0, r3) + m(d1, r2) + m(d2, r1) + m(d3, r0)) % P - p4 = (m(d0, r4) + m(d1, r3) + m(d2, r2) + m(d3, r1) + m(d4, r0)) % P - p5 = (m(d1, r4) + m(d2, r3) + m(d3, r2) + m(d4, r1)) % P - p6 = (m(d2, r4) + m(d3, r3) + m(d4, r2)) % P - p7 = (m(d3, r4) + m(d4, r3)) % P - p8 = m(d4, r4) - cur = np.stack( - [ - (a[:, 0] + p0 + p5 - p8) % P, - (a[:, 1] + p1 + p6) % P, - (a[:, 2] + p2 - p5 + p7 + p8) % P, - (a[:, 3] + p3 - p6 + p8) % P, - (a[:, 4] + p4 - p7) % P, - ], - axis=1, - ) - return EF([Fp(int(v)) for v in cur[0]]) + + # First fold: cur[n] = base[2n] + (base[2n+1] − base[2n]) · r. + r0, r1, r2, r3, r4 = (int(c.value) for c in point[-1].c) + cur = [] + for j in range(0, len(base_evals), 2): + a = base_evals[j] % P + d = (base_evals[j + 1] - a) % P + cur.append(((a + d * r0) % P, (d * r1) % P, (d * r2) % P, (d * r3) % P, (d * r4) % P)) + + # Subsequent folds in EF. Schoolbook produces a degree-8 polynomial p[0..8]; + # reduce via X^5 ≡ 1 − X^2 (so X^6 ≡ X − X^3, X^7 ≡ X^2 − X^4, X^8 ≡ −1 + X^2 + X^3). + for pt in reversed(point[:-1]): + r0, r1, r2, r3, r4 = (int(c.value) for c in pt.c) + new = [] + for j in range(0, len(cur), 2): + a0, a1, a2, a3, a4 = cur[j] + b0, b1, b2, b3, b4 = cur[j + 1] + d0, d1, d2, d3, d4 = (b0 - a0) % P, (b1 - a1) % P, (b2 - a2) % P, (b3 - a3) % P, (b4 - a4) % P + p0 = d0 * r0 + p1 = d0 * r1 + d1 * r0 + p2 = d0 * r2 + d1 * r1 + d2 * r0 + p3 = d0 * r3 + d1 * r2 + d2 * r1 + d3 * r0 + p4 = d0 * r4 + d1 * r3 + d2 * r2 + d3 * r1 + d4 * r0 + p5 = d1 * r4 + d2 * r3 + d3 * r2 + d4 * r1 + p6 = d2 * r4 + d3 * r3 + d4 * r2 + p7 = d3 * r4 + d4 * r3 + p8 = d4 * r4 + new.append(( + (a0 + p0 + p5 - p8) % P, + (a1 + p1 + p6) % P, + (a2 + p2 - p5 + p7 + p8) % P, + (a3 + p3 - p6 + p8) % P, + (a4 + p4 - p7) % P, + )) + cur = new + + return EF([Fp(x) for x in cur[0]]) def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: From a235966d05af13f8153dc558774e9298d8fb04d5 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 24 May 2026 04:17:12 +0400 Subject: [PATCH 29/69] wip --- crates/lean_prover/verifier.py | 82 ++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 6c8038d14..104996b93 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -42,6 +42,8 @@ MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, BASE_TWO_ADICITY = 8, 8, 24 MAX_BYTECODE_LOG_SIZE = 22 MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension_op": 21, "poseidon16_compress": 21} +# Canonical table order — must match Rust `ALL_TABLES` in lean_vm/src/tables/table_enum.rs. +ALL_TABLES_ORDER = ("execution", "extension_op", "poseidon16_compress") # WHIR per-(log_inv_rate, num_variables) parameters. Tuple-of-tuples to keep the table on one line: # (log_inv_rate, num_variables, commitment_ood_samples, starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds) @@ -640,27 +642,35 @@ def stacked_pcs_global_statements( ) -> list[SparseStatement]: assert len(table_log_heights) == len(committed_statements) tables_sorted = sort_tables_by_height(table_log_heights) + max_table_n_vars = tables_sorted[0][1] + + # Layout offsets are assigned in sorted-by-height order (taller tables come first + # in the stacked polynomial), but statements are emitted in canonical ALL_TABLES order. + table_offsets: dict[str, int] = {} + layout_offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, max_table_n_vars)) + for name, n_vars in tables_sorted: + table_offsets[name] = layout_offset + layout_offset += tables[name].n_columns << n_vars out = list(previous_statements) - offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, tables_sorted[0][1])) col_pc = constants["col_pc"] - # Rust uses BTreeMap (sorted); Python dicts are insertion-ordered, sort here. - def values_at(d: dict[int, EF], n_vars: int) -> list[tuple[int, EF]]: - return [((offset >> n_vars) + i, v) for i, v in sorted(d.items())] - - for name, n_vars in tables_sorted: + for name in ALL_TABLES_ORDER: + n_vars = table_log_heights[name] + offset = table_offsets[name] if name == "execution": # PC column: pin first row to STARTING_PC, last row to ending_pc. for idx, pc in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: out.append(SparseStatement.unique_value(stacked_n_vars, offset + (col_pc << n_vars) + idx, fb(pc))) + # Rust uses BTreeMap (sorted); Python dicts are insertion-ordered, sort here. + def values_at(d: dict[int, EF], off: int = offset, nv: int = n_vars) -> list[tuple[int, EF]]: + return [((off >> nv) + i, v) for i, v in sorted(d.items())] + for point, eq_values, next_values in committed_statements[name]: if next_values: - out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values, n_vars))) - out.append(SparseStatement(stacked_n_vars, list(point), values_at(eq_values, n_vars))) - - offset += tables[name].n_columns << n_vars + out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values))) + out.append(SparseStatement(stacked_n_vars, list(point), values_at(eq_values))) return out @@ -823,27 +833,38 @@ def pref_at(offset: int, log_height: int) -> EF: ) offset += 1 << log_byte_pad + # Per-table base offsets in the GKR layout are assigned in sorted-by-height order + # (mirrors `layout_offsets` in sub_protocols/src/logup.rs). + table_offsets: dict[str, int] = {} + for name, log_n_rows in tables_sorted: + table_offsets[name] = offset + offset += n_buses(name) << log_n_rows + final_offset = offset + # Per-table: walk the bus spec in the same order as the Rust prover. The prover # writes col_evals for new (uncached) columns in `bus.data` order via a single # `add_extension_scalars` chunk per bus — the verifier must read in the same chunks. + # Iterate tables in canonical ALL_TABLES order (matches the new prover scalar layout). bus_num_vals: dict[str, EF] = {} bus_den_vals: dict[str, EF] = {} columns_values: dict[str, dict[int, EF]] = {} - for name, log_n_rows in tables_sorted: + for name in ALL_TABLES_ORDER: + log_n_rows = table_log_heights[name] meta = tables[name] table_values: dict[int, EF] = {} row_stride = 1 << log_n_rows + offset_within_table = table_offsets[name] for bus in meta.buses: - pref = pref_at(offset, log_n_rows) + pref = pref_at(offset_within_table, log_n_rows) match bus: case ("col_mult", _direction): bus_num_vals[name] = state.next_extension_scalar() bus_den_vals[name] = state.next_extension_scalar() num = num + pref * bus_num_vals[name] den = den + pref * bus_den_vals[name] - offset += row_stride + offset_within_table += row_stride case ("byte_lookup",): cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [col_pc] evals = state.next_extension_scalars_vec(len(cols)) @@ -851,7 +872,7 @@ def pref_at(offset: int, log_height: int) -> EF: table_values[c_idx] = e num = num + pref # Push direction den = den + pref * (c - finger_print(ds_byte, evals, alphas_eq_poly)) - offset += row_stride + offset_within_table += row_stride case ("mem_group", idx_col, vals_start, n): # One bus per row in the group; first sees idx_col fresh, the rest # see only val_col fresh (mirrors the Rust prover's dedup logic). @@ -864,17 +885,17 @@ def pref_at(offset: int, log_height: int) -> EF: table_values[idx_col] = next(evals) if val_fresh: table_values[val_col] = next(evals) - pref = pref_at(offset, log_n_rows) + pref = pref_at(offset_within_table, log_n_rows) fp = finger_print(ds_mem, [table_values[idx_col] + fb(i), table_values[val_col]], alphas_eq_poly) num = num + pref # Push direction den = den + pref * (c - fp) - offset += row_stride + offset_within_table += row_stride case _: raise ProofError(f"unknown bus kind: {bus[0]}") columns_values[name] = table_values - den = den + mle_of_zeros_then_ones(offset, point_gkr) + den = den + mle_of_zeros_then_ones(final_offset, point_gkr) if num != claim_num: raise ProofError("logup: numerators value mismatch") if den != claim_den: @@ -1243,9 +1264,9 @@ def verify_execution( tables_by_name = {t.name: t for t in tables} tables_sorted = sort_tables_by_height(table_log_heights) - # memory ≥ execution ≥ all other tables. - if log_memory < table_log_heights["execution"] or table_log_heights["execution"] < tables_sorted[0][1]: - raise ProofError("InvalidProof: memory or execution table size invariants broken") + # memory ≥ every table (no longer requires execution to be the largest). + if log_memory < tables_sorted[0][1]: + raise ProofError("InvalidProof: memory smaller than largest table") total_stacked = ( (2 << log_memory) + (1 << max(bytecode_log_size, tables_sorted[0][1])) @@ -1288,12 +1309,14 @@ def powers(x: EF, n: int) -> list[EF]: cur = cur * x return out - total_air_constraints = sum(_TABLE_SPECS[n]["n_constraints"] for n, _ in tables_sorted) + # AIR alpha offsets are now assigned in canonical ALL_TABLES order + # (mirrors `for table in ALL_TABLES { alpha_offset += n_constraints }` in verify_execution.rs). + total_air_constraints = sum(_TABLE_SPECS[n]["n_constraints"] for n in ALL_TABLES_ORDER) alpha_powers = powers(air_alpha, total_air_constraints) - alpha_offsets: list[int] = [] + alpha_offsets: dict[str, int] = {} cumulative = 0 - for name, _ in tables_sorted: - alpha_offsets.append(cumulative) + for name in ALL_TABLES_ORDER: + alpha_offsets[name] = cumulative cumulative += _TABLE_SPECS[name]["n_constraints"] extra_data = {"logup_alphas_eq_poly": logup_alphas_eq} @@ -1301,21 +1324,24 @@ def powers(x: EF, n: int) -> list[EF]: # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (c − bus_den)). The # sign is the direction of each table's unique Column-multiplicity bus. initial_sum = ZERO - for (name, _), offset in zip(tables_sorted, alpha_offsets): + for name in ALL_TABLES_ORDER: + offset = alpha_offsets[name] sign = -ONE if tables_by_name[name].buses[0][1] == "Pull" else ONE initial_sum = initial_sum + alpha_powers[offset] * (logup["bus_num"][name] * sign) initial_sum = initial_sum + alpha_powers[offset + 1] * (logup_c - logup["bus_den"][name]) n_max = tables_sorted[0][1] sc_point, sc_value = verify_sumcheck( - state, initial_sum, n_max, max(_TABLE_SPECS[n]["degree"] + 1 for n, _ in tables_sorted) + state, initial_sum, n_max, max(_TABLE_SPECS[n]["degree"] + 1 for n in ALL_TABLES_ORDER) ) committed = { name: [(list(from_end(gkr_point, table_log_heights[name])), dict(logup["columns_values"][name]), {})] - for name in tables_by_name + for name in ALL_TABLES_ORDER } my_air_final = ZERO - for (name, log_n_rows), offset in zip(tables_sorted, alpha_offsets): + for name in ALL_TABLES_ORDER: + log_n_rows = table_log_heights[name] + offset = alpha_offsets[name] meta, n_shift = tables_by_name[name], _TABLE_SPECS[name]["n_shift"] col_evals = state.next_extension_scalars_vec(meta.n_columns + n_shift) alpha_slice = alpha_powers[offset : offset + _TABLE_SPECS[name]["n_constraints"]] From cf806f94133de121da6d3961edd1ba455af3fb45 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 24 May 2026 04:30:24 +0400 Subject: [PATCH 30/69] simplify --- crates/lean_prover/verifier.py | 217 +++++++++++++++------------------ 1 file changed, 101 insertions(+), 116 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 104996b93..560f7702b 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -79,10 +79,10 @@ def hash_slice(data: Sequence[Fp]) -> list[Fp]: return state[:DIGEST_ELEMS] -def fiat_shamir_domain_sep(bytecode_hash: Sequence[Fp], public_input_size: int) -> list[Fp]: - """Domain-separator absorbed before the proof. Mixes the bytecode hash and the - bytecode's declared `public_input_size` (mirrors `lean_prover::fiat_shamir_domain_sep`).""" - tail = [Fp(public_input_size)] + [Fp(0)] * (RATE - 1) +def fiat_shamir_domain_sep(bytecode_hash: Sequence[Fp]) -> list[Fp]: + """Domain-separator absorbed before the proof. Mixes the bytecode hash and + `PUBLIC_INPUT_SIZE` (mirrors `lean_prover::fiat_shamir_domain_sep`).""" + tail = [Fp(PUBLIC_INPUT_SIZE)] + [Fp(0)] * (RATE - 1) extended = poseidon16_compress(SNARK_DOMAIN_SEP, tail) return poseidon16_compress(bytecode_hash, extended) @@ -115,11 +115,10 @@ def _sample_rate(self) -> list[Fp]: return list(self.state[CAPACITY:]) def _sample_many(self, n: int) -> list[Fp]: - if n == 0: - return [] - out = self._sample_rate() - for _ in range(1, n): - self.duplex() + out: list[Fp] = [] + for i in range(n): + if i: + self.duplex() out.extend(self._sample_rate()) return out @@ -364,6 +363,15 @@ def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: return n, nv - n * WHIR_SUBSEQUENT_FOLDING_FACTOR +def ef_powers(x: EF, n: int) -> list[EF]: + """`[1, x, x², …, x^(n−1)]`.""" + out, cur = [], ONE + for _ in range(n): + out.append(cur) + cur = cur * x + return out + + def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: return num_variables + start_rate - (RS_DOMAIN_INITIAL_REDUCTION_FACTOR + r - 1 if r >= 1 else 0) @@ -540,8 +548,8 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None new_commitment = ParsedCommitment( nvars_round, state.next_base_scalars_vec(DIGEST_ELEMS), - state.sample_vec(nood) if nood else [], - state.next_extension_scalars_vec(nood) if nood else [], + state.sample_vec(nood), + state.next_extension_scalars_vec(nood), ) stir = verify_stir_challenges( state, @@ -621,13 +629,29 @@ class TableMeta: name: str n_columns: int buses: tuple + air_degree: int # max degree of AIR transition constraints + n_constraints: int + n_shift: int # number of shift (next-row) columns + air_fn: object # (folder, extra_data) -> None, fills folder with AIR constraints + + @property + def n_buses(self) -> int: + # mem_group entries expand to `n` individual buses. + return sum(b[3] if b[0] == "mem_group" else 1 for b in self.buses) def tables_from_json(obj: list[dict]) -> list[TableMeta]: - return [ - TableMeta(name=t["name"], n_columns=int(t["n_columns"]), buses=_table_buses(t["name"], int(t["n_columns"]))) - for t in obj - ] + # (air_degree, n_constraints, n_shift, air_fn) per table. + specs = { + "execution": (5, 14, 2, _eval_air_execution), + "extension_op": (6, 35, 13, _eval_air_extension_op), + "poseidon16_compress": (10, 101, 0, _eval_air_poseidon16), + } + out = [] + for t in obj: + name, n_cols = t["name"], int(t["n_columns"]) + out.append(TableMeta(name, n_cols, _table_buses(name, n_cols), *specs[name])) + return out def stacked_pcs_global_statements( @@ -642,12 +666,11 @@ def stacked_pcs_global_statements( ) -> list[SparseStatement]: assert len(table_log_heights) == len(committed_statements) tables_sorted = sort_tables_by_height(table_log_heights) - max_table_n_vars = tables_sorted[0][1] # Layout offsets are assigned in sorted-by-height order (taller tables come first # in the stacked polynomial), but statements are emitted in canonical ALL_TABLES order. table_offsets: dict[str, int] = {} - layout_offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, max_table_n_vars)) + layout_offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, tables_sorted[0][1])) for name, n_vars in tables_sorted: table_offsets[name] = layout_offset layout_offset += tables[name].n_columns << n_vars @@ -655,22 +678,22 @@ def stacked_pcs_global_statements( out = list(previous_statements) col_pc = constants["col_pc"] + # Rust uses BTreeMap (sorted by key); Python dicts are insertion-ordered, sort here. + def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: + return [(col_base + i, v) for i, v in sorted(d.items())] + for name in ALL_TABLES_ORDER: n_vars = table_log_heights[name] offset = table_offsets[name] + col_base = offset >> n_vars if name == "execution": # PC column: pin first row to STARTING_PC, last row to ending_pc. for idx, pc in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: out.append(SparseStatement.unique_value(stacked_n_vars, offset + (col_pc << n_vars) + idx, fb(pc))) - - # Rust uses BTreeMap (sorted); Python dicts are insertion-ordered, sort here. - def values_at(d: dict[int, EF], off: int = offset, nv: int = n_vars) -> list[tuple[int, EF]]: - return [((off >> nv) + i, v) for i, v in sorted(d.items())] - for point, eq_values, next_values in committed_statements[name]: if next_values: - out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values))) - out.append(SparseStatement(stacked_n_vars, list(point), values_at(eq_values))) + out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values, col_base))) + out.append(SparseStatement(stacked_n_vars, list(point), values_at(eq_values, col_base))) return out @@ -707,10 +730,6 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] return quotient, point, claim_num, claim_den -def to_big_endian_in_field(value: int, bit_count: int) -> list[EF]: - return [ONE if (value >> (bit_count - 1 - i)) & 1 else ZERO for i in range(bit_count)] - - def from_end(seq: Sequence, n: int) -> list: """The last `n` elements of `seq` (empty list when `n == 0`).""" return list(seq[-n:]) if n else [] @@ -718,10 +737,8 @@ def from_end(seq: Sequence, n: int) -> list: def mle_of_01234567_etc(point: Sequence[EF]) -> EF: """MLE of `f(i) = i` (big-endian) at `point`.""" - if not point: - return ZERO - e = mle_of_01234567_etc(point[1:]) - return e + point[0] * fb(1 << (len(point) - 1)) + n = len(point) + return ef_sum(p * fb(1 << (n - 1 - i)) for i, p in enumerate(point)) def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: @@ -780,14 +797,10 @@ def verify_generic_logup( log_bytecode = log2_strict_usize(len(bytecode_multilinear) // (1 << log2_ceil_usize(n_instr_cols))) log_instr = log2_ceil_usize(n_instr_cols) - def n_buses(name: str) -> int: - # mem_group entries expand to `n` individual buses. - return sum(b[3] if b[0] == "mem_group" else 1 for b in tables[name].buses) - total_active_len = ( (1 << log_memory) + max(1 << log_bytecode, 1 << tables_sorted[0][1]) - + sum(n_buses(n) << h for n, h in tables_sorted) + + sum(tables[n].n_buses << h for n, h in tables_sorted) ) total_gkr_n_vars = log2_ceil_usize(total_active_len) @@ -799,7 +812,8 @@ def n_buses(name: str) -> int: def pref_at(offset: int, log_height: int) -> EF: n_missing = total_gkr_n_vars - log_height - bits = to_big_endian_in_field(offset >> log_height, n_missing) + idx = offset >> log_height + bits = [ONE if (idx >> (n_missing - 1 - i)) & 1 else ZERO for i in range(n_missing)] return eq_poly_outside(bits, point_gkr[:n_missing]) # Memory (data order: [value_index, value_memory] mirrors `crates/sub_protocols/src/logup.rs`). @@ -838,7 +852,7 @@ def pref_at(offset: int, log_height: int) -> EF: table_offsets: dict[str, int] = {} for name, log_n_rows in tables_sorted: table_offsets[name] = offset - offset += n_buses(name) << log_n_rows + offset += tables[name].n_buses << log_n_rows final_offset = offset # Per-table: walk the bus spec in the same order as the Rust prover. The prover @@ -951,11 +965,11 @@ def air_constraint_eval( extra_data: dict, ) -> EF: folder = ConstraintFolder(col_evals[: table.n_columns], col_evals[table.n_columns :], alpha_powers) - _TABLE_SPECS[table.name]["air"](folder, table, extra_data) + table.air_fn(folder, extra_data) return folder.accumulator -def _eval_air_execution(folder: ConstraintFolder, _table: TableMeta, extra_data: dict) -> None: +def _eval_air_execution(folder: ConstraintFolder, extra_data: dict) -> None: # fmt: off (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, @@ -1008,7 +1022,7 @@ def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: return quintic_mul(a, b, ZERO) -def _eval_air_extension_op(folder: ConstraintFolder, _table: TableMeta, extra_data: dict) -> None: +def _eval_air_extension_op(folder: ConstraintFolder, extra_data: dict) -> None: # Layout: shift columns 0..13 = (is_be, start, len, flag_{add,mul,poly_eq}, # idx_{a,b}, comp[0..5]); then idx_res, va, vb, vres (5 each). f = folder.flat @@ -1113,7 +1127,7 @@ def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: return state -def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, _extra_data: dict) -> None: +def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: """AIR for Poseidon1-16. Each `post` column commits an intermediate state, which we constrain against the local computation, then adopt to bound polynomial degree.""" const = _p1c() @@ -1161,7 +1175,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict, _extra_data: dict) folder.assert_zero(flag_permute * (state[i + _POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) -def _eval_air_poseidon16(folder: ConstraintFolder, _table: TableMeta, extra_data: dict) -> None: +def _eval_air_poseidon16(folder: ConstraintFolder, extra_data: dict) -> None: const = _p1c() flat, W = folder.flat, _POSEIDON_WIDTH half_initial = half_final = const["half_full_rounds"] // 2 @@ -1200,27 +1214,16 @@ def take(n: int) -> list[EF]: folder.assert_zero(flag_hardcoded_left * (offset_hardcoded_left - eff_idx_left_first)) folder.assert_zero(not_hcl * (index_a - eff_idx_left_first)) - _eval_poseidon1_16( - folder, - { - "inputs": inputs, - "beginning_full_rounds": beginning_full_rounds, - "partial_rounds": partial_cols, - "ending_full_rounds": ending_full_rounds, - "outputs_left": outputs_left, - "outputs_right": outputs_right, - "flag_half_output": flag_half_output, - "flag_permute": flag_permute, - }, - extra_data, - ) - - -_TABLE_SPECS: dict[str, dict] = { - "execution": {"degree": 5, "n_constraints": 14, "n_shift": 2, "air": _eval_air_execution}, - "extension_op": {"degree": 6, "n_constraints": 35, "n_shift": 13, "air": _eval_air_extension_op}, - "poseidon16_compress": {"degree": 10, "n_constraints": 101, "n_shift": 0, "air": _eval_air_poseidon16}, -} + _eval_poseidon1_16(folder, { + "inputs": inputs, + "beginning_full_rounds": beginning_full_rounds, + "partial_rounds": partial_cols, + "ending_full_rounds": ending_full_rounds, + "outputs_left": outputs_left, + "outputs_right": outputs_right, + "flag_half_output": flag_half_output, + "flag_permute": flag_permute, + }) def verify_execution( @@ -1237,51 +1240,46 @@ def verify_execution( state = VerifierState(proof) state.observe_scalars(list(public_input)) - state.observe_scalars(fiat_shamir_domain_sep(bytecode_hash, PUBLIC_INPUT_SIZE)) + state.observe_scalars(fiat_shamir_domain_sep(bytecode_hash)) dims = [int(x.value) for x in state.next_base_scalars_vec(2 + len(tables))] log_inv_rate, log_memory, *table_log_n_rows = dims if not MIN_WHIR_LOG_INV_RATE <= log_inv_rate <= MAX_WHIR_LOG_INV_RATE: raise ProofError("InvalidRate") - if log_memory < log2_strict_usize(PUBLIC_INPUT_SIZE): - raise ProofError("InvalidProof: memory smaller than public_input_size") - if any(h < MIN_LOG_N_ROWS_PER_TABLE for h in table_log_n_rows): - raise ProofError("InvalidProof: table too small") - for t, h in zip(tables, table_log_n_rows): - limit = MAX_LOG_N_ROWS_PER_TABLE.get(t.name) - if limit is None: - raise ProofError(f"InvalidProof: unknown table {t.name}") - if h > limit: - raise ProofError(f"InvalidProof: table {t.name} too large (log_n_rows={h} > {limit})") - if log_memory < max(max(table_log_n_rows, default=0), bytecode_log_size): - raise ProofError("InvalidProof: memory smaller than tables/bytecode") if not MIN_LOG_MEMORY_SIZE <= log_memory <= MAX_LOG_MEMORY_SIZE: raise ProofError("InvalidProof: log_memory out of range") if not MIN_BYTECODE_LOG_SIZE <= bytecode_log_size <= MAX_BYTECODE_LOG_SIZE: raise ProofError("InvalidProof: bytecode log_size out of range") + if log_memory < max(max(table_log_n_rows, default=0), bytecode_log_size): + raise ProofError("InvalidProof: memory smaller than tables/bytecode") + for t, h in zip(tables, table_log_n_rows): + limit = MAX_LOG_N_ROWS_PER_TABLE.get(t.name) + if limit is None: + raise ProofError(f"InvalidProof: unknown table {t.name}") + if not MIN_LOG_N_ROWS_PER_TABLE <= h <= limit: + raise ProofError(f"InvalidProof: table {t.name} log_n_rows={h} not in [{MIN_LOG_N_ROWS_PER_TABLE}, {limit}]") table_log_heights = {t.name: h for t, h in zip(tables, table_log_n_rows)} tables_by_name = {t.name: t for t in tables} tables_sorted = sort_tables_by_height(table_log_heights) + n_max = tables_sorted[0][1] - # memory ≥ every table (no longer requires execution to be the largest). - if log_memory < tables_sorted[0][1]: - raise ProofError("InvalidProof: memory smaller than largest table") total_stacked = ( (2 << log_memory) - + (1 << max(bytecode_log_size, tables_sorted[0][1])) + + (1 << max(bytecode_log_size, n_max)) + sum(t.n_columns << table_log_heights[t.name] for t in tables) ) stacked_n_vars = log2_ceil_usize(total_stacked) if stacked_n_vars > BASE_TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") cfg = whir_config(log_inv_rate, stacked_n_vars) - root = state.next_base_scalars_vec(DIGEST_ELEMS) - ood_points = state.sample_vec(cfg["commitment_ood_samples"]) if cfg["commitment_ood_samples"] else [] - ood_answers = ( - state.next_extension_scalars_vec(cfg["commitment_ood_samples"]) if cfg["commitment_ood_samples"] else [] + nood = cfg["commitment_ood_samples"] + parsed_commitment = ParsedCommitment( + stacked_n_vars, + state.next_base_scalars_vec(DIGEST_ELEMS), + state.sample_vec(nood), + state.next_extension_scalars_vec(nood), ) - parsed_commitment = ParsedCommitment(stacked_n_vars, root, ood_points, ood_answers) logup_c = state.sample() state.duplex() @@ -1302,22 +1300,14 @@ def verify_execution( air_alpha = state.sample() - def powers(x: EF, n: int) -> list[EF]: - out, cur = [], ONE - for _ in range(n): - out.append(cur) - cur = cur * x - return out - - # AIR alpha offsets are now assigned in canonical ALL_TABLES order + # AIR alpha powers/offsets are laid out in canonical ALL_TABLES order # (mirrors `for table in ALL_TABLES { alpha_offset += n_constraints }` in verify_execution.rs). - total_air_constraints = sum(_TABLE_SPECS[n]["n_constraints"] for n in ALL_TABLES_ORDER) - alpha_powers = powers(air_alpha, total_air_constraints) alpha_offsets: dict[str, int] = {} cumulative = 0 for name in ALL_TABLES_ORDER: alpha_offsets[name] = cumulative - cumulative += _TABLE_SPECS[name]["n_constraints"] + cumulative += tables_by_name[name].n_constraints + alpha_powers = ef_powers(air_alpha, cumulative) extra_data = {"logup_alphas_eq_poly": logup_alphas_eq} @@ -1329,33 +1319,29 @@ def powers(x: EF, n: int) -> list[EF]: sign = -ONE if tables_by_name[name].buses[0][1] == "Pull" else ONE initial_sum = initial_sum + alpha_powers[offset] * (logup["bus_num"][name] * sign) initial_sum = initial_sum + alpha_powers[offset + 1] * (logup_c - logup["bus_den"][name]) - n_max = tables_sorted[0][1] sc_point, sc_value = verify_sumcheck( - state, initial_sum, n_max, max(_TABLE_SPECS[n]["degree"] + 1 for n in ALL_TABLES_ORDER) + state, initial_sum, n_max, max(t.air_degree + 1 for t in tables) ) committed = { - name: [(list(from_end(gkr_point, table_log_heights[name])), dict(logup["columns_values"][name]), {})] + name: [(from_end(gkr_point, table_log_heights[name]), dict(logup["columns_values"][name]), {})] for name in ALL_TABLES_ORDER } my_air_final = ZERO for name in ALL_TABLES_ORDER: + meta = tables_by_name[name] log_n_rows = table_log_heights[name] + col_evals = state.next_extension_scalars_vec(meta.n_columns + meta.n_shift) offset = alpha_offsets[name] - meta, n_shift = tables_by_name[name], _TABLE_SPECS[name]["n_shift"] - col_evals = state.next_extension_scalars_vec(meta.n_columns + n_shift) - alpha_slice = alpha_powers[offset : offset + _TABLE_SPECS[name]["n_constraints"]] + alpha_slice = alpha_powers[offset : offset + meta.n_constraints] constraint_eval = air_constraint_eval(meta, col_evals, alpha_slice, extra_data) natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = ef_prod(sc_point[: n_max - log_n_rows]) - my_air_final = ( - my_air_final - + k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval - ) + my_air_final = my_air_final + k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} - next_vals = {j: col_evals[meta.n_columns + j] for j in range(n_shift)} + next_vals = {j: col_evals[meta.n_columns + j] for j in range(meta.n_shift)} committed[name].append((natural_pt, eq_vals, next_vals)) if my_air_final != sc_value: raise ProofError("AIR sumcheck: claimed value mismatch") @@ -1364,14 +1350,13 @@ def powers(x: EF, n: int) -> list[EF]: pm_point = state.sample_vec(log2_strict_usize(len(public_memory))) pm_eval = eval_multilinear_evals([EF.from_base(f) for f in public_memory], pm_point) - mk = lambda point, values: SparseStatement(stacked_n_vars, list(point), values) + bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size previous = [ - mk(from_end(gkr_point, log_memory), [(0, logup["value_memory"]), (1, logup["value_memory_acc"])]), - mk(pm_point, [(0, pm_eval)]), - mk( - from_end(gkr_point, bytecode_log_size), - [((2 << log_memory) >> bytecode_log_size, logup["value_bytecode_acc"])], - ), + SparseStatement(stacked_n_vars, from_end(gkr_point, log_memory), + [(0, logup["value_memory"]), (1, logup["value_memory_acc"])]), + SparseStatement(stacked_n_vars, pm_point, [(0, pm_eval)]), + SparseStatement(stacked_n_vars, from_end(gkr_point, bytecode_log_size), + [(bytecode_acc_idx, logup["value_bytecode_acc"])]), ] global_statements = stacked_pcs_global_statements( stacked_n_vars, From 7c0e4a2bcc35f5773a0f9221e132de733f6d58e0 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 24 May 2026 14:57:11 +0400 Subject: [PATCH 31/69] w --- crates/lean_prover/verifier.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 560f7702b..e510b949a 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -72,9 +72,10 @@ def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: def hash_slice(data: Sequence[Fp]) -> list[Fp]: - assert len(data) % RATE == 0 and len(data) >= WIDTH - state = poseidon16_compress_in_place(list(data[-WIDTH:])) - for k in range(len(data) // RATE - 3, -1, -1): + """RTL sponge absorb of `data` (RATE-multiple length) into IV `[len(data), 0, ..., 0]`.""" + assert len(data) % RATE == 0 and len(data) > 0 + state = [Fp(len(data))] + [Fp(0)] * (WIDTH - 1) + for k in range(len(data) // RATE - 1, -1, -1): state = poseidon16_compress_in_place(state[:CAPACITY] + list(data[k * RATE : (k + 1) * RATE])) return state[:DIGEST_ELEMS] @@ -1383,9 +1384,9 @@ def verify_execution( def poseidon_compress_slice_iv(data: Sequence[Fp]) -> list[Fp]: - """Hash a multiple-of-8 sequence (Poseidon16/Davies-Meyer, all-zero IV).""" + """Hash a multiple-of-8 sequence (Poseidon16/Davies-Meyer). IV first element = len(data).""" assert data and len(data) % 8 == 0 - h = [Fp(0)] * 8 + h = [Fp(len(data))] + [Fp(0)] * 7 for i in range(0, len(data), 8): h = poseidon16_compress(h, list(data[i : i + 8])) return h From f2622eca76bd89e62ac47c783cbbacbb9aec246f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 25 May 2026 14:39:42 +0400 Subject: [PATCH 32/69] w --- crates/lean_prover/verifier.py | 64 +++++++++++++++++----------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index e510b949a..3b24e71fd 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -873,40 +873,40 @@ def pref_at(offset: int, log_height: int) -> EF: for bus in meta.buses: pref = pref_at(offset_within_table, log_n_rows) - match bus: - case ("col_mult", _direction): - bus_num_vals[name] = state.next_extension_scalar() - bus_den_vals[name] = state.next_extension_scalar() - num = num + pref * bus_num_vals[name] - den = den + pref * bus_den_vals[name] - offset_within_table += row_stride - case ("byte_lookup",): - cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [col_pc] - evals = state.next_extension_scalars_vec(len(cols)) - for c_idx, e in zip(cols, evals): - table_values[c_idx] = e + if bus[0] == "col_mult": + bus_num_vals[name] = state.next_extension_scalar() + bus_den_vals[name] = state.next_extension_scalar() + num = num + pref * bus_num_vals[name] + den = den + pref * bus_den_vals[name] + offset_within_table += row_stride + elif bus[0] == "byte_lookup": + cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [col_pc] + evals = state.next_extension_scalars_vec(len(cols)) + for c_idx, e in zip(cols, evals): + table_values[c_idx] = e + num = num + pref # Push direction + den = den + pref * (c - finger_print(ds_byte, evals, alphas_eq_poly)) + offset_within_table += row_stride + elif bus[0] == "mem_group": + _, idx_col, vals_start, n = bus + # One bus per row in the group; first sees idx_col fresh, the rest + # see only val_col fresh (mirrors the Rust prover's dedup logic). + for i in range(n): + val_col = vals_start + i + idx_fresh = idx_col not in table_values + val_fresh = val_col not in table_values + evals = iter(state.next_extension_scalars_vec(idx_fresh + val_fresh)) + if idx_fresh: + table_values[idx_col] = next(evals) + if val_fresh: + table_values[val_col] = next(evals) + pref = pref_at(offset_within_table, log_n_rows) + fp = finger_print(ds_mem, [table_values[idx_col] + fb(i), table_values[val_col]], alphas_eq_poly) num = num + pref # Push direction - den = den + pref * (c - finger_print(ds_byte, evals, alphas_eq_poly)) + den = den + pref * (c - fp) offset_within_table += row_stride - case ("mem_group", idx_col, vals_start, n): - # One bus per row in the group; first sees idx_col fresh, the rest - # see only val_col fresh (mirrors the Rust prover's dedup logic). - for i in range(n): - val_col = vals_start + i - idx_fresh = idx_col not in table_values - val_fresh = val_col not in table_values - evals = iter(state.next_extension_scalars_vec(idx_fresh + val_fresh)) - if idx_fresh: - table_values[idx_col] = next(evals) - if val_fresh: - table_values[val_col] = next(evals) - pref = pref_at(offset_within_table, log_n_rows) - fp = finger_print(ds_mem, [table_values[idx_col] + fb(i), table_values[val_col]], alphas_eq_poly) - num = num + pref # Push direction - den = den + pref * (c - fp) - offset_within_table += row_stride - case _: - raise ProofError(f"unknown bus kind: {bus[0]}") + else: + raise ProofError(f"unknown bus kind: {bus[0]}") columns_values[name] = table_values From 9e9efc0c7e9085de8b3ffbcb1679c9126d632359 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 25 May 2026 17:10:45 +0400 Subject: [PATCH 33/69] wip --- crates/lean_prover/primitives.py | 1 + crates/lean_prover/verifier.py | 146 +++++++++++++++---------------- 2 files changed, 71 insertions(+), 76 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index b41137238..b7160a56a 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -7,6 +7,7 @@ P: Final = 2**31 - 2**24 + 1 """KoalaBear prime: `2^31 - 2^24 + 1`.""" +BASE_TWO_ADICITY = 24 class Fp: """An element of the KoalaBear prime field `F_p`.""" diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 3b24e71fd..008db0591 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1,8 +1,4 @@ -"""Pure-Python verifier for leanVM execution proofs. - -Single end-to-end test vector — a rec-aggregation proof over 1000 XMSS -signatures — is generated by the Rust side and stored under `target/`. -Run this script to verify it. +"""Pure-Python verifier for leanVM proofs. Setup (one-time): cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture @@ -20,39 +16,25 @@ from dataclasses import dataclass from typing import Sequence -from primitives import * # noqa: F403 +from primitives import * WHIR_INITIAL_FOLDING_FACTOR, WHIR_SUBSEQUENT_FOLDING_FACTOR = 7, 5 MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 -RATE, WIDTH, DIGEST_ELEMS = 8, 16, 8 -CAPACITY = WIDTH - RATE +SPONGE_RATE, SPONGE_STATE, DIGEST_ELEMS = 8, 16, 8 +SPNGE_CAPACITY = SPONGE_STATE - SPONGE_RATE PUBLIC_INPUT_SIZE = DIGEST_ELEMS - - -# fmt: off -SNARK_DOMAIN_SEP = [Fp(v) for v in ( - 130704175, 1303721200, 493664240, 1035493700, - 2063844858, 1410214009, 1938905908, 1696767928, -)] -# fmt: on +SNARK_DOMAIN_SEP = [Fp(v) for v in (130704175, 1303721200, 493664240, 1035493700, 2063844858, 1410214009, 1938905908, 1696767928)] # fmt: skip MIN_WHIR_LOG_INV_RATE, MAX_WHIR_LOG_INV_RATE = 1, 4 MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 -MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, BASE_TWO_ADICITY = 8, 8, 24 -MAX_BYTECODE_LOG_SIZE = 22 +MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, MAX_BYTECODE_LOG_SIZE = 8, 8, 22 MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension_op": 21, "poseidon16_compress": 21} -# Canonical table order — must match Rust `ALL_TABLES` in lean_vm/src/tables/table_enum.rs. ALL_TABLES_ORDER = ("execution", "extension_op", "poseidon16_compress") -# WHIR per-(log_inv_rate, num_variables) parameters. Tuple-of-tuples to keep the table on one line: -# (log_inv_rate, num_variables, commitment_ood_samples, starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds) -# where `rounds = ((num_queries, ood_samples, query_pow_bits, folding_pow_bits), ...)`. Synced with the -# Rust `WhirConfig` defaults — `cargo test -p lean_prover --test check_whir_configs` enforces a match. -# fmt: off -WHIR_CONFIGS = ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # noqa: E501 - -# fmt: on +# (log_inv_rate, num_variables, commitment_ood_samples, starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds) +# where `rounds = ((num_queries, ood_samples, query_pow_bits, folding_pow_bits), ...)`. (checked by `cargo test -p lean_prover --test check_whir_configs`) +WHIR_CONFIGS = ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # fmt: skip class ProofError(Exception): @@ -63,7 +45,7 @@ class ProofError(Exception): def poseidon16_compress_in_place(state: list[Fp]) -> list[Fp]: - assert len(state) == WIDTH + assert len(state) == SPONGE_STATE return [a + b for a, b in zip(_POSEIDON16.permute(state), state)] @@ -73,17 +55,17 @@ def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: def hash_slice(data: Sequence[Fp]) -> list[Fp]: """RTL sponge absorb of `data` (RATE-multiple length) into IV `[len(data), 0, ..., 0]`.""" - assert len(data) % RATE == 0 and len(data) > 0 - state = [Fp(len(data))] + [Fp(0)] * (WIDTH - 1) - for k in range(len(data) // RATE - 1, -1, -1): - state = poseidon16_compress_in_place(state[:CAPACITY] + list(data[k * RATE : (k + 1) * RATE])) + assert len(data) % SPONGE_RATE == 0 and len(data) > 0 + state = [Fp(len(data))] + [Fp(0)] * (SPONGE_STATE - 1) + for k in range(len(data) // SPONGE_RATE - 1, -1, -1): + state = poseidon16_compress_in_place(state[:SPNGE_CAPACITY] + list(data[k * SPONGE_RATE : (k + 1) * SPONGE_RATE])) return state[:DIGEST_ELEMS] def fiat_shamir_domain_sep(bytecode_hash: Sequence[Fp]) -> list[Fp]: """Domain-separator absorbed before the proof. Mixes the bytecode hash and `PUBLIC_INPUT_SIZE` (mirrors `lean_prover::fiat_shamir_domain_sep`).""" - tail = [Fp(PUBLIC_INPUT_SIZE)] + [Fp(0)] * (RATE - 1) + tail = [Fp(PUBLIC_INPUT_SIZE)] + [Fp(0)] * (SPONGE_RATE - 1) extended = poseidon16_compress(SNARK_DOMAIN_SEP, tail) return poseidon16_compress(bytecode_hash, extended) @@ -93,27 +75,27 @@ class Challenger: `_sample_rate()` reads `state[CAPACITY:]` once per `duplex()`.""" def __init__(self) -> None: - self.state: list[Fp] = [Fp(0)] * WIDTH + self.state: list[Fp] = [Fp(0)] * SPONGE_STATE self.rate_fresh: bool = False def observe(self, chunk: Sequence[Fp]) -> None: - assert len(chunk) == RATE - self.state = _POSEIDON16.permute(self.state[:CAPACITY] + list(chunk)) + assert len(chunk) == SPONGE_RATE + self.state = _POSEIDON16.permute(self.state[:SPNGE_CAPACITY] + list(chunk)) self.rate_fresh = True def observe_many(self, scalars: Sequence[Fp]) -> None: - for i in range(0, len(scalars), RATE): - chunk = list(scalars[i : i + RATE]) - chunk += [Fp(0)] * (RATE - len(chunk)) + for i in range(0, len(scalars), SPONGE_RATE): + chunk = list(scalars[i : i + SPONGE_RATE]) + chunk += [Fp(0)] * (SPONGE_RATE - len(chunk)) self.observe(chunk) def duplex(self) -> None: - self.observe([Fp(0)] * RATE) + self.observe([Fp(0)] * SPONGE_RATE) def _sample_rate(self) -> list[Fp]: assert self.rate_fresh, "stale rate — insert duplex() before sampling" self.rate_fresh = False - return list(self.state[CAPACITY:]) + return list(self.state[SPNGE_CAPACITY:]) def _sample_many(self, n: int) -> list[Fp]: out: list[Fp] = [] @@ -124,7 +106,7 @@ def _sample_many(self, n: int) -> list[Fp]: return out def sample_vec(self, n: int) -> list[EF]: - flat = self._sample_many((n * EF.DIMENSION + RATE - 1) // RATE)[: n * EF.DIMENSION] + flat = self._sample_many((n * EF.DIMENSION + SPONGE_RATE - 1) // SPONGE_RATE)[: n * EF.DIMENSION] return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] def sample(self) -> EF: @@ -132,7 +114,7 @@ def sample(self) -> EF: def sample_in_range(self, bits: int, n_samples: int) -> list[int]: assert bits < 31 - flat = self._sample_many((n_samples + RATE - 1) // RATE)[:n_samples] + flat = self._sample_many((n_samples + SPONGE_RATE - 1) // SPONGE_RATE)[:n_samples] return [int(x.value) & ((1 << bits) - 1) for x in flat] @@ -160,7 +142,7 @@ def __init__(self, proof: Proof) -> None: self.open_idx = 0 def _read_padded(self, n: int) -> list[Fp]: - n_pad = -(-n // RATE) * RATE + n_pad = -(-n // SPONGE_RATE) * SPONGE_RATE if self.offset + n_pad > len(self.transcript): raise ProofError("ExceededTranscript") chunk = self.transcript[self.offset : self.offset + n_pad] @@ -193,7 +175,7 @@ def check_pow_grinding(self, bits: int) -> None: if bits == 0: return self._read_padded(1) - if int(self.state[CAPACITY].value) & ((1 << bits) - 1) != 0: + if int(self.state[SPNGE_CAPACITY].value) & ((1 << bits) - 1) != 0: raise ProofError("InvalidGrindingWitness") @@ -271,9 +253,7 @@ def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: - """Evaluate a base-field multilinear in eval form at an EF point. - - """ + """Evaluate a base-field multilinear in eval form at an EF point.""" assert len(base_evals) == 1 << len(point) # First fold: cur[n] = base[2n] + (base[2n+1] − base[2n]) · r. @@ -302,13 +282,15 @@ def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: p6 = d2 * r4 + d3 * r3 + d4 * r2 p7 = d3 * r4 + d4 * r3 p8 = d4 * r4 - new.append(( - (a0 + p0 + p5 - p8) % P, - (a1 + p1 + p6) % P, - (a2 + p2 - p5 + p7 + p8) % P, - (a3 + p3 - p6 + p8) % P, - (a4 + p4 - p7) % P, - )) + new.append( + ( + (a0 + p0 + p5 - p8) % P, + (a1 + p1 + p6) % P, + (a2 + p2 - p5 + p7 + p8) % P, + (a3 + p3 - p6 + p8) % P, + (a4 + p4 - p7) % P, + ) + ) cur = new return EF([Fp(x) for x in cur[0]]) @@ -598,6 +580,7 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None return folding_flat + def _table_buses(name: str, n_columns: int) -> tuple: if name == "execution": return ( @@ -951,7 +934,9 @@ def assert_bool(self, x: EF) -> None: self.assert_zero(x * (ONE - x)) -def _eval_bus_virtual(folder: "ConstraintFolder", extra_data: dict, multiplicity: EF, discriminator: EF, data: Sequence[EF]) -> None: +def _eval_bus_virtual( + folder: "ConstraintFolder", extra_data: dict, multiplicity: EF, discriminator: EF, data: Sequence[EF] +) -> None: alphas: list[EF] = extra_data["logup_alphas_eq_poly"] assert len(data) < len(alphas) folder.assert_zero(multiplicity) @@ -1088,7 +1073,7 @@ def _p1c() -> dict: mds_dense = [[Fp(MDS_FIRST_ROW_16[(j - i) % n]) for j in range(n)] for i in range(n)] # External full-round RCs: first and last `half_full_rounds * width` entries of the # raw 448-element round-constant table that drives the actual Poseidon permutation. - hf, t = raw["half_full_rounds"], WIDTH + hf, t = raw["half_full_rounds"], SPONGE_STATE rcs = PARAMS_16.round_constants initial_constants = [[Fp(x) for x in rcs[i * t : (i + 1) * t]] for i in range(hf)] tail_start = (hf + raw["partial_rounds"]) * t @@ -1215,16 +1200,19 @@ def take(n: int) -> list[EF]: folder.assert_zero(flag_hardcoded_left * (offset_hardcoded_left - eff_idx_left_first)) folder.assert_zero(not_hcl * (index_a - eff_idx_left_first)) - _eval_poseidon1_16(folder, { - "inputs": inputs, - "beginning_full_rounds": beginning_full_rounds, - "partial_rounds": partial_cols, - "ending_full_rounds": ending_full_rounds, - "outputs_left": outputs_left, - "outputs_right": outputs_right, - "flag_half_output": flag_half_output, - "flag_permute": flag_permute, - }) + _eval_poseidon1_16( + folder, + { + "inputs": inputs, + "beginning_full_rounds": beginning_full_rounds, + "partial_rounds": partial_cols, + "ending_full_rounds": ending_full_rounds, + "outputs_left": outputs_left, + "outputs_right": outputs_right, + "flag_half_output": flag_half_output, + "flag_permute": flag_permute, + }, + ) def verify_execution( @@ -1258,7 +1246,9 @@ def verify_execution( if limit is None: raise ProofError(f"InvalidProof: unknown table {t.name}") if not MIN_LOG_N_ROWS_PER_TABLE <= h <= limit: - raise ProofError(f"InvalidProof: table {t.name} log_n_rows={h} not in [{MIN_LOG_N_ROWS_PER_TABLE}, {limit}]") + raise ProofError( + f"InvalidProof: table {t.name} log_n_rows={h} not in [{MIN_LOG_N_ROWS_PER_TABLE}, {limit}]" + ) table_log_heights = {t.name: h for t, h in zip(tables, table_log_n_rows)} tables_by_name = {t.name: t for t in tables} @@ -1320,9 +1310,7 @@ def verify_execution( sign = -ONE if tables_by_name[name].buses[0][1] == "Pull" else ONE initial_sum = initial_sum + alpha_powers[offset] * (logup["bus_num"][name] * sign) initial_sum = initial_sum + alpha_powers[offset + 1] * (logup_c - logup["bus_den"][name]) - sc_point, sc_value = verify_sumcheck( - state, initial_sum, n_max, max(t.air_degree + 1 for t in tables) - ) + sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in tables)) committed = { name: [(from_end(gkr_point, table_log_heights[name]), dict(logup["columns_values"][name]), {})] @@ -1339,7 +1327,9 @@ def verify_execution( natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = ef_prod(sc_point[: n_max - log_n_rows]) - my_air_final = my_air_final + k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval + my_air_final = ( + my_air_final + k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval + ) eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} next_vals = {j: col_evals[meta.n_columns + j] for j in range(meta.n_shift)} @@ -1353,11 +1343,15 @@ def verify_execution( bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size previous = [ - SparseStatement(stacked_n_vars, from_end(gkr_point, log_memory), - [(0, logup["value_memory"]), (1, logup["value_memory_acc"])]), + SparseStatement( + stacked_n_vars, + from_end(gkr_point, log_memory), + [(0, logup["value_memory"]), (1, logup["value_memory_acc"])], + ), SparseStatement(stacked_n_vars, pm_point, [(0, pm_eval)]), - SparseStatement(stacked_n_vars, from_end(gkr_point, bytecode_log_size), - [(bytecode_acc_idx, logup["value_bytecode_acc"])]), + SparseStatement( + stacked_n_vars, from_end(gkr_point, bytecode_log_size), [(bytecode_acc_idx, logup["value_bytecode_acc"])] + ), ] global_statements = stacked_pcs_global_statements( stacked_n_vars, From af59c09e5f2c9be952fabe3aeccba5ff82853591 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 25 May 2026 17:13:33 +0400 Subject: [PATCH 34/69] wip --- crates/lean_prover/primitives.py | 25 ++++++++--------- crates/lean_prover/verifier.py | 46 ++++++++++++++++---------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index b7160a56a..c8afa0e16 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -61,13 +61,15 @@ class EF: __slots__ = ("c",) DIMENSION = 5 - def __init__(self, coeffs: Sequence[Fp]): - assert len(coeffs) == 5 - self.c = tuple(coeffs) - - @staticmethod - def from_base(x: Fp) -> "EF": - return EF([x, Fp(0), Fp(0), Fp(0), Fp(0)]) + def __init__(self, value): + """Accepts an `int` (lifted via `Fp`), an `Fp` (lifted), or a length-5 `Sequence[Fp]`.""" + if isinstance(value, int): + self.c = (Fp(value), Fp(0), Fp(0), Fp(0), Fp(0)) + elif isinstance(value, Fp): + self.c = (value, Fp(0), Fp(0), Fp(0), Fp(0)) + else: + assert len(value) == 5 + self.c = tuple(value) def __add__(self, o): if isinstance(o, Fp): @@ -110,13 +112,8 @@ def inv(self) -> "EF": return result -ZERO = EF([Fp(0)] * 5) -ONE = EF.from_base(Fp(1)) - - -def fb(v: int) -> EF: - """`EF` lift of an integer base-field element.""" - return EF.from_base(Fp(v)) +ZERO = EF(0) +ONE = EF(1) def ef_sum(terms) -> EF: diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 008db0591..2d136a9a1 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -451,7 +451,7 @@ def verify_stir_challenges( def pack_answers(leaf: list[Fp]) -> list[EF]: if round_index == 0: - return [EF.from_base(f) for f in leaf] + return [EF(f) for f in leaf] return [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] constraints: list[SparseStatement] = [] @@ -460,7 +460,7 @@ def pack_answers(leaf: list[Fp]) -> list[EF]: if not merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path): raise ProofError("Merkle verification failed") fold = eval_multilinear_evals(pack_answers(op.leaf_data), folding_randomness) - ef_pt = fb(pow(int(gen.value), idx, P)) + ef_pt = EF(pow(int(gen.value), idx, P)) constraints.append(SparseStatement.dense(expand_from_univariate(ef_pt, num_variables), fold)) return constraints @@ -673,7 +673,7 @@ def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: if name == "execution": # PC column: pin first row to STARTING_PC, last row to ending_pc. for idx, pc in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: - out.append(SparseStatement.unique_value(stacked_n_vars, offset + (col_pc << n_vars) + idx, fb(pc))) + out.append(SparseStatement.unique_value(stacked_n_vars, offset + (col_pc << n_vars) + idx, EF(pc))) for point, eq_values, next_values in committed_statements[name]: if next_values: out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values, col_base))) @@ -722,7 +722,7 @@ def from_end(seq: Sequence, n: int) -> list: def mle_of_01234567_etc(point: Sequence[EF]) -> EF: """MLE of `f(i) = i` (big-endian) at `point`.""" n = len(point) - return ef_sum(p * fb(1 << (n - 1 - i)) for i, p in enumerate(point)) + return ef_sum(p * EF(1 << (n - 1 - i)) for i, p in enumerate(point)) def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: @@ -743,7 +743,7 @@ def finger_print(discriminator: Fp, data: Sequence[EF], alphas_eq_poly: Sequence """`Σᵢ αᵢ · dataᵢ + α_last · discriminator`.""" assert len(alphas_eq_poly) > len(data) acc = ef_sum(a * d for a, d in zip(alphas_eq_poly, data)) - return acc + alphas_eq_poly[-1] * EF.from_base(discriminator) + return acc + alphas_eq_poly[-1] * EF(discriminator) def sort_tables_by_height(table_log_heights: dict[str, int]) -> list[tuple[str, int]]: @@ -821,7 +821,7 @@ def pref_at(offset: int, log_height: int) -> EF: fp_byte = ( bytecode_value * correction + mle_of_01234567_etc(byte_pt) * alphas_eq_poly[n_instr_cols] - + alphas_eq_poly[-1] * EF.from_base(ds_byte) + + alphas_eq_poly[-1] * EF(ds_byte) ) num = num - pref * value_bytecode_acc den = ( @@ -884,7 +884,7 @@ def pref_at(offset: int, log_height: int) -> EF: if val_fresh: table_values[val_col] = next(evals) pref = pref_at(offset_within_table, log_n_rows) - fp = finger_print(ds_mem, [table_values[idx_col] + fb(i), table_values[val_col]], alphas_eq_poly) + fp = finger_print(ds_mem, [table_values[idx_col] + EF(i), table_values[val_col]], alphas_eq_poly) num = num + pref # Push direction den = den + pref * (c - fp) offset_within_table += row_stride @@ -972,8 +972,8 @@ def _eval_air_execution(folder: ConstraintFolder, extra_data: dict) -> None: nu_c = flag_c * operand_c + nfc * value_c + flag_c_fp * (fp + operand_c) # aux ∈ {0,1,2}: 0=nothing, 1=add, 2=deref. - add = aux * fb(2) - aux * aux - deref = aux * (aux - ONE) * EF.from_base(_INV_TWO) + add = aux * EF(2) - aux * aux + deref = aux * (aux - ONE) * EF(_INV_TWO) is_precompile = ONE - add - mul - deref - jump az = folder.assert_zero @@ -1020,11 +1020,11 @@ def _eval_air_extension_op(folder: ConstraintFolder, extra_data: dict) -> None: comp_sh = s[8:13] aux = ( - is_be * fb(_EXT_OP_FLAG_IS_BE) - + flag_add * fb(_EXT_OP_FLAG_ADD) - + flag_mul * fb(_EXT_OP_FLAG_MUL) - + flag_poly_eq * fb(_EXT_OP_FLAG_POLY_EQ) - + len_col * fb(_EXT_OP_LEN_MULTIPLIER) + is_be * EF(_EXT_OP_FLAG_IS_BE) + + flag_add * EF(_EXT_OP_FLAG_ADD) + + flag_mul * EF(_EXT_OP_FLAG_MUL) + + flag_poly_eq * EF(_EXT_OP_FLAG_POLY_EQ) + + len_col * EF(_EXT_OP_LEN_MULTIPLIER) ) _eval_bus_virtual(folder, extra_data, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res]) @@ -1059,8 +1059,8 @@ def _eval_air_extension_op(folder: ConstraintFolder, extra_data: dict) -> None: ]: folder.assert_zero(not_start_sh * (x - y)) - folder.assert_zero(not_start_sh * (idx_a_sh - idx_a - (is_be + is_ee * fb(5)))) - folder.assert_zero(not_start_sh * (idx_b_sh - idx_b - fb(5))) + folder.assert_zero(not_start_sh * (idx_a_sh - idx_a - (is_be + is_ee * EF(5)))) + folder.assert_zero(not_start_sh * (idx_b_sh - idx_b - EF(5))) folder.assert_zero(start_sh * (len_col - ONE)) @@ -1184,14 +1184,14 @@ def take(n: int) -> list[EF]: outputs_left, outputs_right = take(W // 2), take(W // 2) discriminator = ( - fb(_POSEIDON_DISCRIMINATOR_BASE) - + flag_permute * fb(_POSEIDON_PERMUTE_SHIFT) - + flag_half_output * fb(_POSEIDON_HALF_OUTPUT_SHIFT) - + flag_hardcoded_left * fb(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) - + flag_hardcoded_left * offset_hardcoded_left * fb(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT) + EF(_POSEIDON_DISCRIMINATOR_BASE) + + flag_permute * EF(_POSEIDON_PERMUTE_SHIFT) + + flag_half_output * EF(_POSEIDON_HALF_OUTPUT_SHIFT) + + flag_hardcoded_left * EF(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) + + flag_hardcoded_left * offset_hardcoded_left * EF(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT) ) not_hcl = ONE - flag_hardcoded_left - index_a = eff_idx_left_second - not_hcl * fb(_HALF_DIGEST_LEN) + index_a = eff_idx_left_second - not_hcl * EF(_HALF_DIGEST_LEN) _eval_bus_virtual(folder, extra_data, multiplicity, discriminator, [index_a, index_b, index_res]) for f in (multiplicity, flag_half_output, flag_hardcoded_left, flag_permute): @@ -1339,7 +1339,7 @@ def verify_execution( public_memory = padd_with_zero_to_next_power_of_two(public_input) pm_point = state.sample_vec(log2_strict_usize(len(public_memory))) - pm_eval = eval_multilinear_evals([EF.from_base(f) for f in public_memory], pm_point) + pm_eval = eval_multilinear_evals([EF(f) for f in public_memory], pm_point) bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size previous = [ From 94a31fd5e6cbd11ccfbc95c216db0f34a7ea1023 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 25 May 2026 17:18:24 +0400 Subject: [PATCH 35/69] wip --- crates/lean_prover/primitives.py | 17 ++++------------- crates/lean_prover/verifier.py | 27 ++++++++++++++------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index c8afa0e16..781edc282 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -72,6 +72,8 @@ def __init__(self, value): self.c = tuple(value) def __add__(self, o): + if isinstance(o, int): + return self if o == 0 else self + EF(o) if isinstance(o, Fp): return EF([self.c[0] + o, *self.c[1:]]) return EF([a + b for a, b in zip(self.c, o.c)]) @@ -87,6 +89,8 @@ def __neg__(self): __radd__ = __add__ def __mul__(self, o): + if isinstance(o, int): + return self if o == 1 else self * EF(o) if isinstance(o, Fp): return EF([a * o for a in self.c]) return EF(quintic_mul(self.c, o.c, Fp(0))) @@ -116,19 +120,6 @@ def inv(self) -> "EF": ONE = EF(1) -def ef_sum(terms) -> EF: - """Sum of an iterable of `EF` (empty -> `ZERO`).""" - return sum(terms, ZERO) - - -def ef_prod(factors) -> EF: - """Product of an iterable of `EF` (empty -> `ONE`).""" - acc = ONE - for f in factors: - acc = acc * f - return acc - - # Plonky3 width-16 circulant MDS first row. MDS_FIRST_ROW_16: Final = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 2d136a9a1..8fcc61efd 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -13,6 +13,7 @@ from __future__ import annotations import functools +import math from dataclasses import dataclass from typing import Sequence @@ -226,7 +227,7 @@ def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: """`Π (1 − a_i − b_i + 2·a_i·b_i)`.""" assert len(a) == len(b) - return ef_prod(ONE + x * y + x * y - x - y for x, y in zip(a, b)) + return math.prod(ONE + x * y + x * y - x - y for x, y in zip(a, b)) def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: @@ -239,8 +240,8 @@ def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: low_suffix = [ONE] * (n + 1) for i in range(n - 1, -1, -1): low_suffix[i] = low_suffix[i + 1] * x[i] * (ONE - y[i]) - s = ef_sum(eq_prefix[i] * (ONE - x[i]) * y[i] * low_suffix[i + 1] for i in range(n)) - return s + ef_prod([*x, *y]) + s = sum(eq_prefix[i] * (ONE - x[i]) * y[i] * low_suffix[i + 1] for i in range(n)) + return s + math.prod([*x, *y]) def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: @@ -410,7 +411,7 @@ def verify_sumcheck( point: list[EF] = [] for _ in range(n_vars): coeffs = state.next_extension_scalars_vec(degree + 1) - s = coeffs[0] + ef_sum(coeffs) + s = coeffs[0] + sum(coeffs) if s != target: raise ProofError("Sumcheck identity failed: h(0) + h(1) != target") state.check_pow_grinding(pow_bits) @@ -487,7 +488,7 @@ def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatement common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly_outside(smt.point, inner_pt) sel_n = smt.selector_num_variables for v in smt.values: - lagrange = ef_prod(pt[j] if (v[0] >> (sel_n - 1 - j)) & 1 else ONE - pt[j] for j in range(sel_n)) + lagrange = math.prod(pt[j] if (v[0] >> (sel_n - 1 - j)) & 1 else ONE - pt[j] for j in range(sel_n)) value = value + lagrange * common * randomness[i] i += 1 assert i == len(randomness) @@ -691,7 +692,7 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] nums = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) dens = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) - quotient = ef_sum(n * d.inv() for n, d in zip(nums, dens)) + quotient = sum(n * d.inv() for n, d in zip(nums, dens)) point = state.sample_vec(N_VARS_TO_SEND_GKR_COEFFS) claim_num = eval_multilinear_evals(nums, point) @@ -722,7 +723,7 @@ def from_end(seq: Sequence, n: int) -> list: def mle_of_01234567_etc(point: Sequence[EF]) -> EF: """MLE of `f(i) = i` (big-endian) at `point`.""" n = len(point) - return ef_sum(p * EF(1 << (n - 1 - i)) for i, p in enumerate(point)) + return sum(p * EF(1 << (n - 1 - i)) for i, p in enumerate(point)) def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: @@ -742,7 +743,7 @@ def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: def finger_print(discriminator: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: """`Σᵢ αᵢ · dataᵢ + α_last · discriminator`.""" assert len(alphas_eq_poly) > len(data) - acc = ef_sum(a * d for a, d in zip(alphas_eq_poly, data)) + acc = sum(a * d for a, d in zip(alphas_eq_poly, data)) return acc + alphas_eq_poly[-1] * EF(discriminator) @@ -817,7 +818,7 @@ def pref_at(offset: int, log_height: int) -> EF: pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() bytecode_value = eval_mle_base_at_ef(bytecode_multilinear, list(byte_pt) + list(from_end(alphas, log_instr))) - correction = ef_prod(ONE - a for a in alphas[: len(alphas) - log_instr]) + correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( bytecode_value * correction + mle_of_01234567_etc(byte_pt) * alphas_eq_poly[n_instr_cols] @@ -940,7 +941,7 @@ def _eval_bus_virtual( alphas: list[EF] = extra_data["logup_alphas_eq_poly"] assert len(data) < len(alphas) folder.assert_zero(multiplicity) - encoded = ef_sum(a * d for a, d in zip(alphas, data)) + alphas[-1] * discriminator + encoded = sum(a * d for a, d in zip(alphas, data)) + alphas[-1] * discriminator folder.assert_zero(encoded) @@ -1103,7 +1104,7 @@ def _p1c() -> dict: def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: - return [ef_sum(s * m for s, m in zip(state, row)) for row in mat] + return [sum(s * m for s, m in zip(state, row)) for row in mat] def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: @@ -1137,7 +1138,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: if r < n_partial - 1: state[0] = state[0] + const["sparse_scalar_rc"][r] old_s0 = state[0] - state[0] = ef_sum(state[j] * const["sparse_first_row"][r][j] for j in range(_POSEIDON_WIDTH)) + state[0] = sum(state[j] * const["sparse_first_row"][r][j] for j in range(_POSEIDON_WIDTH)) for i in range(1, _POSEIDON_WIDTH): state[i] = state[i] + old_s0 * const["sparse_v"][r][i - 1] @@ -1326,7 +1327,7 @@ def verify_execution( constraint_eval = air_constraint_eval(meta, col_evals, alpha_slice, extra_data) natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] - k_t = ef_prod(sc_point[: n_max - log_n_rows]) + k_t = math.prod(sc_point[: n_max - log_n_rows]) my_air_final = ( my_air_final + k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval ) From 3b1535334f08dad4e4eb6a6750d4e04f79061f93 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 25 May 2026 17:53:03 +0400 Subject: [PATCH 36/69] wip --- crates/lean_prover/primitives.py | 40 ++++++-------------------------- crates/lean_prover/verifier.py | 2 +- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index 781edc282..9a262ee81 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -1,13 +1,14 @@ -# extracted from: https://github.com/leanEthereum/leanSpec +# source: https://github.com/leanEthereum/leanSpec from __future__ import annotations from typing import Final, Sequence -P: Final = 2**31 - 2**24 + 1 -"""KoalaBear prime: `2^31 - 2^24 + 1`.""" +P: Final = 2**31 - 2**24 + 1 # Koalabear prime +TWO_ADICITY = 24 +MDS_FIRST_ROW_16: Final = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) # for Poseidon +KB_TWO_ADIC_GENERATORS: Final = tuple(pow(0x6AC49F88, 1 << (TWO_ADICITY - b), P) for b in range(TWO_ADICITY + 1)) -BASE_TWO_ADICITY = 24 class Fp: """An element of the KoalaBear prime field `F_p`.""" @@ -43,8 +44,7 @@ def __repr__(self) -> str: def quintic_mul(a, b, zero): - """Schoolbook product in `Fp[X]/(X⁵+X²−1)`. `a`, `b` and the result are length-5 - coefficient lists over any ring sharing the additive identity `zero`.""" + """Schoolbook product in `Fp[X]/(X⁵+X²−1)`""" prod = [zero] * 9 for i in range(5): for j in range(5): @@ -120,20 +120,6 @@ def inv(self) -> "EF": ONE = EF(1) -# Plonky3 width-16 circulant MDS first row. -MDS_FIRST_ROW_16: Final = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) - - -# KoalaBear two-adic generators: index `b` is a primitive 2^b-th root of unity. -# Built by repeatedly squaring `g[24]` (the canonical generator of the order-2^24 -# subgroup): `g[b] = g[b+1]^2 mod P`. Yields `g[1] = -1` and `g[0] = 1`. -KB_TWO_ADIC_GENERATORS: list[int] = [1] * 25 -KB_TWO_ADIC_GENERATORS[24] = 0x6AC49F88 -for _b in range(23, -1, -1): - KB_TWO_ADIC_GENERATORS[_b] = pow(KB_TWO_ADIC_GENERATORS[_b + 1], 2, P) -del _b - - # 448 raw Poseidon1-KoalaBear width-16 round constants generated by the Grain # LFSR (Poseidon paper §5.3, parameters field_type=1, α=3, n=31, t=16, R_F=8, # R_P=20). Reference: https://github.com/Plonky3/Plonky3/blob/main/poseidon1/generate_constants.py @@ -256,18 +242,7 @@ def mds_mul() -> None: # --------------------------------------------------------------------------- -# Plonky3 / HorizenLabs partial-round optimization for the AIR. -# -# The AIR circuit verifies the Poseidon1 permutation in a more compact form: for -# the 20 partial rounds, the dense MDS multiply is replaced by a single -# precomputed transition matrix `m_i` (applied once) plus, per round, a -# rank-1-style update parameterized by `sparse_first_row[r]` and `sparse_v[r]`. -# Round constants are similarly compressed into `sparse_first_round_constants` -# (16 elements, added once) and `sparse_scalar_round_constants` (R_P - 1 -# scalars, added to position 0 between rounds). -# -# Algorithm follows `crates/backend/koala-bear/src/poseidon1_koalabear_16.rs` -# (`compute_equivalent_matrices`, `equivalent_round_constants`). +# sparse partial-round optimization for the AIR. # --------------------------------------------------------------------------- @@ -370,5 +345,4 @@ def _compute_air_sparse_constants() -> dict: } -# Precomputed once at module load. Consumed by `verifier._p1c()`. POSEIDON1_AIR_CONSTANTS = _compute_air_sparse_constants() diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 8fcc61efd..6a270b61a 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1262,7 +1262,7 @@ def verify_execution( + sum(t.n_columns << table_log_heights[t.name] for t in tables) ) stacked_n_vars = log2_ceil_usize(total_stacked) - if stacked_n_vars > BASE_TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: + if stacked_n_vars > TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") cfg = whir_config(log_inv_rate, stacked_n_vars) nood = cfg["commitment_ood_samples"] From 57a4b1c46f98ae28f0ee3365848e78d44ab126a5 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 25 May 2026 19:16:14 +0400 Subject: [PATCH 37/69] w --- crates/lean_prover/primitives.py | 23 +++++ crates/lean_prover/verifier.py | 143 ++++++++++--------------------- 2 files changed, 69 insertions(+), 97 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index 9a262ee81..472c5a059 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -9,6 +9,8 @@ MDS_FIRST_ROW_16: Final = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) # for Poseidon KB_TWO_ADIC_GENERATORS: Final = tuple(pow(0x6AC49F88, 1 << (TWO_ADICITY - b), P) for b in range(TWO_ADICITY + 1)) +SPONGE_RATE, SPONGE_STATE, DIGEST_ELEMS = 8, 16, 8 +SPONGE_CAPACITY = SPONGE_STATE - SPONGE_RATE class Fp: """An element of the KoalaBear prime field `F_p`.""" @@ -241,6 +243,27 @@ def mds_mul() -> None: """Poseidon1 parameters for width-16 (8 full rounds, 20 partial).""" +POSEIDON16 = Poseidon1(PARAMS_16) + +def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: + state = list(left) + list(right) + assert len(state) == SPONGE_STATE + return [a + b for a, b in zip(POSEIDON16.permute(state), state)][:DIGEST_ELEMS] + + +def log2_ceil_usize(x: int) -> int: + return 0 if x <= 1 else (x - 1).bit_length() + + +def log2_strict_usize(x: int) -> int: + assert x > 0 and (x & (x - 1)) == 0, f"{x} is not a power of two" + return x.bit_length() - 1 + + +def next_multiple_of(n: int, k: int) -> int: + return (n + k - 1) // k * k + + # --------------------------------------------------------------------------- # sparse partial-round optimization for the AIR. # --------------------------------------------------------------------------- diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 6a270b61a..e9f6700ef 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1,87 +1,61 @@ """Pure-Python verifier for leanVM proofs. - -Setup (one-time): +Setup the test vector (one-time): cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture - Run: python3 crates/lean_prover/verifier.py - Format: ruff format --line-length 120 crates/lean_prover/verifier.py """ from __future__ import annotations - import functools import math from dataclasses import dataclass from typing import Sequence - from primitives import * -WHIR_INITIAL_FOLDING_FACTOR, WHIR_SUBSEQUENT_FOLDING_FACTOR = 7, 5 -MAX_NUM_VARIABLES_TO_SEND_COEFFS = 8 -RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 5 -SPONGE_RATE, SPONGE_STATE, DIGEST_ELEMS = 8, 16, 8 -SPNGE_CAPACITY = SPONGE_STATE - SPONGE_RATE + PUBLIC_INPUT_SIZE = DIGEST_ELEMS SNARK_DOMAIN_SEP = [Fp(v) for v in (130704175, 1303721200, 493664240, 1035493700, 2063844858, 1410214009, 1938905908, 1696767928)] # fmt: skip -MIN_WHIR_LOG_INV_RATE, MAX_WHIR_LOG_INV_RATE = 1, 4 +WHIR_INITIAL_FOLDING_FACTOR, WHIR_SUBSEQUENT_FOLDING_FACTOR, WHIR_MAX_NUM_VARIABLES_TO_SEND_COEFFS = 7, 5, 8 +MIN_WHIR_LOG_INV_RATE, MAX_WHIR_LOG_INV_RATE, RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 1, 4, 5 +# (log_inv_rate, num_variables, commitment_ood_samples, starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds) +# where `rounds = ((num_queries, ood_samples, query_pow_bits, folding_pow_bits), ...)`. (checked by `cargo test -p lean_prover --test check_whir_configs`) +WHIR_CONFIGS = ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # fmt: skip + MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, MAX_BYTECODE_LOG_SIZE = 8, 8, 22 MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension_op": 21, "poseidon16_compress": 21} ALL_TABLES_ORDER = ("execution", "extension_op", "poseidon16_compress") -# (log_inv_rate, num_variables, commitment_ood_samples, starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds) -# where `rounds = ((num_queries, ood_samples, query_pow_bits, folding_pow_bits), ...)`. (checked by `cargo test -p lean_prover --test check_whir_configs`) -WHIR_CONFIGS = ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # fmt: skip - class ProofError(Exception): pass -_POSEIDON16 = Poseidon1(PARAMS_16) - - -def poseidon16_compress_in_place(state: list[Fp]) -> list[Fp]: - assert len(state) == SPONGE_STATE - return [a + b for a, b in zip(_POSEIDON16.permute(state), state)] - - -def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: - return poseidon16_compress_in_place(list(left) + list(right))[:DIGEST_ELEMS] - - -def hash_slice(data: Sequence[Fp]) -> list[Fp]: - """RTL sponge absorb of `data` (RATE-multiple length) into IV `[len(data), 0, ..., 0]`.""" +# T-Sponge (compression instead of permutation) with replacement (instead of xoring / adding the ingested data). +def sponge_hash(data: Sequence[Fp]) -> list[Fp]: assert len(data) % SPONGE_RATE == 0 and len(data) > 0 - state = [Fp(len(data))] + [Fp(0)] * (SPONGE_STATE - 1) - for k in range(len(data) // SPONGE_RATE - 1, -1, -1): - state = poseidon16_compress_in_place(state[:SPNGE_CAPACITY] + list(data[k * SPONGE_RATE : (k + 1) * SPONGE_RATE])) - return state[:DIGEST_ELEMS] + state = [Fp(len(data))] + [Fp(0)] * (SPONGE_CAPACITY - 1) + for k in range(len(data) // SPONGE_RATE): + state = poseidon16_compress(state, data[k * SPONGE_RATE : (k + 1) * SPONGE_RATE]) + return state def fiat_shamir_domain_sep(bytecode_hash: Sequence[Fp]) -> list[Fp]: - """Domain-separator absorbed before the proof. Mixes the bytecode hash and - `PUBLIC_INPUT_SIZE` (mirrors `lean_prover::fiat_shamir_domain_sep`).""" tail = [Fp(PUBLIC_INPUT_SIZE)] + [Fp(0)] * (SPONGE_RATE - 1) - extended = poseidon16_compress(SNARK_DOMAIN_SEP, tail) - return poseidon16_compress(bytecode_hash, extended) + return poseidon16_compress(bytecode_hash, poseidon16_compress(SNARK_DOMAIN_SEP, tail)) -class Challenger: - """Duplex-sponge Fiat-Shamir. `observe(chunk)` permutes `state[:CAPACITY] || chunk`; - `_sample_rate()` reads `state[CAPACITY:]` once per `duplex()`.""" - +class Challenger: # https://eprint.iacr.org/2025/536.pdf def __init__(self) -> None: self.state: list[Fp] = [Fp(0)] * SPONGE_STATE self.rate_fresh: bool = False def observe(self, chunk: Sequence[Fp]) -> None: assert len(chunk) == SPONGE_RATE - self.state = _POSEIDON16.permute(self.state[:SPNGE_CAPACITY] + list(chunk)) + self.state = POSEIDON16.permute(self.state[:SPONGE_CAPACITY] + list(chunk)) self.rate_fresh = True def observe_many(self, scalars: Sequence[Fp]) -> None: @@ -96,7 +70,7 @@ def duplex(self) -> None: def _sample_rate(self) -> list[Fp]: assert self.rate_fresh, "stale rate — insert duplex() before sampling" self.rate_fresh = False - return list(self.state[SPNGE_CAPACITY:]) + return list(self.state[SPONGE_CAPACITY:]) def _sample_many(self, n: int) -> list[Fp]: out: list[Fp] = [] @@ -106,12 +80,12 @@ def _sample_many(self, n: int) -> list[Fp]: out.extend(self._sample_rate()) return out - def sample_vec(self, n: int) -> list[EF]: + def sample_many_ef(self, n: int) -> list[EF]: flat = self._sample_many((n * EF.DIMENSION + SPONGE_RATE - 1) // SPONGE_RATE)[: n * EF.DIMENSION] return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] - def sample(self) -> EF: - return self.sample_vec(1)[0] + def sample_ef(self) -> EF: + return self.sample_many_ef(1)[0] def sample_in_range(self, bits: int, n_samples: int) -> list[int]: assert bits < 31 @@ -132,18 +106,14 @@ class Proof: class VerifierState(Challenger): - """Challenger + transcript reader. `n` scalars are consumed as `next_multiple_of(n, RATE)` - raw scalars (trailing must be zero) and absorbed.""" - def __init__(self, proof: Proof) -> None: super().__init__() self.transcript = list(proof.transcript) - self.openings = list(proof.merkle_openings) + self.openings = list(reversed(proof.merkle_openings)) self.offset = 0 - self.open_idx = 0 def _read_padded(self, n: int) -> list[Fp]: - n_pad = -(-n // SPONGE_RATE) * SPONGE_RATE + n_pad = next_multiple_of(n, SPONGE_RATE) if self.offset + n_pad > len(self.transcript): raise ProofError("ExceededTranscript") chunk = self.transcript[self.offset : self.offset + n_pad] @@ -167,38 +137,18 @@ def next_extension_scalar(self) -> EF: return self.next_extension_scalars_vec(1)[0] def next_merkle_opening(self) -> MerkleOpening: - if self.open_idx >= len(self.openings): + if not self.openings: raise ProofError("ExceededTranscript: no more Merkle openings") - self.open_idx += 1 - return self.openings[self.open_idx - 1] + return self.openings.pop() def check_pow_grinding(self, bits: int) -> None: if bits == 0: return self._read_padded(1) - if int(self.state[SPNGE_CAPACITY].value) & ((1 << bits) - 1) != 0: + if int(self.state[SPONGE_CAPACITY].value) & ((1 << bits) - 1) != 0: raise ProofError("InvalidGrindingWitness") -_INV_TWO = Fp(pow(2, P - 2, P)) - - -def log2_ceil_usize(x: int) -> int: - return 0 if x <= 1 else (x - 1).bit_length() - - -def log2_strict_usize(x: int) -> int: - assert x > 0 and (x & (x - 1)) == 0, f"{x} is not a power of two" - return x.bit_length() - 1 - - -def padd_with_zero_to_next_power_of_two(values: Sequence[Fp]) -> list[Fp]: - if not values: - return [Fp(0)] - n = 1 << log2_ceil_usize(len(values)) - return list(values) + [Fp(0)] * (n - len(values)) - - def merkle_verify_path( commit: list[Fp], log_height: int, @@ -208,7 +158,8 @@ def merkle_verify_path( ) -> bool: if len(opening_proof) != log_height: return False - cur = hash_slice(list(opened_values)) + chunks = [list(opened_values[i : i + SPONGE_RATE]) for i in range(0, len(opened_values), SPONGE_RATE)] + cur = sponge_hash([x for c in reversed(chunks) for x in c]) for sibling in opening_proof: cur = poseidon16_compress(cur, sibling) if index & 1 == 0 else poseidon16_compress(sibling, cur) index >>= 1 @@ -341,9 +292,9 @@ def whir_folding_factor_at_round(r: int) -> int: def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: nv = num_variables - WHIR_INITIAL_FOLDING_FACTOR - if nv < MAX_NUM_VARIABLES_TO_SEND_COEFFS: + if nv < WHIR_MAX_NUM_VARIABLES_TO_SEND_COEFFS: return 0, nv - n = -(-(nv - MAX_NUM_VARIABLES_TO_SEND_COEFFS) // WHIR_SUBSEQUENT_FOLDING_FACTOR) + n = -(-(nv - WHIR_MAX_NUM_VARIABLES_TO_SEND_COEFFS) // WHIR_SUBSEQUENT_FOLDING_FACTOR) return n, nv - n * WHIR_SUBSEQUENT_FOLDING_FACTOR @@ -415,7 +366,7 @@ def verify_sumcheck( if s != target: raise ProofError("Sumcheck identity failed: h(0) + h(1) != target") state.check_pow_grinding(pow_bits) - r = state.sample() + r = state.sample_ef() point.append(r) target = _eval_univariate(coeffs, r) return point, target @@ -423,7 +374,7 @@ def verify_sumcheck( def combine_constraints(state: VerifierState, target: EF, constraints: list[SparseStatement]) -> tuple[EF, list[EF]]: """Fold all constraint values into `target` via powers of γ; return those γ-power weights.""" - gamma: EF = state.sample() + gamma: EF = state.sample_ef() combo: list[EF] = [] g = ONE for smt in constraints: @@ -532,7 +483,7 @@ def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None new_commitment = ParsedCommitment( nvars_round, state.next_base_scalars_vec(DIGEST_ELEMS), - state.sample_vec(nood), + state.sample_many_ef(nood), state.next_extension_scalars_vec(nood), ) stir = verify_stir_challenges( @@ -694,19 +645,19 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] dens = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) quotient = sum(n * d.inv() for n, d in zip(nums, dens)) - point = state.sample_vec(N_VARS_TO_SEND_GKR_COEFFS) + point = state.sample_many_ef(N_VARS_TO_SEND_GKR_COEFFS) claim_num = eval_multilinear_evals(nums, point) claim_den = eval_multilinear_evals(dens, point) for layer_n_vars in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): state.duplex() - alpha = state.sample() + alpha = state.sample_ef() raw_pt, sc_value = verify_sumcheck(state, claim_num + alpha * claim_den, layer_n_vars, 3) sc_point = list(reversed(raw_pt)) nl, nr, dl, dr = state.next_extension_scalars_vec(4) if sc_value != eq_poly_outside(point, sc_point) * (alpha * dl * dr + nl * dr + nr * dl): raise ProofError("GKR step: postponed value mismatch") - beta = state.sample() + beta = state.sample_ef() one_minus = ONE - beta claim_num = one_minus * nl + beta * nr claim_den = one_minus * dl + beta * dr @@ -974,7 +925,7 @@ def _eval_air_execution(folder: ConstraintFolder, extra_data: dict) -> None: # aux ∈ {0,1,2}: 0=nothing, 1=add, 2=deref. add = aux * EF(2) - aux * aux - deref = aux * (aux - ONE) * EF(_INV_TWO) + deref = aux * (aux - ONE) * EF((P + 1) // 2) # (P+1)/2 is the inverse of 2 mod P is_precompile = ONE - add - mul - deref - jump az = folder.assert_zero @@ -1269,13 +1220,13 @@ def verify_execution( parsed_commitment = ParsedCommitment( stacked_n_vars, state.next_base_scalars_vec(DIGEST_ELEMS), - state.sample_vec(nood), + state.sample_many_ef(nood), state.next_extension_scalars_vec(nood), ) - logup_c = state.sample() + logup_c = state.sample_ef() state.duplex() - logup_alphas = state.sample_vec(constants["log_max_bus_width"]) + logup_alphas = state.sample_many_ef(constants["log_max_bus_width"]) logup_alphas_eq = eval_eq(logup_alphas) logup = verify_generic_logup( state, @@ -1290,7 +1241,7 @@ def verify_execution( ) gkr_point = logup["gkr_point"] - air_alpha = state.sample() + air_alpha = state.sample_ef() # AIR alpha powers/offsets are laid out in canonical ALL_TABLES order # (mirrors `for table in ALL_TABLES { alpha_offset += n_constraints }` in verify_execution.rs). @@ -1338,9 +1289,9 @@ def verify_execution( if my_air_final != sc_value: raise ProofError("AIR sumcheck: claimed value mismatch") - public_memory = padd_with_zero_to_next_power_of_two(public_input) - pm_point = state.sample_vec(log2_strict_usize(len(public_memory))) - pm_eval = eval_multilinear_evals([EF(f) for f in public_memory], pm_point) + assert len(public_input) % DIGEST_ELEMS == 0 + pm_point = state.sample_many_ef(log2_strict_usize(len(public_input))) + pm_eval = eval_multilinear_evals([EF(f) for f in public_input], pm_point) bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size previous = [ @@ -1370,10 +1321,8 @@ def verify_execution( raise ProofError( f"InvalidProof: transcript not fully consumed ({state.offset}/{len(state.transcript)} scalars read)" ) - if state.open_idx != len(state.openings): - raise ProofError( - f"InvalidProof: Merkle openings not fully consumed ({state.open_idx}/{len(state.openings)} openings used)" - ) + if state.openings: + raise ProofError(f"InvalidProof: {len(state.openings)} Merkle openings unused") return {"log_inv_rate": log_inv_rate, "log_memory": log_memory, "stacked_n_vars": stacked_n_vars} From 7826f361a32caf4d84b1797695599ab7bc58c6fc Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 01:55:29 +0400 Subject: [PATCH 38/69] wip --- crates/lean_prover/verifier.py | 55 ++++++++++++++-------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index e9f6700ef..8984bc18e 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -11,6 +11,7 @@ import functools import math from dataclasses import dataclass +from itertools import accumulate, repeat from typing import Sequence from primitives import * @@ -44,8 +45,7 @@ def sponge_hash(data: Sequence[Fp]) -> list[Fp]: def fiat_shamir_domain_sep(bytecode_hash: Sequence[Fp]) -> list[Fp]: - tail = [Fp(PUBLIC_INPUT_SIZE)] + [Fp(0)] * (SPONGE_RATE - 1) - return poseidon16_compress(bytecode_hash, poseidon16_compress(SNARK_DOMAIN_SEP, tail)) + return poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP) class Challenger: # https://eprint.iacr.org/2025/536.pdf @@ -167,36 +167,25 @@ def merkle_verify_path( def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: - """`[x, x², x⁴, …, x^(2^(n−1))]`.""" - out, cur = [], x - for _ in range(num_variables): - out.append(cur) - cur = cur * cur - return out + return list(accumulate(repeat(x, num_variables), lambda a, _: a * a)) # [x, x², x⁴, …, x^(2^(n−1))] -def eq_poly_outside(a: Sequence[EF], b: Sequence[EF]) -> EF: - """`Π (1 − a_i − b_i + 2·a_i·b_i)`.""" +def eq_poly(a: Sequence[EF], b: Sequence[EF]) -> EF: assert len(a) == len(b) - return math.prod(ONE + x * y + x * y - x - y for x, y in zip(a, b)) + return math.prod(x*y + (ONE - x) * (ONE - y) for x, y in zip(a, b)) def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: - """Multilinear extension of `y = x + 1` (big-endian, mod `2^n`).""" assert len(x) == len(y) - n = len(x) - eq_prefix = [ONE] - for i in range(n): - eq_prefix.append(eq_prefix[i] * (x[i] * y[i] + (ONE - x[i]) * (ONE - y[i]))) - low_suffix = [ONE] * (n + 1) - for i in range(n - 1, -1, -1): - low_suffix[i] = low_suffix[i + 1] * x[i] * (ONE - y[i]) - s = sum(eq_prefix[i] * (ONE - x[i]) * y[i] * low_suffix[i + 1] for i in range(n)) + s, eq_prefix = ZERO, ONE + for xi, yi in zip(x, y): + s = xi * (ONE - yi) * s + eq_prefix * (ONE - xi) * yi + eq_prefix *= xi * yi + (ONE - xi) * (ONE - yi) return s + math.prod([*x, *y]) -def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: - """Evaluate a multilinear in evaluation form at `point` (big-endian fold).""" +def eval_multilinear(evals: Sequence[EF], point: Sequence[EF]) -> EF: + """Evaluate a multilinear in evaluation form at `point`.""" assert len(evals) == 1 << len(point) cur = list(evals) for r in reversed(point): @@ -204,8 +193,8 @@ def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: return cur[0] -def eval_mle_base_at_ef(base_evals: Sequence[int], point: Sequence[EF]) -> EF: - """Evaluate a base-field multilinear in eval form at an EF point.""" +def eval_base_field_multilinear(base_evals: Sequence[int], point: Sequence[EF]) -> EF: + """Evaluate a base-field multilinear in evaluation form at `point`.""" assert len(base_evals) == 1 << len(point) # First fold: cur[n] = base[2n] + (base[2n+1] − base[2n]) · r. @@ -411,7 +400,7 @@ def pack_answers(leaf: list[Fp]) -> list[EF]: op = state.next_merkle_opening() if not merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path): raise ProofError("Merkle verification failed") - fold = eval_multilinear_evals(pack_answers(op.leaf_data), folding_randomness) + fold = eval_multilinear(pack_answers(op.leaf_data), folding_randomness) ef_pt = EF(pow(int(gen.value), idx, P)) constraints.append(SparseStatement.dense(expand_from_univariate(ef_pt, num_variables), fold)) return constraints @@ -436,7 +425,7 @@ def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatement i = 0 for smt in smts: inner_pt = pt[len(pt) - len(smt.point) :] - common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly_outside(smt.point, inner_pt) + common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly(smt.point, inner_pt) sel_n = smt.selector_num_variables for v in smt.values: lagrange = math.prod(pt[j] if (v[0] >> (sel_n - 1 - j)) & 1 else ONE - pt[j] for j in range(sel_n)) @@ -646,8 +635,8 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] quotient = sum(n * d.inv() for n, d in zip(nums, dens)) point = state.sample_many_ef(N_VARS_TO_SEND_GKR_COEFFS) - claim_num = eval_multilinear_evals(nums, point) - claim_den = eval_multilinear_evals(dens, point) + claim_num = eval_multilinear(nums, point) + claim_den = eval_multilinear(dens, point) for layer_n_vars in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): state.duplex() @@ -655,7 +644,7 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] raw_pt, sc_value = verify_sumcheck(state, claim_num + alpha * claim_den, layer_n_vars, 3) sc_point = list(reversed(raw_pt)) nl, nr, dl, dr = state.next_extension_scalars_vec(4) - if sc_value != eq_poly_outside(point, sc_point) * (alpha * dl * dr + nl * dr + nr * dl): + if sc_value != eq_poly(point, sc_point) * (alpha * dl * dr + nl * dr + nr * dl): raise ProofError("GKR step: postponed value mismatch") beta = state.sample_ef() one_minus = ONE - beta @@ -750,7 +739,7 @@ def pref_at(offset: int, log_height: int) -> EF: n_missing = total_gkr_n_vars - log_height idx = offset >> log_height bits = [ONE if (idx >> (n_missing - 1 - i)) & 1 else ZERO for i in range(n_missing)] - return eq_poly_outside(bits, point_gkr[:n_missing]) + return eq_poly(bits, point_gkr[:n_missing]) # Memory (data order: [value_index, value_memory] mirrors `crates/sub_protocols/src/logup.rs`). mem_pt = from_end(point_gkr, log_memory) @@ -768,7 +757,7 @@ def pref_at(offset: int, log_height: int) -> EF: pref = pref_at(offset, log_bytecode) pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() - bytecode_value = eval_mle_base_at_ef(bytecode_multilinear, list(byte_pt) + list(from_end(alphas, log_instr))) + bytecode_value = eval_base_field_multilinear(bytecode_multilinear, list(byte_pt) + list(from_end(alphas, log_instr))) correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( bytecode_value * correction @@ -1280,7 +1269,7 @@ def verify_execution( natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = math.prod(sc_point[: n_max - log_n_rows]) my_air_final = ( - my_air_final + k_t * eq_poly_outside(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval + my_air_final + k_t * eq_poly(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval ) eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} @@ -1291,7 +1280,7 @@ def verify_execution( assert len(public_input) % DIGEST_ELEMS == 0 pm_point = state.sample_many_ef(log2_strict_usize(len(public_input))) - pm_eval = eval_multilinear_evals([EF(f) for f in public_input], pm_point) + pm_eval = eval_multilinear([EF(f) for f in public_input], pm_point) bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size previous = [ From 40e02c147ad21f4102e7c248304e718f9f4b5737 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 04:00:34 +0400 Subject: [PATCH 39/69] w --- crates/lean_prover/verifier.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 8984bc18e..926d707cb 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -238,7 +238,7 @@ def eval_base_field_multilinear(base_evals: Sequence[int], point: Sequence[EF]) def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: - """`Σ_b c_b · Π_i x_i^(b_i)` in the standard multilinear coefficient basis.""" + """Evaluate a multilinear in coefficient form at `point`.""" assert len(coeffs) == 1 << len(point) if not point: return coeffs[0] @@ -250,9 +250,6 @@ def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: @dataclass class SparseStatement: - """Claim with multilinear `point` and `(selector, value)` pairs. `is_next` - swaps the eq-weight for `next_mle`.""" - total_num_variables: int point: list[EF] values: list[tuple[int, EF]] From ffa1bf349886ca57f4d2a8ccc1b04eb4e5b91de0 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 15:09:30 +0400 Subject: [PATCH 40/69] wip --- crates/lean_prover/src/test_zkvm.rs | 138 ++++++++++++-- crates/lean_prover/tests/dump_zkvm_vector.rs | 187 ------------------- crates/lean_prover/verifier.py | 61 +----- 3 files changed, 128 insertions(+), 258 deletions(-) delete mode 100644 crates/lean_prover/tests/dump_zkvm_vector.rs diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index 9d8acc068..821a75364 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -1,3 +1,5 @@ +use std::{collections::BTreeMap, io::Write}; + use crate::{default_whir_config, prove_execution::prove_execution, verify_execution::verify_execution}; use backend::*; use lean_compiler::*; @@ -5,15 +7,17 @@ use lean_vm::*; use rand::{RngExt, SeedableRng, rngs::StdRng}; use utils::{init_tracing, poseidon16_compress, poseidon16_permute}; -#[test] -fn test_zk_vm_all_precompiles() { - let program_str = r#" +const N: usize = 11; +const M: usize = 3; + +const ALL_PRECOMPILES_PROGRAM: &str = r#" DIM = 5 N = 11 M = 3 DIGEST_LEN = 8 HALF_DIGEST_LEN = 4 SCRATCH_SIZE = 8192 +LOOP_ITERS = LOOP_ITERS_PLACEHOLDER def main(): scratch = Array(SCRATCH_SIZE) @@ -80,16 +84,20 @@ def main(): poly_eq_ee(ext_a_ptr, ext_b_ptr, scratch + 1300, N) c: Mut = 0 - for i in range(0,100): + for i in range(0, LOOP_ITERS): c += 1 - assert c == 100 + assert c == LOOP_ITERS return "#; - const N: usize = 11; - const M: usize = 3; +fn all_precompiles_flags(loop_iters: usize) -> CompilationFlags { + CompilationFlags { + replacements: BTreeMap::from([("LOOP_ITERS_PLACEHOLDER".to_string(), loop_iters.to_string())]), + } +} +fn all_precompiles_witness() -> (Vec, ExecutionWitness) { let mut rng = StdRng::seed_from_u64(0); let mut scratch = F::zero_vec(8192); @@ -195,7 +203,93 @@ def main(): hints, ..Default::default() }; - test_zk_vm_helper_with_witness(program_str, &public_input, witness); + (public_input, witness) +} + +#[test] +fn test_zk_vm_all_precompiles() { + let (public_input, witness) = all_precompiles_witness(); + test_zk_vm_helper_with_witness( + ALL_PRECOMPILES_PROGRAM, + &public_input, + witness, + all_precompiles_flags(100), + ); +} + +#[test] +fn dump_test_vector_for_python_verifier() { + const LOOP_ITERS: usize = 5000; + + let (public_input, witness) = all_precompiles_witness(); + let bytecode = compile_program_with_flags( + &ProgramSource::Raw(ALL_PRECOMPILES_PROGRAM.to_string()), + all_precompiles_flags(LOOP_ITERS), + ); + let exec_proof = prove_execution(&bytecode, &public_input, &witness, &default_whir_config(1), false).unwrap(); + let n_cycles = exec_proof.metadata.as_ref().map_or(0, |m| m.cycles); + let (_details, raw_proof) = verify_execution(&bytecode, &public_input, exec_proof.proof).unwrap(); + + let f_u32 = |x: F| x.as_canonical_u32(); + let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".into())) + .join("zkvm_test_vectors"); + std::fs::create_dir_all(&out_dir).unwrap(); + + // Sidecar: raw u32-LE bytecode multilinear. + let mle_path = "proof.bytecode_mle.bin"; + let mut mle_file = std::fs::File::create(out_dir.join(mle_path)).unwrap(); + for v in &bytecode.instructions_multilinear { + mle_file.write_all(&f_u32(*v).to_le_bytes()).unwrap(); + } + + let tables_json: Vec = ALL_TABLES + .iter() + .map(|t| serde_json::json!({"name": t.name(), "n_columns":
::n_columns(t)})) + .collect(); + let opening_json = |o: &MerkleOpening| -> serde_json::Value { + serde_json::json!({ + "leaf_data": o.leaf_data.iter().map(|&f| f_u32(f)).collect::>(), + "path": o.path.iter().map(|d| d.map(f_u32)).collect::>(), + }) + }; + let out = serde_json::json!({ + "bytecode_log_size": bytecode.log_size(), + "bytecode_hash": bytecode.hash.map(f_u32), + "bytecode_multilinear_path": mle_path, + "bytecode_multilinear_len": bytecode.instructions_multilinear.len(), + "public_input": public_input.iter().map(|&f| f_u32(f)).collect::>(), + "n_tables": N_TABLES, + "tables": tables_json, + "constants": { + "n_instruction_columns": N_INSTRUCTION_COLUMNS, + "n_runtime_columns": N_RUNTIME_COLUMNS, + "col_pc": COL_PC, + "logup_memory_domainsep": LOGUP_MEMORY_DOMAINSEP, + "logup_bytecode_domainsep": LOGUP_BYTECODE_DOMAINSEP, + "log_max_bus_width": LOG_MAX_BUS_WIDTH, + "starting_pc": STARTING_PC, + "ending_pc": bytecode.ending_pc, + }, + "snark_domain_sep": crate::SNARK_DOMAIN_SEP.map(f_u32), + "n_cycles": n_cycles, + "proof": { + "transcript": raw_proof.transcript.iter().map(|&f| f_u32(f)).collect::>(), + "merkle_openings": raw_proof.merkle_openings.iter().map(opening_json).collect::>(), + }, + }); + let json_path = out_dir.join("proof.json"); + std::fs::write(&json_path, serde_json::to_string(&out).unwrap()).unwrap(); + + println!( + "wrote {} ({:.1} KiB), bytecode_log_size={}, n_cycles={} (~2^{:.2})", + json_path.display(), + json_path.metadata().unwrap().len() as f64 / 1024.0, + bytecode.log_size(), + n_cycles, + (n_cycles as f64).log2(), + ); } #[test] @@ -245,18 +339,34 @@ def fibonacci_const(a, b, n: Const): buff[j] = buff[j - 1] + buff[j - 2] return buff[n], buff[n + 1] "#; - let program_str = program_str.replace("FIB_N_PLACEHOLDER", &n.to_string()); - - test_zk_vm_helper(&program_str, &[F::ZERO; PUBLIC_INPUT_LEN]); + let flags = CompilationFlags { + replacements: [("FIB_N_PLACEHOLDER".to_string(), n.to_string())].into_iter().collect(), + }; + test_zk_vm_helper_with_witness( + program_str, + &[F::ZERO; PUBLIC_INPUT_LEN], + ExecutionWitness::default(), + flags, + ); } fn test_zk_vm_helper(program_str: &str, public_input: &[F]) { - test_zk_vm_helper_with_witness(program_str, public_input, ExecutionWitness::default()) + test_zk_vm_helper_with_witness( + program_str, + public_input, + ExecutionWitness::default(), + CompilationFlags::default(), + ) } -fn test_zk_vm_helper_with_witness(program_str: &str, public_input: &[F], witness: ExecutionWitness) { +fn test_zk_vm_helper_with_witness( + program_str: &str, + public_input: &[F], + witness: ExecutionWitness, + flags: CompilationFlags, +) { utils::init_tracing(); - let bytecode = compile_program(&ProgramSource::Raw(program_str.to_string())); + let bytecode = compile_program_with_flags(&ProgramSource::Raw(program_str.to_string()), flags); let time = std::time::Instant::now(); let starting_log_inv_rate = 1; let proof = prove_execution( diff --git a/crates/lean_prover/tests/dump_zkvm_vector.rs b/crates/lean_prover/tests/dump_zkvm_vector.rs deleted file mode 100644 index 32af4c14d..000000000 --- a/crates/lean_prover/tests/dump_zkvm_vector.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Single end-to-end test vector for the Python verifier: aggregate 1000 XMSS -//! signatures using rec-aggregation, then dump the resulting bytecode, public -//! input, table metadata, and proof. -//! -//! Run: -//! cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture -//! -//! Output: `target/zkvm_test_vectors/proof.json` + `proof.bytecode_mle.bin`. - -use std::fs; -use std::path::PathBuf; - -use backend::{Air, MerkleOpening, PrimeField32}; -use lean_vm::*; -use rec_aggregation::{aggregate_type_1, get_aggregation_bytecode, init_aggregation_bytecode, verify_type_1}; -use serde::Serialize; -use std::io::Write; -use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; - -type F = lean_vm::F; - -const DIGEST_ELEMS: usize = 8; - -fn f_to_u32(x: F) -> u32 { - x.as_canonical_u32() -} - -#[derive(Serialize)] -struct MerkleOpeningJson { - leaf_data: Vec, - path: Vec<[u32; DIGEST_ELEMS]>, -} - -#[derive(Serialize)] -struct RawProofJson { - /// Flat raw transcript: every absorbed group is padded to a multiple of 8 - /// (RATE) with zeros — the format the zkDSL recursion verifier reads. - transcript: Vec, - /// Already-restored Merkle openings (no pruning) in the order the verifier - /// consumes them. - merkle_openings: Vec, -} - -#[derive(Serialize)] -struct TableInfoJson { - name: &'static str, - n_columns: usize, -} - -#[derive(Serialize)] -struct ConstantsJson { - n_instruction_columns: usize, - n_runtime_columns: usize, - col_pc: usize, - logup_memory_domainsep: usize, - logup_bytecode_domainsep: usize, - log_max_bus_width: usize, - starting_pc: usize, - ending_pc: usize, -} - -#[derive(Serialize)] -struct OutJson { - /// Aggregation bytecode metadata. The multilinear is in the sidecar. - bytecode_log_size: usize, - bytecode_hash: [u32; DIGEST_ELEMS], - bytecode_multilinear_path: String, - bytecode_multilinear_len: usize, - - /// Public input to `verify_execution` (the hashed `input_data`). - public_input: [u32; DIGEST_ELEMS], - - /// Pre-image of `public_input`. Dumped so Python can re-derive the hash. - input_data: Vec, - - /// Per-table metadata + global constants. - n_tables: usize, - tables: Vec, - constants: ConstantsJson, - snark_domain_sep: [u32; DIGEST_ELEMS], - - proof: RawProofJson, -} - -fn convert_opening(o: &MerkleOpening) -> MerkleOpeningJson { - MerkleOpeningJson { - leaf_data: o.leaf_data.iter().map(|&f| f_to_u32(f)).collect(), - path: o.path.iter().map(|d| d.map(f_to_u32)).collect(), - } -} - -#[test] -fn dump_zkvm_vector() { - let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); - let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(&target_dir) - .join("zkvm_test_vectors"); - fs::create_dir_all(&out_dir).unwrap(); - - // Compile the aggregation program once. - init_aggregation_bytecode(); - let bytecode = get_aggregation_bytecode(); - - // Aggregate 1000 raw XMSS signatures into a TypeOneMultiSignature. - const N_SIGNATURES: usize = 1000; - let sig = { - let raw_xmss = get_benchmark_signatures()[..N_SIGNATURES].to_vec(); - aggregate_type_1( - &[], - raw_xmss, - message_for_benchmark(), - BENCHMARK_SLOT, - /* log_inv_rate = */ 1, - ) - .expect("aggregate_type_1 failed") - }; - - // `verify_type_1` runs the Rust verifier (self-check) and returns the - // restored, padded raw transcript that the zkDSL recursion verifier - // expects — which is exactly what the Python verifier consumes. - let verified = verify_type_1(&sig).expect("Rust verify_type_1 failed"); - let input_data = verified.input_data; - let public_input = verified.input_data_hash; - let raw_proof = verified.raw_proof; - - let table_infos: Vec = ALL_TABLES - .iter() - .map(|t| TableInfoJson { - name: t.name(), - n_columns:
::n_columns(t), - }) - .collect(); - - // Sidecar: raw u32-LE bytecode multilinear. - let mle_path = "proof.bytecode_mle.bin"; - { - let mut f = fs::File::create(out_dir.join(mle_path)).unwrap(); - for v in &bytecode.instructions_multilinear { - f.write_all(&f_to_u32(*v).to_le_bytes()).unwrap(); - } - } - - let out = OutJson { - bytecode_log_size: bytecode.log_size(), - bytecode_hash: bytecode.hash.map(f_to_u32), - bytecode_multilinear_path: mle_path.to_string(), - bytecode_multilinear_len: bytecode.instructions_multilinear.len(), - public_input: public_input.map(f_to_u32), - input_data: input_data.iter().map(|&f| f_to_u32(f)).collect(), - n_tables: N_TABLES, - tables: table_infos, - constants: ConstantsJson { - n_instruction_columns: N_INSTRUCTION_COLUMNS, - n_runtime_columns: N_RUNTIME_COLUMNS, - col_pc: COL_PC, - logup_memory_domainsep: LOGUP_MEMORY_DOMAINSEP, - logup_bytecode_domainsep: LOGUP_BYTECODE_DOMAINSEP, - log_max_bus_width: LOG_MAX_BUS_WIDTH, - starting_pc: STARTING_PC, - ending_pc: bytecode.ending_pc, - }, - snark_domain_sep: lean_prover::SNARK_DOMAIN_SEP.map(f_to_u32), - proof: RawProofJson { - transcript: raw_proof.transcript.iter().map(|&f| f_to_u32(f)).collect(), - merkle_openings: raw_proof.merkle_openings.iter().map(convert_opening).collect(), - }, - }; - - let json_path = out_dir.join("proof.json"); - fs::write(&json_path, serde_json::to_string(&out).unwrap()).unwrap(); - - let mle_bytes = out_dir.join(mle_path).metadata().unwrap().len(); - println!( - "wrote test vector:\n {} ({:.1} KiB)\n {} ({:.1} KiB)", - json_path.display(), - json_path.metadata().unwrap().len() as f64 / 1024.0, - out_dir.join(mle_path).display(), - mle_bytes as f64 / 1024.0, - ); - println!( - " bytecode_log_size={}, transcript_len={}, input_data_len={}", - out.bytecode_log_size, - out.proof.transcript.len(), - out.input_data.len(), - ); -} diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 926d707cb..229accb2f 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1,6 +1,6 @@ """Pure-Python verifier for leanVM proofs. Setup the test vector (one-time): - cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture + cargo test --release -p lean_prover dump_test_vector_for_python_verifier -- --nocapture Run: python3 crates/lean_prover/verifier.py Format: @@ -195,46 +195,7 @@ def eval_multilinear(evals: Sequence[EF], point: Sequence[EF]) -> EF: def eval_base_field_multilinear(base_evals: Sequence[int], point: Sequence[EF]) -> EF: """Evaluate a base-field multilinear in evaluation form at `point`.""" - assert len(base_evals) == 1 << len(point) - - # First fold: cur[n] = base[2n] + (base[2n+1] − base[2n]) · r. - r0, r1, r2, r3, r4 = (int(c.value) for c in point[-1].c) - cur = [] - for j in range(0, len(base_evals), 2): - a = base_evals[j] % P - d = (base_evals[j + 1] - a) % P - cur.append(((a + d * r0) % P, (d * r1) % P, (d * r2) % P, (d * r3) % P, (d * r4) % P)) - - # Subsequent folds in EF. Schoolbook produces a degree-8 polynomial p[0..8]; - # reduce via X^5 ≡ 1 − X^2 (so X^6 ≡ X − X^3, X^7 ≡ X^2 − X^4, X^8 ≡ −1 + X^2 + X^3). - for pt in reversed(point[:-1]): - r0, r1, r2, r3, r4 = (int(c.value) for c in pt.c) - new = [] - for j in range(0, len(cur), 2): - a0, a1, a2, a3, a4 = cur[j] - b0, b1, b2, b3, b4 = cur[j + 1] - d0, d1, d2, d3, d4 = (b0 - a0) % P, (b1 - a1) % P, (b2 - a2) % P, (b3 - a3) % P, (b4 - a4) % P - p0 = d0 * r0 - p1 = d0 * r1 + d1 * r0 - p2 = d0 * r2 + d1 * r1 + d2 * r0 - p3 = d0 * r3 + d1 * r2 + d2 * r1 + d3 * r0 - p4 = d0 * r4 + d1 * r3 + d2 * r2 + d3 * r1 + d4 * r0 - p5 = d1 * r4 + d2 * r3 + d3 * r2 + d4 * r1 - p6 = d2 * r4 + d3 * r3 + d4 * r2 - p7 = d3 * r4 + d4 * r3 - p8 = d4 * r4 - new.append( - ( - (a0 + p0 + p5 - p8) % P, - (a1 + p1 + p6) % P, - (a2 + p2 - p5 + p7 + p8) % P, - (a3 + p3 - p6 + p8) % P, - (a4 + p4 - p7) % P, - ) - ) - cur = new - - return EF([Fp(x) for x in cur[0]]) + return eval_multilinear([EF(v) for v in base_evals], point) def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: @@ -1313,15 +1274,6 @@ def verify_execution( return {"log_inv_rate": log_inv_rate, "log_memory": log_memory, "stacked_n_vars": stacked_n_vars} -def poseidon_compress_slice_iv(data: Sequence[Fp]) -> list[Fp]: - """Hash a multiple-of-8 sequence (Poseidon16/Davies-Meyer). IV first element = len(data).""" - assert data and len(data) % 8 == 0 - h = [Fp(len(data))] + [Fp(0)] * 7 - for i in range(0, len(data), 8): - h = poseidon16_compress(h, list(data[i : i + 8])) - return h - - def main() -> int: import array, json from pathlib import Path @@ -1330,7 +1282,7 @@ def main() -> int: if not vector_path.exists(): print( f"Test vector not found at {vector_path}. Generate it first with:\n" - " cargo test --release -p lean_prover --test dump_zkvm_vector -- --nocapture" + " cargo test --release -p lean_prover dump_zkvm_vector -- --nocapture" ) return 1 @@ -1344,7 +1296,6 @@ def main() -> int: fp_list = lambda xs: [Fp(v) for v in xs] public_input = fp_list(raw["public_input"]) - input_data = fp_list(raw["input_data"]) proof = Proof( transcript=fp_list(raw["proof"]["transcript"]), merkle_openings=[ @@ -1353,10 +1304,6 @@ def main() -> int: ], ) - if poseidon_compress_slice_iv(input_data) != public_input: - print("FAIL: poseidon_compress_slice(input_data) doesn't match dumped public_input") - return 1 - try: result = verify_execution( fp_list(raw["bytecode_hash"]), @@ -1371,7 +1318,7 @@ def main() -> int: print(f"FAIL: {e}") return 1 - print(f"OK: rec-aggregation proof verified ({result})") + print(f"OK: minimal-zkVM proof verified ({result})") return 0 From d0370933791ddf7ea1cdcf2dcc2e96801f21219a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 15:17:39 +0400 Subject: [PATCH 41/69] wip --- crates/lean_prover/src/test_zkvm.rs | 24 +---------- crates/lean_prover/verifier.py | 62 +++++++++++++++-------------- 2 files changed, 33 insertions(+), 53 deletions(-) diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index 821a75364..4f8dd45b8 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -227,7 +227,6 @@ fn dump_test_vector_for_python_verifier() { all_precompiles_flags(LOOP_ITERS), ); let exec_proof = prove_execution(&bytecode, &public_input, &witness, &default_whir_config(1), false).unwrap(); - let n_cycles = exec_proof.metadata.as_ref().map_or(0, |m| m.cycles); let (_details, raw_proof) = verify_execution(&bytecode, &public_input, exec_proof.proof).unwrap(); let f_u32 = |x: F| x.as_canonical_u32(); @@ -244,10 +243,6 @@ fn dump_test_vector_for_python_verifier() { mle_file.write_all(&f_u32(*v).to_le_bytes()).unwrap(); } - let tables_json: Vec = ALL_TABLES - .iter() - .map(|t| serde_json::json!({"name": t.name(), "n_columns":
::n_columns(t)})) - .collect(); let opening_json = |o: &MerkleOpening| -> serde_json::Value { serde_json::json!({ "leaf_data": o.leaf_data.iter().map(|&f| f_u32(f)).collect::>(), @@ -258,22 +253,7 @@ fn dump_test_vector_for_python_verifier() { "bytecode_log_size": bytecode.log_size(), "bytecode_hash": bytecode.hash.map(f_u32), "bytecode_multilinear_path": mle_path, - "bytecode_multilinear_len": bytecode.instructions_multilinear.len(), "public_input": public_input.iter().map(|&f| f_u32(f)).collect::>(), - "n_tables": N_TABLES, - "tables": tables_json, - "constants": { - "n_instruction_columns": N_INSTRUCTION_COLUMNS, - "n_runtime_columns": N_RUNTIME_COLUMNS, - "col_pc": COL_PC, - "logup_memory_domainsep": LOGUP_MEMORY_DOMAINSEP, - "logup_bytecode_domainsep": LOGUP_BYTECODE_DOMAINSEP, - "log_max_bus_width": LOG_MAX_BUS_WIDTH, - "starting_pc": STARTING_PC, - "ending_pc": bytecode.ending_pc, - }, - "snark_domain_sep": crate::SNARK_DOMAIN_SEP.map(f_u32), - "n_cycles": n_cycles, "proof": { "transcript": raw_proof.transcript.iter().map(|&f| f_u32(f)).collect::>(), "merkle_openings": raw_proof.merkle_openings.iter().map(opening_json).collect::>(), @@ -283,12 +263,10 @@ fn dump_test_vector_for_python_verifier() { std::fs::write(&json_path, serde_json::to_string(&out).unwrap()).unwrap(); println!( - "wrote {} ({:.1} KiB), bytecode_log_size={}, n_cycles={} (~2^{:.2})", + "wrote {} ({:.1} KiB), bytecode_log_size={}", json_path.display(), json_path.metadata().unwrap().len() as f64 / 1024.0, bytecode.log_size(), - n_cycles, - (n_cycles as f64).log2(), ); } diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 229accb2f..12a8069a5 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -30,6 +30,12 @@ MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension_op": 21, "poseidon16_compress": 21} ALL_TABLES_ORDER = ("execution", "extension_op", "poseidon16_compress") +# leanVM constants (mirror `crates/lean_vm/src/`). `ending_pc` is the only bytecode-specific value. +N_INSTRUCTION_COLUMNS, N_RUNTIME_COLUMNS, COL_PC = 12, 8, 0 +LOGUP_MEMORY_DOMAINSEP, LOGUP_BYTECODE_DOMAINSEP = 1, 2 +LOG_MAX_BUS_WIDTH = 4 # = log2_ceil(N_INSTRUCTION_COLUMNS + 2) +STARTING_PC = 0 + class ProofError(Exception): pass @@ -523,18 +529,17 @@ def n_buses(self) -> int: return sum(b[3] if b[0] == "mem_group" else 1 for b in self.buses) -def tables_from_json(obj: list[dict]) -> list[TableMeta]: - # (air_degree, n_constraints, n_shift, air_fn) per table. - specs = { - "execution": (5, 14, 2, _eval_air_execution), - "extension_op": (6, 35, 13, _eval_air_extension_op), - "poseidon16_compress": (10, 101, 0, _eval_air_poseidon16), - } - out = [] - for t in obj: - name, n_cols = t["name"], int(t["n_columns"]) - out.append(TableMeta(name, n_cols, _table_buses(name, n_cols), *specs[name])) - return out +def default_tables() -> list[TableMeta]: + # (n_columns, air_degree, n_constraints, n_shift, air_fn) per table — all constants. + # poseidon16_compress: 9 scalars + 16 inputs + half_full_rounds*WIDTH (begin + end-1) + partial + WIDTH outputs. + p = POSEIDON1_AIR_CONSTANTS + poseidon_n_cols = 25 + 32 * (p["half_full_rounds"] // 2) + p["partial_rounds"] + specs = ( + ("execution", N_INSTRUCTION_COLUMNS + N_RUNTIME_COLUMNS, 5, 14, 2, _eval_air_execution), + ("extension_op", 29, 6, 35, 13, _eval_air_extension_op), + ("poseidon16_compress", poseidon_n_cols, 10, 101, 0, _eval_air_poseidon16), + ) + return [TableMeta(name, n, _table_buses(name, n), d, k, s, fn) for name, n, d, k, s, fn in specs] def stacked_pcs_global_statements( @@ -545,7 +550,7 @@ def stacked_pcs_global_statements( table_log_heights: dict[str, int], committed_statements: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]], tables: dict[str, TableMeta], - constants: dict, + ending_pc: int, ) -> list[SparseStatement]: assert len(table_log_heights) == len(committed_statements) tables_sorted = sort_tables_by_height(table_log_heights) @@ -559,7 +564,6 @@ def stacked_pcs_global_statements( layout_offset += tables[name].n_columns << n_vars out = list(previous_statements) - col_pc = constants["col_pc"] # Rust uses BTreeMap (sorted by key); Python dicts are insertion-ordered, sort here. def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: @@ -571,8 +575,8 @@ def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: col_base = offset >> n_vars if name == "execution": # PC column: pin first row to STARTING_PC, last row to ending_pc. - for idx, pc in [(0, constants["starting_pc"]), ((1 << n_vars) - 1, constants["ending_pc"])]: - out.append(SparseStatement.unique_value(stacked_n_vars, offset + (col_pc << n_vars) + idx, EF(pc))) + for idx, pc in [(0, STARTING_PC), ((1 << n_vars) - 1, ending_pc)]: + out.append(SparseStatement.unique_value(stacked_n_vars, offset + (COL_PC << n_vars) + idx, EF(pc))) for point, eq_values, next_values in committed_statements[name]: if next_values: out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values, col_base))) @@ -667,14 +671,13 @@ def verify_generic_logup( bytecode_multilinear: list[int], table_log_heights: dict[str, int], tables: dict[str, TableMeta], - constants: dict, ) -> dict: """GKR-quotient + section-by-section (memory/bytecode/per-table) reconstruction.""" - n_instr_cols = constants["n_instruction_columns"] - n_runtime_cols = constants["n_runtime_columns"] - col_pc = constants["col_pc"] - ds_mem = Fp(constants["logup_memory_domainsep"]) - ds_byte = Fp(constants["logup_bytecode_domainsep"]) + n_instr_cols = N_INSTRUCTION_COLUMNS + n_runtime_cols = N_RUNTIME_COLUMNS + col_pc = COL_PC + ds_mem = Fp(LOGUP_MEMORY_DOMAINSEP) + ds_byte = Fp(LOGUP_BYTECODE_DOMAINSEP) tables_sorted = sort_tables_by_height(table_log_heights) log_bytecode = log2_strict_usize(len(bytecode_multilinear) // (1 << log2_ceil_usize(n_instr_cols))) @@ -1119,10 +1122,11 @@ def verify_execution( bytecode_log_size: int, public_input: Sequence[Fp], proof: Proof, - tables: Sequence[TableMeta], - constants: dict, bytecode_multilinear: list[int], ) -> dict: + tables = default_tables() + # Bytecode is padded to the next power of two; `ending_pc` is the last slot. + ending_pc = (1 << bytecode_log_size) - 1 if len(public_input) != PUBLIC_INPUT_SIZE: raise ProofError("InvalidProof: public_input length mismatch") @@ -1173,7 +1177,7 @@ def verify_execution( logup_c = state.sample_ef() state.duplex() - logup_alphas = state.sample_many_ef(constants["log_max_bus_width"]) + logup_alphas = state.sample_many_ef(LOG_MAX_BUS_WIDTH) logup_alphas_eq = eval_eq(logup_alphas) logup = verify_generic_logup( state, @@ -1184,7 +1188,6 @@ def verify_execution( bytecode_multilinear, table_log_heights, tables_by_name, - constants, ) gkr_point = logup["gkr_point"] @@ -1260,7 +1263,7 @@ def verify_execution( table_log_heights, committed, tables_by_name, - constants, + ending_pc, ) whir_verify(state, cfg, parsed_commitment, global_statements) @@ -1291,7 +1294,8 @@ def main() -> int: arr = array.array("I") arr.frombytes((vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes()) - assert len(arr) == raw["bytecode_multilinear_len"] + expected_len = (1 << raw["bytecode_log_size"]) * (1 << log2_ceil_usize(N_INSTRUCTION_COLUMNS)) + assert len(arr) == expected_len, f"bytecode_multilinear length {len(arr)} != expected {expected_len}" bytecode_multilinear: list[int] = list(arr) fp_list = lambda xs: [Fp(v) for v in xs] @@ -1310,8 +1314,6 @@ def main() -> int: raw["bytecode_log_size"], public_input, proof, - tables_from_json(raw["tables"]), - raw["constants"], bytecode_multilinear, ) except ProofError as e: From 38990ad7c3034c3c0253dde122ed6b42ef7942e2 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 15:33:34 +0400 Subject: [PATCH 42/69] wip --- crates/lean_prover/src/test_zkvm.rs | 2 -- crates/lean_prover/verifier.py | 15 +++------------ 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index 4f8dd45b8..b8c7d270d 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -250,8 +250,6 @@ fn dump_test_vector_for_python_verifier() { }) }; let out = serde_json::json!({ - "bytecode_log_size": bytecode.log_size(), - "bytecode_hash": bytecode.hash.map(f_u32), "bytecode_multilinear_path": mle_path, "public_input": public_input.iter().map(|&f| f_u32(f)).collect::>(), "proof": { diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 12a8069a5..29e0ebaae 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1118,15 +1118,14 @@ def take(n: int) -> list[EF]: def verify_execution( - bytecode_hash: list[Fp], - bytecode_log_size: int, public_input: Sequence[Fp], proof: Proof, bytecode_multilinear: list[int], ) -> dict: tables = default_tables() - # Bytecode is padded to the next power of two; `ending_pc` is the last slot. + bytecode_log_size = log2_strict_usize(len(bytecode_multilinear)) - log2_ceil_usize(N_INSTRUCTION_COLUMNS) ending_pc = (1 << bytecode_log_size) - 1 + bytecode_hash = sponge_hash([Fp(v) for v in bytecode_multilinear]) if len(public_input) != PUBLIC_INPUT_SIZE: raise ProofError("InvalidProof: public_input length mismatch") @@ -1294,8 +1293,6 @@ def main() -> int: arr = array.array("I") arr.frombytes((vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes()) - expected_len = (1 << raw["bytecode_log_size"]) * (1 << log2_ceil_usize(N_INSTRUCTION_COLUMNS)) - assert len(arr) == expected_len, f"bytecode_multilinear length {len(arr)} != expected {expected_len}" bytecode_multilinear: list[int] = list(arr) fp_list = lambda xs: [Fp(v) for v in xs] @@ -1309,13 +1306,7 @@ def main() -> int: ) try: - result = verify_execution( - fp_list(raw["bytecode_hash"]), - raw["bytecode_log_size"], - public_input, - proof, - bytecode_multilinear, - ) + result = verify_execution(public_input, proof, bytecode_multilinear) except ProofError as e: print(f"FAIL: {e}") return 1 From 1d5b2e4accfb041ed51f7ef294ef8a1eb6629104 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 15:39:15 +0400 Subject: [PATCH 43/69] w --- crates/lean_prover/primitives.py | 4 ++-- crates/lean_prover/verifier.py | 27 ++++++++++++--------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index 472c5a059..7613d714f 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -251,11 +251,11 @@ def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: return [a + b for a, b in zip(POSEIDON16.permute(state), state)][:DIGEST_ELEMS] -def log2_ceil_usize(x: int) -> int: +def log2_ceil(x: int) -> int: return 0 if x <= 1 else (x - 1).bit_length() -def log2_strict_usize(x: int) -> int: +def log2_strict(x: int) -> int: assert x > 0 and (x & (x - 1)) == 0, f"{x} is not a power of two" return x.bit_length() - 1 diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 29e0ebaae..34c7df155 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -30,10 +30,8 @@ MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension_op": 21, "poseidon16_compress": 21} ALL_TABLES_ORDER = ("execution", "extension_op", "poseidon16_compress") -# leanVM constants (mirror `crates/lean_vm/src/`). `ending_pc` is the only bytecode-specific value. -N_INSTRUCTION_COLUMNS, N_RUNTIME_COLUMNS, COL_PC = 12, 8, 0 +N_RUNTIME_COLUMNS, N_INSTRUCTION_COLUMNS, COL_PC = 8, 12, 0 LOGUP_MEMORY_DOMAINSEP, LOGUP_BYTECODE_DOMAINSEP = 1, 2 -LOG_MAX_BUS_WIDTH = 4 # = log2_ceil(N_INSTRUCTION_COLUMNS + 2) STARTING_PC = 0 @@ -680,15 +678,15 @@ def verify_generic_logup( ds_byte = Fp(LOGUP_BYTECODE_DOMAINSEP) tables_sorted = sort_tables_by_height(table_log_heights) - log_bytecode = log2_strict_usize(len(bytecode_multilinear) // (1 << log2_ceil_usize(n_instr_cols))) - log_instr = log2_ceil_usize(n_instr_cols) + log_bytecode = log2_strict(len(bytecode_multilinear) // (1 << log2_ceil(n_instr_cols))) + log_instr = log2_ceil(n_instr_cols) total_active_len = ( (1 << log_memory) + max(1 << log_bytecode, 1 << tables_sorted[0][1]) + sum(tables[n].n_buses << h for n, h in tables_sorted) ) - total_gkr_n_vars = log2_ceil_usize(total_active_len) + total_gkr_n_vars = log2_ceil(total_active_len) quotient, point_gkr, claim_num, claim_den = verify_gkr_quotient(state, total_gkr_n_vars) if quotient != ZERO: @@ -1121,9 +1119,9 @@ def verify_execution( public_input: Sequence[Fp], proof: Proof, bytecode_multilinear: list[int], -) -> dict: +): tables = default_tables() - bytecode_log_size = log2_strict_usize(len(bytecode_multilinear)) - log2_ceil_usize(N_INSTRUCTION_COLUMNS) + bytecode_log_size = log2_strict(len(bytecode_multilinear)) - log2_ceil(N_INSTRUCTION_COLUMNS) ending_pc = (1 << bytecode_log_size) - 1 bytecode_hash = sponge_hash([Fp(v) for v in bytecode_multilinear]) if len(public_input) != PUBLIC_INPUT_SIZE: @@ -1162,7 +1160,7 @@ def verify_execution( + (1 << max(bytecode_log_size, n_max)) + sum(t.n_columns << table_log_heights[t.name] for t in tables) ) - stacked_n_vars = log2_ceil_usize(total_stacked) + stacked_n_vars = log2_ceil(total_stacked) if stacked_n_vars > TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") cfg = whir_config(log_inv_rate, stacked_n_vars) @@ -1176,7 +1174,7 @@ def verify_execution( logup_c = state.sample_ef() state.duplex() - logup_alphas = state.sample_many_ef(LOG_MAX_BUS_WIDTH) + logup_alphas = state.sample_many_ef(log2_ceil(N_INSTRUCTION_COLUMNS + 2)) logup_alphas_eq = eval_eq(logup_alphas) logup = verify_generic_logup( state, @@ -1239,7 +1237,7 @@ def verify_execution( raise ProofError("AIR sumcheck: claimed value mismatch") assert len(public_input) % DIGEST_ELEMS == 0 - pm_point = state.sample_many_ef(log2_strict_usize(len(public_input))) + pm_point = state.sample_many_ef(log2_strict(len(public_input))) pm_eval = eval_multilinear([EF(f) for f in public_input], pm_point) bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size @@ -1273,8 +1271,6 @@ def verify_execution( if state.openings: raise ProofError(f"InvalidProof: {len(state.openings)} Merkle openings unused") - return {"log_inv_rate": log_inv_rate, "log_memory": log_memory, "stacked_n_vars": stacked_n_vars} - def main() -> int: import array, json @@ -1290,6 +1286,7 @@ def main() -> int: print(f"Loading {vector_path.name}...") raw = json.loads(vector_path.read_text()) + print("... done") arr = array.array("I") arr.frombytes((vector_path.parent / raw["bytecode_multilinear_path"]).read_bytes()) @@ -1306,12 +1303,12 @@ def main() -> int: ) try: - result = verify_execution(public_input, proof, bytecode_multilinear) + verify_execution(public_input, proof, bytecode_multilinear) except ProofError as e: print(f"FAIL: {e}") return 1 - print(f"OK: minimal-zkVM proof verified ({result})") + print(f"Proof successfully verified") return 0 From 037edf267e8ad86f72f305f016ac9b2b77911d14 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 15:50:07 +0400 Subject: [PATCH 44/69] w --- crates/lean_prover/verifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 34c7df155..6da8079ac 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -42,7 +42,7 @@ class ProofError(Exception): # T-Sponge (compression instead of permutation) with replacement (instead of xoring / adding the ingested data). def sponge_hash(data: Sequence[Fp]) -> list[Fp]: assert len(data) % SPONGE_RATE == 0 and len(data) > 0 - state = [Fp(len(data))] + [Fp(0)] * (SPONGE_CAPACITY - 1) + state = [Fp(len(data))] + [Fp(0)] * (SPONGE_CAPACITY - 1) # IV = [size, 0, 0, 0, ..., 0] for k in range(len(data) // SPONGE_RATE): state = poseidon16_compress(state, data[k * SPONGE_RATE : (k + 1) * SPONGE_RATE]) return state From a54584d46557ed777e098384f91d5f78d9e020f7 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 16:33:50 +0400 Subject: [PATCH 45/69] wip --- crates/lean_prover/verifier.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 6da8079ac..3045f23d7 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -53,8 +53,8 @@ def fiat_shamir_domain_sep(bytecode_hash: Sequence[Fp]) -> list[Fp]: class Challenger: # https://eprint.iacr.org/2025/536.pdf - def __init__(self) -> None: - self.state: list[Fp] = [Fp(0)] * SPONGE_STATE + def __init__(self, initial_capacity: Sequence[Fp]) -> None: + self.state: list[Fp] = list(initial_capacity) + [Fp(0)] * SPONGE_RATE self.rate_fresh: bool = False def observe(self, chunk: Sequence[Fp]) -> None: @@ -110,8 +110,8 @@ class Proof: class VerifierState(Challenger): - def __init__(self, proof: Proof) -> None: - super().__init__() + def __init__(self, proof: Proof, initial_capacity: Sequence[Fp]) -> None: + super().__init__(initial_capacity) self.transcript = list(proof.transcript) self.openings = list(reversed(proof.merkle_openings)) self.offset = 0 @@ -1127,9 +1127,8 @@ def verify_execution( if len(public_input) != PUBLIC_INPUT_SIZE: raise ProofError("InvalidProof: public_input length mismatch") - state = VerifierState(proof) + state = VerifierState(proof, fiat_shamir_domain_sep(bytecode_hash)) state.observe_scalars(list(public_input)) - state.observe_scalars(fiat_shamir_domain_sep(bytecode_hash)) dims = [int(x.value) for x in state.next_base_scalars_vec(2 + len(tables))] log_inv_rate, log_memory, *table_log_n_rows = dims From 1cf05c8f8c62bd4f924f76267b503517982ca201 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 18:09:10 +0400 Subject: [PATCH 46/69] w --- crates/lean_prover/verifier.py | 69 ++++++++++++++++------------------ 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 3045f23d7..b05e4519f 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -188,7 +188,7 @@ def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: return s + math.prod([*x, *y]) -def eval_multilinear(evals: Sequence[EF], point: Sequence[EF]) -> EF: +def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: """Evaluate a multilinear in evaluation form at `point`.""" assert len(evals) == 1 << len(point) cur = list(evals) @@ -197,11 +197,6 @@ def eval_multilinear(evals: Sequence[EF], point: Sequence[EF]) -> EF: return cur[0] -def eval_base_field_multilinear(base_evals: Sequence[int], point: Sequence[EF]) -> EF: - """Evaluate a base-field multilinear in evaluation form at `point`.""" - return eval_multilinear([EF(v) for v in base_evals], point) - - def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: """Evaluate a multilinear in coefficient form at `point`.""" assert len(coeffs) == 1 << len(point) @@ -214,7 +209,7 @@ def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: @dataclass -class SparseStatement: +class SparseStatements: total_num_variables: int point: list[EF] values: list[tuple[int, EF]] @@ -225,16 +220,16 @@ def selector_num_variables(self) -> int: return self.total_num_variables - len(self.point) @staticmethod - def dense(point: list[EF], value: EF) -> "SparseStatement": - return SparseStatement(len(point), point, [(0, value)]) + def dense(point: list[EF], value: EF) -> "SparseStatements": + return SparseStatements(len(point), point, [(0, value)]) @staticmethod - def unique_value(total: int, index: int, value: EF) -> "SparseStatement": - return SparseStatement(total, [], [(index, value)]) + def unique_value(total: int, index: int, value: EF) -> "SparseStatements": + return SparseStatements(total, [], [(index, value)]) @staticmethod - def new_next(total: int, point: list[EF], values: list[tuple[int, EF]]) -> "SparseStatement": - return SparseStatement(total, point, values, is_next=True) + def new_next(total: int, point: list[EF], values: list[tuple[int, EF]]) -> "SparseStatements": + return SparseStatements(total, point, values, is_next=True) def whir_folding_factor_at_round(r: int) -> int: @@ -292,9 +287,9 @@ class ParsedCommitment: ood_points: list[EF] ood_answers: list[EF] - def oods_constraints(self) -> list[SparseStatement]: + def oods_constraints(self) -> list[SparseStatements]: return [ - SparseStatement.dense(expand_from_univariate(p, self.num_variables), ev) + SparseStatements.dense(expand_from_univariate(p, self.num_variables), ev) for p, ev in zip(self.ood_points, self.ood_answers) ] @@ -323,7 +318,7 @@ def verify_sumcheck( return point, target -def combine_constraints(state: VerifierState, target: EF, constraints: list[SparseStatement]) -> tuple[EF, list[EF]]: +def combine_constraints(state: VerifierState, target: EF, constraints: list[SparseStatements]) -> tuple[EF, list[EF]]: """Fold all constraint values into `target` via powers of γ; return those γ-power weights.""" gamma: EF = state.sample_ef() combo: list[EF] = [] @@ -346,7 +341,7 @@ def verify_stir_challenges( query_pow_bits: int, commitment: ParsedCommitment, folding_randomness: list[EF], -) -> list[SparseStatement]: +) -> list[SparseStatements]: log_height = whir_log_domain_size_at(cfg["num_variables"], cfg["log_inv_rate"], round_index) - folding_factor gen = two_adic_generator(log_height) state.check_pow_grinding(query_pow_bits) @@ -357,18 +352,18 @@ def pack_answers(leaf: list[Fp]) -> list[EF]: return [EF(f) for f in leaf] return [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] - constraints: list[SparseStatement] = [] + constraints: list[SparseStatements] = [] for idx in indices: op = state.next_merkle_opening() if not merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path): raise ProofError("Merkle verification failed") - fold = eval_multilinear(pack_answers(op.leaf_data), folding_randomness) + fold = eval_multilinear_evals(pack_answers(op.leaf_data), folding_randomness) ef_pt = EF(pow(int(gen.value), idx, P)) - constraints.append(SparseStatement.dense(expand_from_univariate(ef_pt, num_variables), fold)) + constraints.append(SparseStatements.dense(expand_from_univariate(ef_pt, num_variables), fold)) return constraints -def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> bool: +def verify_constraint_coeffs(constraint: SparseStatements, coeffs: list[EF]) -> bool: """Checks `point == [α, α², α⁴, …]` and `Σ coeffs[i]·α^i == value`.""" assert constraint.selector_num_variables == 0 alpha = constraint.point[0] @@ -378,7 +373,7 @@ def verify_constraint_coeffs(constraint: SparseStatement, coeffs: list[EF]) -> b return all(univ_eval == v[1] for v in constraint.values) -def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatement]]], point: list[EF]) -> EF: +def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatements]]], point: list[EF]) -> EF: value = ZERO pt = list(point) for round_idx, (randomness, smts) in enumerate(constraints): @@ -401,17 +396,17 @@ def whir_verify( state: VerifierState, cfg: dict, parsed_commitment: ParsedCommitment, - statement: list[SparseStatement], + statement: list[SparseStatements], ) -> list[EF]: for s in statement: assert s.total_num_variables == parsed_commitment.num_variables n_rounds, final_sumcheck_rounds = whir_n_rounds_and_final_sumcheck(cfg["num_variables"]) - round_constraints: list[tuple[list[EF], list[SparseStatement]]] = [] + round_constraints: list[tuple[list[EF], list[SparseStatements]]] = [] round_folding: list[list[EF]] = [] target = ZERO - def step(constraints: list[SparseStatement], n_fold: int, pow_bits: int) -> None: + def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> None: nonlocal target state.duplex() new_target, combo = combine_constraints(state, target, constraints) @@ -544,12 +539,12 @@ def stacked_pcs_global_statements( stacked_n_vars: int, memory_n_vars: int, bytecode_n_vars: int, - previous_statements: list[SparseStatement], + previous_statements: list[SparseStatements], table_log_heights: dict[str, int], committed_statements: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]], tables: dict[str, TableMeta], ending_pc: int, -) -> list[SparseStatement]: +) -> list[SparseStatements]: assert len(table_log_heights) == len(committed_statements) tables_sorted = sort_tables_by_height(table_log_heights) @@ -574,11 +569,11 @@ def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: if name == "execution": # PC column: pin first row to STARTING_PC, last row to ending_pc. for idx, pc in [(0, STARTING_PC), ((1 << n_vars) - 1, ending_pc)]: - out.append(SparseStatement.unique_value(stacked_n_vars, offset + (COL_PC << n_vars) + idx, EF(pc))) + out.append(SparseStatements.unique_value(stacked_n_vars, offset + (COL_PC << n_vars) + idx, EF(pc))) for point, eq_values, next_values in committed_statements[name]: if next_values: - out.append(SparseStatement.new_next(stacked_n_vars, list(point), values_at(next_values, col_base))) - out.append(SparseStatement(stacked_n_vars, list(point), values_at(eq_values, col_base))) + out.append(SparseStatements.new_next(stacked_n_vars, list(point), values_at(next_values, col_base))) + out.append(SparseStatements(stacked_n_vars, list(point), values_at(eq_values, col_base))) return out @@ -595,8 +590,8 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] quotient = sum(n * d.inv() for n, d in zip(nums, dens)) point = state.sample_many_ef(N_VARS_TO_SEND_GKR_COEFFS) - claim_num = eval_multilinear(nums, point) - claim_den = eval_multilinear(dens, point) + claim_num = eval_multilinear_evals(nums, point) + claim_den = eval_multilinear_evals(dens, point) for layer_n_vars in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): state.duplex() @@ -716,7 +711,7 @@ def pref_at(offset: int, log_height: int) -> EF: pref = pref_at(offset, log_bytecode) pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() - bytecode_value = eval_base_field_multilinear(bytecode_multilinear, list(byte_pt) + list(from_end(alphas, log_instr))) + bytecode_value = eval_multilinear_evals([EF(v) for v in bytecode_multilinear], list(byte_pt) + list(from_end(alphas, log_instr))) correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( bytecode_value * correction @@ -1237,17 +1232,17 @@ def verify_execution( assert len(public_input) % DIGEST_ELEMS == 0 pm_point = state.sample_many_ef(log2_strict(len(public_input))) - pm_eval = eval_multilinear([EF(f) for f in public_input], pm_point) + pm_eval = eval_multilinear_evals([EF(f) for f in public_input], pm_point) bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size previous = [ - SparseStatement( + SparseStatements( stacked_n_vars, from_end(gkr_point, log_memory), [(0, logup["value_memory"]), (1, logup["value_memory_acc"])], ), - SparseStatement(stacked_n_vars, pm_point, [(0, pm_eval)]), - SparseStatement( + SparseStatements(stacked_n_vars, pm_point, [(0, pm_eval)]), + SparseStatements( stacked_n_vars, from_end(gkr_point, bytecode_log_size), [(bytecode_acc_idx, logup["value_bytecode_acc"])] ), ] From 8b6d088e7227661f0e354ff352abbdcb0018f3ec Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 18:17:42 +0400 Subject: [PATCH 47/69] wip --- crates/lean_prover/primitives.py | 8 ++++++- crates/lean_prover/verifier.py | 36 ++++++++++---------------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index 7613d714f..dbbf5371c 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -1,7 +1,7 @@ # source: https://github.com/leanEthereum/leanSpec from __future__ import annotations - +from itertools import accumulate, repeat from typing import Final, Sequence P: Final = 2**31 - 2**24 + 1 # Koalabear prime @@ -122,6 +122,12 @@ def inv(self) -> "EF": ONE = EF(1) +def ef_powers(x: EF, n: int) -> list[EF]: + """`[1, x, x², …, x^(n−1)]`.""" + return list(accumulate(repeat(x, n), lambda a, _: a * x, initial=ONE))[:n] + + + # 448 raw Poseidon1-KoalaBear width-16 round constants generated by the Grain # LFSR (Poseidon paper §5.3, parameters field_type=1, α=3, n=31, t=16, R_F=8, # R_P=20). Reference: https://github.com/Plonky3/Plonky3/blob/main/poseidon1/generate_constants.py diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index b05e4519f..1e4fb1672 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -11,7 +11,6 @@ import functools import math from dataclasses import dataclass -from itertools import accumulate, repeat from typing import Sequence from primitives import * @@ -244,14 +243,6 @@ def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: return n, nv - n * WHIR_SUBSEQUENT_FOLDING_FACTOR -def ef_powers(x: EF, n: int) -> list[EF]: - """`[1, x, x², …, x^(n−1)]`.""" - out, cur = [], ONE - for _ in range(n): - out.append(cur) - cur = cur * x - return out - def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: return num_variables + start_rate - (RS_DOMAIN_INITIAL_REDUCTION_FACTOR + r - 1 if r >= 1 else 0) @@ -294,7 +285,7 @@ def oods_constraints(self) -> list[SparseStatements]: ] -def _eval_univariate(coeffs: list[EF], x: EF) -> EF: +def eval_univariate_polynomial(coeffs: list[EF], x: EF) -> EF: acc = ZERO for c in reversed(coeffs): acc = acc * x + c @@ -314,7 +305,7 @@ def verify_sumcheck( state.check_pow_grinding(pow_bits) r = state.sample_ef() point.append(r) - target = _eval_univariate(coeffs, r) + target = eval_univariate_polynomial(coeffs, r) return point, target @@ -369,7 +360,7 @@ def verify_constraint_coeffs(constraint: SparseStatements, coeffs: list[EF]) -> alpha = constraint.point[0] if any(a * a != b for a, b in zip(constraint.point, constraint.point[1:])): return False - univ_eval = _eval_univariate(coeffs, alpha) + univ_eval = eval_univariate_polynomial(coeffs, alpha) return all(univ_eval == v[1] for v in constraint.values) @@ -610,11 +601,6 @@ def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF] return quotient, point, claim_num, claim_den -def from_end(seq: Sequence, n: int) -> list: - """The last `n` elements of `seq` (empty list when `n == 0`).""" - return list(seq[-n:]) if n else [] - - def mle_of_01234567_etc(point: Sequence[EF]) -> EF: """MLE of `f(i) = i` (big-endian) at `point`.""" n = len(point) @@ -696,7 +682,7 @@ def pref_at(offset: int, log_height: int) -> EF: return eq_poly(bits, point_gkr[:n_missing]) # Memory (data order: [value_index, value_memory] mirrors `crates/sub_protocols/src/logup.rs`). - mem_pt = from_end(point_gkr, log_memory) + mem_pt = point_gkr[-log_memory:] pref = pref_at(0, log_memory) value_memory_acc = state.next_extension_scalar() num = num - pref * value_memory_acc @@ -707,11 +693,11 @@ def pref_at(offset: int, log_height: int) -> EF: # Bytecode (padded to the tallest table). log_byte_pad = max(log_bytecode, tables_sorted[0][1]) - byte_pt = from_end(point_gkr, log_bytecode) + byte_pt = point_gkr[-log_bytecode:] pref = pref_at(offset, log_bytecode) pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() - bytecode_value = eval_multilinear_evals([EF(v) for v in bytecode_multilinear], list(byte_pt) + list(from_end(alphas, log_instr))) + bytecode_value = eval_multilinear_evals([EF(v) for v in bytecode_multilinear], list(byte_pt) + list(alphas[-log_instr:])) correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( bytecode_value * correction @@ -722,7 +708,7 @@ def pref_at(offset: int, log_height: int) -> EF: den = ( den + pref * (c - fp_byte) - + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, from_end(point_gkr, log_byte_pad)) + + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) ) offset += 1 << log_byte_pad @@ -1206,7 +1192,7 @@ def verify_execution( sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in tables)) committed = { - name: [(from_end(gkr_point, table_log_heights[name]), dict(logup["columns_values"][name]), {})] + name: [(gkr_point[-table_log_heights[name]:], dict(logup["columns_values"][name]), {})] for name in ALL_TABLES_ORDER } my_air_final = ZERO @@ -1221,7 +1207,7 @@ def verify_execution( natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = math.prod(sc_point[: n_max - log_n_rows]) my_air_final = ( - my_air_final + k_t * eq_poly(from_end(gkr_point, log_n_rows), natural_pt) * constraint_eval + my_air_final + k_t * eq_poly(gkr_point[-log_n_rows:], natural_pt) * constraint_eval ) eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} @@ -1238,12 +1224,12 @@ def verify_execution( previous = [ SparseStatements( stacked_n_vars, - from_end(gkr_point, log_memory), + gkr_point[-log_memory:], [(0, logup["value_memory"]), (1, logup["value_memory_acc"])], ), SparseStatements(stacked_n_vars, pm_point, [(0, pm_eval)]), SparseStatements( - stacked_n_vars, from_end(gkr_point, bytecode_log_size), [(bytecode_acc_idx, logup["value_bytecode_acc"])] + stacked_n_vars, gkr_point[-bytecode_log_size:], [(bytecode_acc_idx, logup["value_bytecode_acc"])] ), ] global_statements = stacked_pcs_global_statements( From d31de48e1325396ad7d46be45037fd91785d21e8 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 26 May 2026 22:07:37 +0400 Subject: [PATCH 48/69] wip --- crates/lean_prover/primitives.py | 11 +- crates/lean_prover/verifier.py | 170 ++++++++++++++----------------- 2 files changed, 84 insertions(+), 97 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index dbbf5371c..5e6623aa3 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -4,14 +4,15 @@ from itertools import accumulate, repeat from typing import Final, Sequence -P: Final = 2**31 - 2**24 + 1 # Koalabear prime +P: Final = 2**31 - 2**24 + 1 # Koalabear prime TWO_ADICITY = 24 -MDS_FIRST_ROW_16: Final = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) # for Poseidon +MDS_FIRST_ROW_16: Final = (1, 1, 51, 1, 11, 17, 2, 1, 101, 63, 15, 2, 67, 22, 13, 3) # for Poseidon KB_TWO_ADIC_GENERATORS: Final = tuple(pow(0x6AC49F88, 1 << (TWO_ADICITY - b), P) for b in range(TWO_ADICITY + 1)) SPONGE_RATE, SPONGE_STATE, DIGEST_ELEMS = 8, 16, 8 SPONGE_CAPACITY = SPONGE_STATE - SPONGE_RATE + class Fp: """An element of the KoalaBear prime field `F_p`.""" @@ -127,7 +128,6 @@ def ef_powers(x: EF, n: int) -> list[EF]: return list(accumulate(repeat(x, n), lambda a, _: a * x, initial=ONE))[:n] - # 448 raw Poseidon1-KoalaBear width-16 round constants generated by the Grain # LFSR (Poseidon paper §5.3, parameters field_type=1, α=3, n=31, t=16, R_F=8, # R_P=20). Reference: https://github.com/Plonky3/Plonky3/blob/main/poseidon1/generate_constants.py @@ -251,6 +251,7 @@ def mds_mul() -> None: POSEIDON16 = Poseidon1(PARAMS_16) + def poseidon16_compress(left: Sequence[Fp], right: Sequence[Fp]) -> list[Fp]: state = list(left) + list(right) assert len(state) == SPONGE_STATE @@ -343,9 +344,7 @@ def _compute_air_sparse_constants() -> dict: w_col = [m_mul[i + 1][0] for i in range(15)] sub = [[m_mul[i + 1][j + 1] for j in range(15)] for i in range(15)] m_hat_inv = _gauss_jordan_inv(sub, 15) - w_hat = [ - sum(m_hat_inv[i][k] * w_col[k] for k in range(15)) % P if i < 15 else 0 for i in range(w) - ] + w_hat = [sum(m_hat_inv[i][k] * w_col[k] for k in range(15)) % P if i < 15 else 0 for i in range(w)] v_collection.append(v_row) w_hat_collection.append(w_hat) m_i = [row[:] for row in m_mul] diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 1e4fb1672..0eead6ce0 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -4,13 +4,16 @@ Run: python3 crates/lean_prover/verifier.py Format: - ruff format --line-length 120 crates/lean_prover/verifier.py + ruff format --line-length 120 crates/lean_prover """ from __future__ import annotations -import functools +import array +import json import math +import sys from dataclasses import dataclass +from pathlib import Path from typing import Sequence from primitives import * @@ -41,7 +44,7 @@ class ProofError(Exception): # T-Sponge (compression instead of permutation) with replacement (instead of xoring / adding the ingested data). def sponge_hash(data: Sequence[Fp]) -> list[Fp]: assert len(data) % SPONGE_RATE == 0 and len(data) > 0 - state = [Fp(len(data))] + [Fp(0)] * (SPONGE_CAPACITY - 1) # IV = [size, 0, 0, 0, ..., 0] + state = [Fp(len(data))] + [Fp(0)] * (SPONGE_CAPACITY - 1) # IV = [size, 0, 0, 0, ..., 0] for k in range(len(data) // SPONGE_RATE): state = poseidon16_compress(state, data[k * SPONGE_RATE : (k + 1) * SPONGE_RATE]) return state @@ -170,12 +173,12 @@ def merkle_verify_path( def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: - return list(accumulate(repeat(x, num_variables), lambda a, _: a * a)) # [x, x², x⁴, …, x^(2^(n−1))] + return list(accumulate(repeat(x, num_variables), lambda a, _: a * a)) # [x, x², x⁴, …, x^(2^(n−1))] def eq_poly(a: Sequence[EF], b: Sequence[EF]) -> EF: assert len(a) == len(b) - return math.prod(x*y + (ONE - x) * (ONE - y) for x, y in zip(a, b)) + return math.prod(x * y + (ONE - x) * (ONE - y) for x, y in zip(a, b)) def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: @@ -243,7 +246,6 @@ def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: return n, nv - n * WHIR_SUBSEQUENT_FOLDING_FACTOR - def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: return num_variables + start_rate - (RS_DOMAIN_INITIAL_REDUCTION_FACTOR + r - 1 if r >= 1 else 0) @@ -252,23 +254,20 @@ def two_adic_generator(bits: int) -> Fp: return Fp(KB_TWO_ADIC_GENERATORS[bits]) -@functools.cache -def whir_config(log_inv_rate: int, num_variables: int) -> dict: - for c in WHIR_CONFIGS: - if (c[0], c[1]) == (log_inv_rate, num_variables): - return { - "log_inv_rate": c[0], - "num_variables": c[1], - "commitment_ood_samples": c[2], - "starting_folding_pow_bits": c[3], - "final_queries": c[4], - "final_query_pow_bits": c[5], - "rounds": [ - {"num_queries": r[0], "ood_samples": r[1], "query_pow_bits": r[2], "folding_pow_bits": r[3]} - for r in c[6] - ], - } - raise KeyError(f"No WHIR config for (log_inv_rate={log_inv_rate}, num_variables={num_variables}).") +WHIR_CONFIG_BY_KEY = { + (c[0], c[1]): { + "log_inv_rate": c[0], + "num_variables": c[1], + "commitment_ood_samples": c[2], + "starting_folding_pow_bits": c[3], + "final_queries": c[4], + "final_query_pow_bits": c[5], + "rounds": [ + {"num_queries": r[0], "ood_samples": r[1], "query_pow_bits": r[2], "folding_pow_bits": r[3]} for r in c[6] + ], + } + for c in WHIR_CONFIGS +} @dataclass @@ -470,31 +469,32 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non return folding_flat -def _table_buses(name: str, n_columns: int) -> tuple: - if name == "execution": - return ( - ("col_mult", "Push"), - ("byte_lookup",), - ("mem_group", 2, 5, 1), # addr_a, value_a - ("mem_group", 3, 6, 1), # addr_b, value_b - ("mem_group", 4, 7, 1), # addr_c, value_c - ) - if name == "extension_op": - return ( - ("col_mult", "Pull"), - ("mem_group", 6, 14, 5), # idx_a, va - ("mem_group", 7, 19, 5), # idx_b, vb - ("mem_group", 13, 24, 5), # idx_res, vres - ) - if name == "poseidon16_compress": - return ( - ("col_mult", "Pull"), - ("mem_group", 6, 9, 4), - ("mem_group", 7, 13, 4), - ("mem_group", 1, 17, 8), - ("mem_group", 2, n_columns - 16, 16), - ) - raise ProofError(f"unknown table: {name}") +# poseidon16_compress: 9 scalars + 16 inputs + half_full_rounds*WIDTH (begin + end-1) + partial + WIDTH outputs. +_POSEIDON_N_COLS = ( + 25 + 32 * (POSEIDON1_AIR_CONSTANTS["half_full_rounds"] // 2) + POSEIDON1_AIR_CONSTANTS["partial_rounds"] +) +TABLE_BUSES = { + "execution": ( + ("col_mult", "Push"), + ("byte_lookup",), + ("mem_group", 2, 5, 1), # addr_a, value_a + ("mem_group", 3, 6, 1), # addr_b, value_b + ("mem_group", 4, 7, 1), # addr_c, value_c + ), + "extension_op": ( + ("col_mult", "Pull"), + ("mem_group", 6, 14, 5), # idx_a, va + ("mem_group", 7, 19, 5), # idx_b, vb + ("mem_group", 13, 24, 5), # idx_res, vres + ), + "poseidon16_compress": ( + ("col_mult", "Pull"), + ("mem_group", 6, 9, 4), + ("mem_group", 7, 13, 4), + ("mem_group", 1, 17, 8), + ("mem_group", 2, _POSEIDON_N_COLS - 16, 16), + ), +} @dataclass(frozen=True) @@ -513,19 +513,6 @@ def n_buses(self) -> int: return sum(b[3] if b[0] == "mem_group" else 1 for b in self.buses) -def default_tables() -> list[TableMeta]: - # (n_columns, air_degree, n_constraints, n_shift, air_fn) per table — all constants. - # poseidon16_compress: 9 scalars + 16 inputs + half_full_rounds*WIDTH (begin + end-1) + partial + WIDTH outputs. - p = POSEIDON1_AIR_CONSTANTS - poseidon_n_cols = 25 + 32 * (p["half_full_rounds"] // 2) + p["partial_rounds"] - specs = ( - ("execution", N_INSTRUCTION_COLUMNS + N_RUNTIME_COLUMNS, 5, 14, 2, _eval_air_execution), - ("extension_op", 29, 6, 35, 13, _eval_air_extension_op), - ("poseidon16_compress", poseidon_n_cols, 10, 101, 0, _eval_air_poseidon16), - ) - return [TableMeta(name, n, _table_buses(name, n), d, k, s, fn) for name, n, d, k, s, fn in specs] - - def stacked_pcs_global_statements( stacked_n_vars: int, memory_n_vars: int, @@ -697,7 +684,9 @@ def pref_at(offset: int, log_height: int) -> EF: pref = pref_at(offset, log_bytecode) pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() - bytecode_value = eval_multilinear_evals([EF(v) for v in bytecode_multilinear], list(byte_pt) + list(alphas[-log_instr:])) + bytecode_value = eval_multilinear_evals( + [EF(v) for v in bytecode_multilinear], list(byte_pt) + list(alphas[-log_instr:]) + ) correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( bytecode_value * correction @@ -705,11 +694,7 @@ def pref_at(offset: int, log_height: int) -> EF: + alphas_eq_poly[-1] * EF(ds_byte) ) num = num - pref * value_bytecode_acc - den = ( - den - + pref * (c - fp_byte) - + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) - ) + den = den + pref * (c - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) offset += 1 << log_byte_pad # Per-table base offsets in the GKR layout are assigned in sorted-by-height order @@ -818,7 +803,7 @@ def assert_bool(self, x: EF) -> None: def _eval_bus_virtual( folder: "ConstraintFolder", extra_data: dict, multiplicity: EF, discriminator: EF, data: Sequence[EF] ) -> None: - alphas: list[EF] = extra_data["logup_alphas_eq_poly"] + alphas: list[EF] = extra_data["logup_alphas_eq"] assert len(data) < len(alphas) folder.assert_zero(multiplicity) encoded = sum(a * d for a, d in zip(alphas, data)) + alphas[-1] * discriminator @@ -945,8 +930,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, extra_data: dict) -> None: folder.assert_zero(start_sh * (len_col - ONE)) -@functools.cache -def _p1c() -> dict: +def _build_p1c() -> dict: raw = POSEIDON1_AIR_CONSTANTS fp_mat = lambda m: [[Fp(v) for v in row] for row in m] fp_vec = lambda v: [Fp(x) for x in v] @@ -973,6 +957,9 @@ def _p1c() -> dict: } +P1C = _build_p1c() + + _POSEIDON_WIDTH = 16 _HALF_DIGEST_LEN = 4 _POSEIDON_DISCRIMINATOR_BASE = 3 # odd ≥ 3, disjoint from memory (1) / bytecode (2) @@ -990,14 +977,14 @@ def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: for rc in (rc1, rc2): sbox = [(t := s + c) * t * t for s, c in zip(state, rc)] - state = _matvec_kb(_p1c()["mds_dense"], sbox) + state = _matvec_kb(P1C["mds_dense"], sbox) return state def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: """AIR for Poseidon1-16. Each `post` column commits an intermediate state, which we constrain against the local computation, then adopt to bound polynomial degree.""" - const = _p1c() + const = P1C state = list(cols["inputs"]) initial = list(cols["inputs"]) half_initial = half_final = const["half_full_rounds"] // 2 @@ -1043,7 +1030,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: def _eval_air_poseidon16(folder: ConstraintFolder, extra_data: dict) -> None: - const = _p1c() + const = P1C flat, W = folder.flat, _POSEIDON_WIDTH half_initial = half_final = const["half_full_rounds"] // 2 @@ -1096,12 +1083,23 @@ def take(n: int) -> list[EF]: ) +# (n_columns, air_degree, n_constraints, n_shift, air_fn) per table — all constants. +DEFAULT_TABLES = [ + TableMeta(name, n, TABLE_BUSES[name], d, k, s, fn) + for name, n, d, k, s, fn in ( + ("execution", N_INSTRUCTION_COLUMNS + N_RUNTIME_COLUMNS, 5, 14, 2, _eval_air_execution), + ("extension_op", 29, 6, 35, 13, _eval_air_extension_op), + ("poseidon16_compress", _POSEIDON_N_COLS, 10, 101, 0, _eval_air_poseidon16), + ) +] + + def verify_execution( public_input: Sequence[Fp], proof: Proof, bytecode_multilinear: list[int], ): - tables = default_tables() + tables = DEFAULT_TABLES bytecode_log_size = log2_strict(len(bytecode_multilinear)) - log2_ceil(N_INSTRUCTION_COLUMNS) ending_pc = (1 << bytecode_log_size) - 1 bytecode_hash = sponge_hash([Fp(v) for v in bytecode_multilinear]) @@ -1122,9 +1120,7 @@ def verify_execution( if log_memory < max(max(table_log_n_rows, default=0), bytecode_log_size): raise ProofError("InvalidProof: memory smaller than tables/bytecode") for t, h in zip(tables, table_log_n_rows): - limit = MAX_LOG_N_ROWS_PER_TABLE.get(t.name) - if limit is None: - raise ProofError(f"InvalidProof: unknown table {t.name}") + limit = MAX_LOG_N_ROWS_PER_TABLE[t.name] if not MIN_LOG_N_ROWS_PER_TABLE <= h <= limit: raise ProofError( f"InvalidProof: table {t.name} log_n_rows={h} not in [{MIN_LOG_N_ROWS_PER_TABLE}, {limit}]" @@ -1143,7 +1139,7 @@ def verify_execution( stacked_n_vars = log2_ceil(total_stacked) if stacked_n_vars > TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") - cfg = whir_config(log_inv_rate, stacked_n_vars) + cfg = WHIR_CONFIG_BY_KEY[(log_inv_rate, stacked_n_vars)] nood = cfg["commitment_ood_samples"] parsed_commitment = ParsedCommitment( stacked_n_vars, @@ -1179,7 +1175,7 @@ def verify_execution( cumulative += tables_by_name[name].n_constraints alpha_powers = ef_powers(air_alpha, cumulative) - extra_data = {"logup_alphas_eq_poly": logup_alphas_eq} + extra_data = {"logup_alphas_eq": logup_alphas_eq} # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (c − bus_den)). The # sign is the direction of each table's unique Column-multiplicity bus. @@ -1192,8 +1188,7 @@ def verify_execution( sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in tables)) committed = { - name: [(gkr_point[-table_log_heights[name]:], dict(logup["columns_values"][name]), {})] - for name in ALL_TABLES_ORDER + name: [(gkr_point[-table_log_heights[name] :], logup["columns_values"][name], {})] for name in ALL_TABLES_ORDER } my_air_final = ZERO for name in ALL_TABLES_ORDER: @@ -1206,9 +1201,7 @@ def verify_execution( natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = math.prod(sc_point[: n_max - log_n_rows]) - my_air_final = ( - my_air_final + k_t * eq_poly(gkr_point[-log_n_rows:], natural_pt) * constraint_eval - ) + my_air_final = my_air_final + k_t * eq_poly(gkr_point[-log_n_rows:], natural_pt) * constraint_eval eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} next_vals = {j: col_evals[meta.n_columns + j] for j in range(meta.n_shift)} @@ -1253,14 +1246,11 @@ def verify_execution( def main() -> int: - import array, json - from pathlib import Path - vector_path = Path(__file__).resolve().parents[2] / "target" / "zkvm_test_vectors" / "proof.json" if not vector_path.exists(): print( f"Test vector not found at {vector_path}. Generate it first with:\n" - " cargo test --release -p lean_prover dump_zkvm_vector -- --nocapture" + " cargo test --release -p lean_prover dump_test_vector_for_python_verifier -- --nocapture" ) return 1 @@ -1293,6 +1283,4 @@ def main() -> int: if __name__ == "__main__": - import sys as _sys - - _sys.exit(main()) + sys.exit(main()) From af4c7afe222d1db831f675400ab34e7436fcce85 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 00:51:11 +0400 Subject: [PATCH 49/69] wip --- crates/lean_prover/verifier.py | 171 +++++++++++++++------------------ 1 file changed, 79 insertions(+), 92 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 0eead6ce0..fcf286a86 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -23,18 +23,32 @@ WHIR_INITIAL_FOLDING_FACTOR, WHIR_SUBSEQUENT_FOLDING_FACTOR, WHIR_MAX_NUM_VARIABLES_TO_SEND_COEFFS = 7, 5, 8 MIN_WHIR_LOG_INV_RATE, MAX_WHIR_LOG_INV_RATE, RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 1, 4, 5 -# (log_inv_rate, num_variables, commitment_ood_samples, starting_folding_pow_bits, final_queries, final_query_pow_bits, rounds) -# where `rounds = ((num_queries, ood_samples, query_pow_bits, folding_pow_bits), ...)`. (checked by `cargo test -p lean_prover --test check_whir_configs`) -WHIR_CONFIGS = ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # fmt: skip +WHIR_CONFIGS = { + (c[0], c[1]): { + "log_inv_rate": c[0], + "num_variables": c[1], + "commitment_ood_samples": c[2], + "starting_folding_pow_bits": c[3], + "final_queries": c[4], + "final_query_pow_bits": c[5], + "rounds": [ + {"num_queries": r[0], "ood_samples": r[1], "query_pow_bits": r[2], "folding_pow_bits": r[3]} for r in c[6] + ], + } + for c in ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # fmt: skip +} MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, MAX_BYTECODE_LOG_SIZE = 8, 8, 22 -MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension_op": 21, "poseidon16_compress": 21} -ALL_TABLES_ORDER = ("execution", "extension_op", "poseidon16_compress") +MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension": 21, "poseidon": 21} +ALL_TABLES_ORDER = ("execution", "extension", "poseidon") +N_VARS_TO_SEND_GKR_COEFFS = 5 -N_RUNTIME_COLUMNS, N_INSTRUCTION_COLUMNS, COL_PC = 8, 12, 0 +N_RUNTIME_COLUMNS, N_INSTRUCTION_COLUMNS, PC_COL_INDEX = 8, 12, 0 LOGUP_MEMORY_DOMAINSEP, LOGUP_BYTECODE_DOMAINSEP = 1, 2 -STARTING_PC = 0 +STARTING_PC = 0 # every program starts at PC = 0, and ends at PC = len(bytecode) - 1 + +POSEIDON_WIDTH, POSEIDON_FULL_ROUNDS, POSEIDON_PARTIAL_ROUNDS = 16, 8, 20 class ProofError(Exception): @@ -50,10 +64,6 @@ def sponge_hash(data: Sequence[Fp]) -> list[Fp]: return state -def fiat_shamir_domain_sep(bytecode_hash: Sequence[Fp]) -> list[Fp]: - return poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP) - - class Challenger: # https://eprint.iacr.org/2025/536.pdf def __init__(self, initial_capacity: Sequence[Fp]) -> None: self.state: list[Fp] = list(initial_capacity) + [Fp(0)] * SPONGE_RATE @@ -156,7 +166,7 @@ def check_pow_grinding(self, bits: int) -> None: def merkle_verify_path( - commit: list[Fp], + root: list[Fp], log_height: int, index: int, opened_values: Sequence[Fp], @@ -165,11 +175,11 @@ def merkle_verify_path( if len(opening_proof) != log_height: return False chunks = [list(opened_values[i : i + SPONGE_RATE]) for i in range(0, len(opened_values), SPONGE_RATE)] - cur = sponge_hash([x for c in reversed(chunks) for x in c]) + current = sponge_hash([x for c in reversed(chunks) for x in c]) for sibling in opening_proof: - cur = poseidon16_compress(cur, sibling) if index & 1 == 0 else poseidon16_compress(sibling, cur) + current = poseidon16_compress(current, sibling) if index & 1 == 0 else poseidon16_compress(sibling, current) index >>= 1 - return list(commit) == list(cur) + return root == current def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: @@ -181,6 +191,11 @@ def eq_poly(a: Sequence[EF], b: Sequence[EF]) -> EF: return math.prod(x * y + (ONE - x) * (ONE - y) for x, y in zip(a, b)) +def dot_product(a: Sequence, b: Sequence): + """`Σᵢ aᵢ · bᵢ` over the common prefix of `a` and `b`.""" + return sum(x * y for x, y in zip(a, b)) + + def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: assert len(x) == len(y) s, eq_prefix = ZERO, ONE @@ -253,27 +268,10 @@ def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: def two_adic_generator(bits: int) -> Fp: return Fp(KB_TWO_ADIC_GENERATORS[bits]) - -WHIR_CONFIG_BY_KEY = { - (c[0], c[1]): { - "log_inv_rate": c[0], - "num_variables": c[1], - "commitment_ood_samples": c[2], - "starting_folding_pow_bits": c[3], - "final_queries": c[4], - "final_query_pow_bits": c[5], - "rounds": [ - {"num_queries": r[0], "ood_samples": r[1], "query_pow_bits": r[2], "folding_pow_bits": r[3]} for r in c[6] - ], - } - for c in WHIR_CONFIGS -} - - @dataclass class ParsedCommitment: num_variables: int - root: list[Fp] # length DIGEST_ELEMS + root: list[Fp] ood_points: list[EF] ood_answers: list[EF] @@ -308,19 +306,6 @@ def verify_sumcheck( return point, target -def combine_constraints(state: VerifierState, target: EF, constraints: list[SparseStatements]) -> tuple[EF, list[EF]]: - """Fold all constraint values into `target` via powers of γ; return those γ-power weights.""" - gamma: EF = state.sample_ef() - combo: list[EF] = [] - g = ONE - for smt in constraints: - for _, value in smt.values: - target = target + g * value - combo.append(g) - g = g * gamma - return target, combo - - def verify_stir_challenges( state: VerifierState, cfg: dict, @@ -337,17 +322,17 @@ def verify_stir_challenges( state.check_pow_grinding(query_pow_bits) indices = state.sample_in_range(log_height, num_queries) - def pack_answers(leaf: list[Fp]) -> list[EF]: - if round_index == 0: - return [EF(f) for f in leaf] - return [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] - constraints: list[SparseStatements] = [] for idx in indices: op = state.next_merkle_opening() if not merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path): raise ProofError("Merkle verification failed") - fold = eval_multilinear_evals(pack_answers(op.leaf_data), folding_randomness) + leaf = op.leaf_data + if round_index == 0: + packed = [EF(f) for f in leaf] + else: + packed = [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] + fold = eval_multilinear_evals(packed, folding_randomness) ef_pt = EF(pow(int(gen.value), idx, P)) constraints.append(SparseStatements.dense(expand_from_univariate(ef_pt, num_variables), fold)) return constraints @@ -399,9 +384,17 @@ def whir_verify( def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> None: nonlocal target state.duplex() - new_target, combo = combine_constraints(state, target, constraints) + # Fold each constraint value into `target` via successive powers of γ. + gamma = state.sample_ef() + combo: list[EF] = [] + g = ONE + for smt in constraints: + for _, value in smt.values: + target = target + g * value + combo.append(g) + g = g * gamma round_constraints.append((combo, constraints)) - sc_point, target = verify_sumcheck(state, new_target, n_fold, 2, pow_bits) + sc_point, target = verify_sumcheck(state, target, n_fold, 2, pow_bits) round_folding.append(sc_point) step( @@ -469,10 +462,8 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non return folding_flat -# poseidon16_compress: 9 scalars + 16 inputs + half_full_rounds*WIDTH (begin + end-1) + partial + WIDTH outputs. -_POSEIDON_N_COLS = ( - 25 + 32 * (POSEIDON1_AIR_CONSTANTS["half_full_rounds"] // 2) + POSEIDON1_AIR_CONSTANTS["partial_rounds"] -) +# poseidon16_compress: 9 scalars + WIDTH inputs + (ROUNDS_F/2) pairs of WIDTH cols (begin + end-1 + outputs) + partial. +_POSEIDON_N_COLS = 9 + POSEIDON_WIDTH + POSEIDON_WIDTH * (POSEIDON_FULL_ROUNDS // 2) + POSEIDON_PARTIAL_ROUNDS TABLE_BUSES = { "execution": ( ("col_mult", "Push"), @@ -481,13 +472,13 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non ("mem_group", 3, 6, 1), # addr_b, value_b ("mem_group", 4, 7, 1), # addr_c, value_c ), - "extension_op": ( + "extension": ( ("col_mult", "Pull"), ("mem_group", 6, 14, 5), # idx_a, va ("mem_group", 7, 19, 5), # idx_b, vb ("mem_group", 13, 24, 5), # idx_res, vres ), - "poseidon16_compress": ( + "poseidon": ( ("col_mult", "Pull"), ("mem_group", 6, 9, 4), ("mem_group", 7, 13, 4), @@ -547,7 +538,9 @@ def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: if name == "execution": # PC column: pin first row to STARTING_PC, last row to ending_pc. for idx, pc in [(0, STARTING_PC), ((1 << n_vars) - 1, ending_pc)]: - out.append(SparseStatements.unique_value(stacked_n_vars, offset + (COL_PC << n_vars) + idx, EF(pc))) + out.append( + SparseStatements.unique_value(stacked_n_vars, offset + (PC_COL_INDEX << n_vars) + idx, EF(pc)) + ) for point, eq_values, next_values in committed_statements[name]: if next_values: out.append(SparseStatements.new_next(stacked_n_vars, list(point), values_at(next_values, col_base))) @@ -556,9 +549,6 @@ def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: return out -N_VARS_TO_SEND_GKR_COEFFS = 5 - - def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF], EF, EF]: """Layered sumcheck for `Σ nᵢ/dᵢ`. Returns `(quotient, point, claim_num, claim_den)`.""" assert n_vars > N_VARS_TO_SEND_GKR_COEFFS @@ -611,8 +601,7 @@ def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: def finger_print(discriminator: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: """`Σᵢ αᵢ · dataᵢ + α_last · discriminator`.""" assert len(alphas_eq_poly) > len(data) - acc = sum(a * d for a, d in zip(alphas_eq_poly, data)) - return acc + alphas_eq_poly[-1] * EF(discriminator) + return dot_product(alphas_eq_poly, data) + alphas_eq_poly[-1] * EF(discriminator) def sort_tables_by_height(table_log_heights: dict[str, int]) -> list[tuple[str, int]]: @@ -641,7 +630,7 @@ def verify_generic_logup( """GKR-quotient + section-by-section (memory/bytecode/per-table) reconstruction.""" n_instr_cols = N_INSTRUCTION_COLUMNS n_runtime_cols = N_RUNTIME_COLUMNS - col_pc = COL_PC + col_pc = PC_COL_INDEX ds_mem = Fp(LOGUP_MEMORY_DOMAINSEP) ds_byte = Fp(LOGUP_BYTECODE_DOMAINSEP) @@ -685,7 +674,7 @@ def pref_at(offset: int, log_height: int) -> EF: pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = state.next_extension_scalar() bytecode_value = eval_multilinear_evals( - [EF(v) for v in bytecode_multilinear], list(byte_pt) + list(alphas[-log_instr:]) + [EF(v) for v in bytecode_multilinear], byte_pt + alphas[-log_instr:] ) correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( @@ -806,7 +795,7 @@ def _eval_bus_virtual( alphas: list[EF] = extra_data["logup_alphas_eq"] assert len(data) < len(alphas) folder.assert_zero(multiplicity) - encoded = sum(a * d for a, d in zip(alphas, data)) + alphas[-1] * discriminator + encoded = dot_product(alphas, data) + alphas[-1] * discriminator folder.assert_zero(encoded) @@ -936,16 +925,14 @@ def _build_p1c() -> dict: fp_vec = lambda v: [Fp(x) for x in v] n = len(MDS_FIRST_ROW_16) mds_dense = [[Fp(MDS_FIRST_ROW_16[(j - i) % n]) for j in range(n)] for i in range(n)] - # External full-round RCs: first and last `half_full_rounds * width` entries of the - # raw 448-element round-constant table that drives the actual Poseidon permutation. - hf, t = raw["half_full_rounds"], SPONGE_STATE + # External full-round RCs: first and last `(ROUNDS_F/2) * WIDTH` entries of the + # raw round-constant table that drives the actual Poseidon permutation. + hf, t = POSEIDON_FULL_ROUNDS // 2, POSEIDON_WIDTH rcs = PARAMS_16.round_constants initial_constants = [[Fp(x) for x in rcs[i * t : (i + 1) * t]] for i in range(hf)] - tail_start = (hf + raw["partial_rounds"]) * t + tail_start = (hf + POSEIDON_PARTIAL_ROUNDS) * t final_constants = [[Fp(x) for x in rcs[tail_start + i * t : tail_start + (i + 1) * t]] for i in range(hf)] return { - "half_full_rounds": raw["half_full_rounds"], - "partial_rounds": raw["partial_rounds"], "initial_constants": initial_constants, "final_constants": final_constants, "sparse_m_i": fp_mat(raw["sparse_m_i"]), @@ -960,7 +947,6 @@ def _build_p1c() -> dict: P1C = _build_p1c() -_POSEIDON_WIDTH = 16 _HALF_DIGEST_LEN = 4 _POSEIDON_DISCRIMINATOR_BASE = 3 # odd ≥ 3, disjoint from memory (1) / bytecode (2) _POSEIDON_PERMUTE_SHIFT, _POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1, 1 << 2 @@ -971,7 +957,7 @@ def _build_p1c() -> dict: def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: - return [sum(s * m for s, m in zip(state, row)) for row in mat] + return [dot_product(state, row) for row in mat] def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: @@ -987,7 +973,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: const = P1C state = list(cols["inputs"]) initial = list(cols["inputs"]) - half_initial = half_final = const["half_full_rounds"] // 2 + half_initial = half_final = POSEIDON_FULL_ROUNDS // 4 for r in range(half_initial): state = _full_round(state, const["initial_constants"][2 * r], const["initial_constants"][2 * r + 1]) @@ -998,15 +984,15 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: state = [s + c for s, c in zip(state, const["sparse_first_rc"])] state = _matvec_kb(const["sparse_m_i"], state) - n_partial = const["partial_rounds"] + n_partial = POSEIDON_PARTIAL_ROUNDS for r in range(n_partial): folder.assert_eq(state[0] * state[0] * state[0], cols["partial_rounds"][r]) state[0] = cols["partial_rounds"][r] if r < n_partial - 1: state[0] = state[0] + const["sparse_scalar_rc"][r] old_s0 = state[0] - state[0] = sum(state[j] * const["sparse_first_row"][r][j] for j in range(_POSEIDON_WIDTH)) - for i in range(1, _POSEIDON_WIDTH): + state[0] = dot_product(state, const["sparse_first_row"][r]) + for i in range(1, POSEIDON_WIDTH): state[i] = state[i] + old_s0 * const["sparse_v"][r][i - 1] for r in range(half_final - 1): @@ -1022,17 +1008,16 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: flag_permute = cols["flag_permute"] not_permute = ONE - flag_permute compression_last4 = not_permute - cols["flag_half_output"] - for i in range(_POSEIDON_WIDTH // 2): + for i in range(POSEIDON_WIDTH // 2): gate = not_permute if i < _HALF_DIGEST_LEN else compression_last4 folder.assert_zero(gate * (state[i] + initial[i] - cols["outputs_left"][i])) folder.assert_zero(flag_permute * (state[i] - cols["outputs_left"][i])) - folder.assert_zero(flag_permute * (state[i + _POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) + folder.assert_zero(flag_permute * (state[i + POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) def _eval_air_poseidon16(folder: ConstraintFolder, extra_data: dict) -> None: - const = P1C - flat, W = folder.flat, _POSEIDON_WIDTH - half_initial = half_final = const["half_full_rounds"] // 2 + flat, W = folder.flat, POSEIDON_WIDTH + half_initial = half_final = POSEIDON_FULL_ROUNDS // 4 o = 0 @@ -1047,7 +1032,7 @@ def take(n: int) -> list[EF]: # fmt: on inputs = take(W) beginning_full_rounds = [take(W) for _ in range(half_initial)] - partial_cols = take(const["partial_rounds"]) + partial_cols = take(POSEIDON_PARTIAL_ROUNDS) ending_full_rounds = [take(W) for _ in range(half_final - 1)] outputs_left, outputs_right = take(W // 2), take(W // 2) @@ -1088,8 +1073,8 @@ def take(n: int) -> list[EF]: TableMeta(name, n, TABLE_BUSES[name], d, k, s, fn) for name, n, d, k, s, fn in ( ("execution", N_INSTRUCTION_COLUMNS + N_RUNTIME_COLUMNS, 5, 14, 2, _eval_air_execution), - ("extension_op", 29, 6, 35, 13, _eval_air_extension_op), - ("poseidon16_compress", _POSEIDON_N_COLS, 10, 101, 0, _eval_air_poseidon16), + ("extension", 29, 6, 35, 13, _eval_air_extension_op), + ("poseidon", _POSEIDON_N_COLS, 10, 101, 0, _eval_air_poseidon16), ) ] @@ -1106,8 +1091,10 @@ def verify_execution( if len(public_input) != PUBLIC_INPUT_SIZE: raise ProofError("InvalidProof: public_input length mismatch") - state = VerifierState(proof, fiat_shamir_domain_sep(bytecode_hash)) - state.observe_scalars(list(public_input)) + state = VerifierState( + proof, poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP) + ) # domain separator accross bytecode + state.observe_scalars(public_input) dims = [int(x.value) for x in state.next_base_scalars_vec(2 + len(tables))] log_inv_rate, log_memory, *table_log_n_rows = dims @@ -1139,7 +1126,7 @@ def verify_execution( stacked_n_vars = log2_ceil(total_stacked) if stacked_n_vars > TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") - cfg = WHIR_CONFIG_BY_KEY[(log_inv_rate, stacked_n_vars)] + cfg = WHIR_CONFIGS[(log_inv_rate, stacked_n_vars)] nood = cfg["commitment_ood_samples"] parsed_commitment = ParsedCommitment( stacked_n_vars, From 79c5e5b59673b7f3b73432105def3fb0ace36826 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 01:32:24 +0400 Subject: [PATCH 50/69] wip --- crates/lean_prover/verifier.py | 214 +++++++++++++++------------------ 1 file changed, 94 insertions(+), 120 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index fcf286a86..2fff03639 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -35,8 +35,8 @@ {"num_queries": r[0], "ood_samples": r[1], "query_pow_bits": r[2], "folding_pow_bits": r[3]} for r in c[6] ], } - for c in ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # fmt: skip -} + for c in ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) +} # fmt: skip MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, MAX_BYTECODE_LOG_SIZE = 8, 8, 22 @@ -45,7 +45,16 @@ N_VARS_TO_SEND_GKR_COEFFS = 5 N_RUNTIME_COLUMNS, N_INSTRUCTION_COLUMNS, PC_COL_INDEX = 8, 12, 0 + LOGUP_MEMORY_DOMAINSEP, LOGUP_BYTECODE_DOMAINSEP = 1, 2 +POSEIDON_DISCRIMINATOR_BASE = 3 # odd ≥ 3 +POSEIDON_PERMUTE_SHIFT, POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1, 1 << 2 +POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT, POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = ( + 1 << 3, + 1 << 4, +) + + STARTING_PC = 0 # every program starts at PC = 0, and ends at PC = len(bytecode) - 1 POSEIDON_WIDTH, POSEIDON_FULL_ROUNDS, POSEIDON_PARTIAL_ROUNDS = 16, 8, 20 @@ -64,7 +73,7 @@ def sponge_hash(data: Sequence[Fp]) -> list[Fp]: return state -class Challenger: # https://eprint.iacr.org/2025/536.pdf +class DuplexSpongeChallenger: # https://eprint.iacr.org/2025/536.pdf def __init__(self, initial_capacity: Sequence[Fp]) -> None: self.state: list[Fp] = list(initial_capacity) + [Fp(0)] * SPONGE_RATE self.rate_fresh: bool = False @@ -121,7 +130,7 @@ class Proof: merkle_openings: list[MerkleOpening] -class VerifierState(Challenger): +class FiatShamir(DuplexSpongeChallenger): def __init__(self, proof: Proof, initial_capacity: Sequence[Fp]) -> None: super().__init__(initial_capacity) self.transcript = list(proof.transcript) @@ -171,15 +180,16 @@ def merkle_verify_path( index: int, opened_values: Sequence[Fp], opening_proof: Sequence[list[Fp]], -) -> bool: +) -> None: if len(opening_proof) != log_height: - return False + raise ProofError("Merkle verification failed: opening proof has wrong length") chunks = [list(opened_values[i : i + SPONGE_RATE]) for i in range(0, len(opened_values), SPONGE_RATE)] current = sponge_hash([x for c in reversed(chunks) for x in c]) for sibling in opening_proof: current = poseidon16_compress(current, sibling) if index & 1 == 0 else poseidon16_compress(sibling, current) index >>= 1 - return root == current + if root != current: + raise ProofError("Merkle verification failed: root mismatch") def expand_from_univariate(x: EF, num_variables: int) -> list[EF]: @@ -192,7 +202,6 @@ def eq_poly(a: Sequence[EF], b: Sequence[EF]) -> EF: def dot_product(a: Sequence, b: Sequence): - """`Σᵢ aᵢ · bᵢ` over the common prefix of `a` and `b`.""" return sum(x * y for x, y in zip(a, b)) @@ -225,6 +234,13 @@ def eval_multilinear_coeffs(coeffs: Sequence[EF], point: Sequence[EF]) -> EF: return lo + hi * point[0] +def eval_univariate_polynomial(coeffs: list[EF], x: EF) -> EF: + acc = ZERO + for c in reversed(coeffs): + acc = acc * x + c + return acc + + @dataclass class SparseStatements: total_num_variables: int @@ -261,13 +277,6 @@ def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: return n, nv - n * WHIR_SUBSEQUENT_FOLDING_FACTOR -def whir_log_domain_size_at(num_variables: int, start_rate: int, r: int) -> int: - return num_variables + start_rate - (RS_DOMAIN_INITIAL_REDUCTION_FACTOR + r - 1 if r >= 1 else 0) - - -def two_adic_generator(bits: int) -> Fp: - return Fp(KB_TWO_ADIC_GENERATORS[bits]) - @dataclass class ParsedCommitment: num_variables: int @@ -282,51 +291,39 @@ def oods_constraints(self) -> list[SparseStatements]: ] -def eval_univariate_polynomial(coeffs: list[EF], x: EF) -> EF: - acc = ZERO - for c in reversed(coeffs): - acc = acc * x + c - return acc - - def verify_sumcheck( - state: VerifierState, target: EF, n_vars: int, degree: int, pow_bits: int = 0 + fiat_shamir: FiatShamir, target: EF, n_rounds: int, degree: int, pow_bits: int = 0 ) -> tuple[list[EF], EF]: - """Round-by-round: check `h(0) + h(1) == target`, grind, sample, fold. Returns (point, value).""" point: list[EF] = [] - for _ in range(n_vars): - coeffs = state.next_extension_scalars_vec(degree + 1) + for _ in range(n_rounds): + coeffs = fiat_shamir.next_extension_scalars_vec(degree + 1) s = coeffs[0] + sum(coeffs) if s != target: raise ProofError("Sumcheck identity failed: h(0) + h(1) != target") - state.check_pow_grinding(pow_bits) - r = state.sample_ef() + fiat_shamir.check_pow_grinding(pow_bits) + r = fiat_shamir.sample_ef() point.append(r) target = eval_univariate_polynomial(coeffs, r) return point, target def verify_stir_challenges( - state: VerifierState, - cfg: dict, + fiat_shamir: FiatShamir, round_index: int, + log_height: int, num_variables: int, - folding_factor: int, num_queries: int, query_pow_bits: int, commitment: ParsedCommitment, folding_randomness: list[EF], ) -> list[SparseStatements]: - log_height = whir_log_domain_size_at(cfg["num_variables"], cfg["log_inv_rate"], round_index) - folding_factor - gen = two_adic_generator(log_height) - state.check_pow_grinding(query_pow_bits) - indices = state.sample_in_range(log_height, num_queries) - + gen = Fp(KB_TWO_ADIC_GENERATORS[log_height]) + fiat_shamir.check_pow_grinding(query_pow_bits) + indices = fiat_shamir.sample_in_range(log_height, num_queries) constraints: list[SparseStatements] = [] for idx in indices: - op = state.next_merkle_opening() - if not merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path): - raise ProofError("Merkle verification failed") + op = fiat_shamir.next_merkle_opening() + merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path) leaf = op.leaf_data if round_index == 0: packed = [EF(f) for f in leaf] @@ -338,16 +335,6 @@ def verify_stir_challenges( return constraints -def verify_constraint_coeffs(constraint: SparseStatements, coeffs: list[EF]) -> bool: - """Checks `point == [α, α², α⁴, …]` and `Σ coeffs[i]·α^i == value`.""" - assert constraint.selector_num_variables == 0 - alpha = constraint.point[0] - if any(a * a != b for a, b in zip(constraint.point, constraint.point[1:])): - return False - univ_eval = eval_univariate_polynomial(coeffs, alpha) - return all(univ_eval == v[1] for v in constraint.values) - - def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatements]]], point: list[EF]) -> EF: value = ZERO pt = list(point) @@ -368,14 +355,11 @@ def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatement def whir_verify( - state: VerifierState, + fiat_shamir: FiatShamir, cfg: dict, parsed_commitment: ParsedCommitment, - statement: list[SparseStatements], + statements: list[SparseStatements], ) -> list[EF]: - for s in statement: - assert s.total_num_variables == parsed_commitment.num_variables - n_rounds, final_sumcheck_rounds = whir_n_rounds_and_final_sumcheck(cfg["num_variables"]) round_constraints: list[tuple[list[EF], list[SparseStatements]]] = [] round_folding: list[list[EF]] = [] @@ -383,9 +367,9 @@ def whir_verify( def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> None: nonlocal target - state.duplex() + fiat_shamir.duplex() # Fold each constraint value into `target` via successive powers of γ. - gamma = state.sample_ef() + gamma = fiat_shamir.sample_ef() combo: list[EF] = [] g = ONE for smt in constraints: @@ -394,63 +378,66 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non combo.append(g) g = g * gamma round_constraints.append((combo, constraints)) - sc_point, target = verify_sumcheck(state, target, n_fold, 2, pow_bits) + sc_point, target = verify_sumcheck(fiat_shamir, target, n_fold, 2, pow_bits) round_folding.append(sc_point) step( - parsed_commitment.oods_constraints() + statement, + parsed_commitment.oods_constraints() + statements, whir_folding_factor_at_round(0), cfg["starting_folding_pow_bits"], ) prev_commitment = parsed_commitment - nvars_round = cfg["num_variables"] + current_vars = cfg["num_variables"] + log_domain = cfg["num_variables"] + cfg["log_inv_rate"] for r in range(n_rounds): - rp = cfg["rounds"][r] - nvars_round -= whir_folding_factor_at_round(r) - nood = rp["ood_samples"] + round_params = cfg["rounds"][r] + current_vars -= whir_folding_factor_at_round(r) + n_ood_samples = round_params["ood_samples"] new_commitment = ParsedCommitment( - nvars_round, - state.next_base_scalars_vec(DIGEST_ELEMS), - state.sample_many_ef(nood), - state.next_extension_scalars_vec(nood), + current_vars, + fiat_shamir.next_base_scalars_vec(DIGEST_ELEMS), + fiat_shamir.sample_many_ef(n_ood_samples), + fiat_shamir.next_extension_scalars_vec(n_ood_samples), ) stir = verify_stir_challenges( - state, - cfg, + fiat_shamir, r, - nvars_round, - whir_folding_factor_at_round(r), - rp["num_queries"], - rp["query_pow_bits"], + log_domain - whir_folding_factor_at_round(r), + current_vars, + round_params["num_queries"], + round_params["query_pow_bits"], prev_commitment, round_folding[-1], ) step( new_commitment.oods_constraints() + stir, whir_folding_factor_at_round(r + 1), - rp["folding_pow_bits"], + round_params["folding_pow_bits"], ) + log_domain -= RS_DOMAIN_INITIAL_REDUCTION_FACTOR if r == 0 else 1 prev_commitment = new_commitment - n_vars_final = nvars_round - whir_folding_factor_at_round(n_rounds) - final_coeffs = state.next_extension_scalars_vec(1 << n_vars_final) + n_vars_final = current_vars - whir_folding_factor_at_round(n_rounds) + final_coeffs = fiat_shamir.next_extension_scalars_vec(1 << n_vars_final) final_stir = verify_stir_challenges( - state, - cfg, + fiat_shamir, n_rounds, + log_domain - whir_folding_factor_at_round(n_rounds), n_vars_final, - whir_folding_factor_at_round(n_rounds), cfg["final_queries"], cfg["final_query_pow_bits"], prev_commitment, round_folding[-1], ) + # Each STIR constraint's point is `expand_from_univariate(α, n)` = [α, α², α⁴, …]; check that `Σ coeffs[i]·α^i == value`. for smt in final_stir: - if not verify_constraint_coeffs(smt, final_coeffs): + assert smt.selector_num_variables == 0 + univ_eval = eval_univariate_polynomial(final_coeffs, smt.point[0]) + if any(univ_eval != v[1] for v in smt.values): raise ProofError("Final STIR constraint mismatch") - final_sc_point, final_sc_value = verify_sumcheck(state, target, final_sumcheck_rounds, 2) + final_sc_point, final_sc_value = verify_sumcheck(fiat_shamir, target, final_sumcheck_rounds, 2) round_folding.append(final_sc_point) folding_flat = [r for chunk in round_folding for r in chunk] @@ -549,27 +536,27 @@ def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: return out -def verify_gkr_quotient(state: VerifierState, n_vars: int) -> tuple[EF, list[EF], EF, EF]: +def verify_gkr_quotient(fiat_shamir: FiatShamir, n_vars: int) -> tuple[EF, list[EF], EF, EF]: """Layered sumcheck for `Σ nᵢ/dᵢ`. Returns `(quotient, point, claim_num, claim_den)`.""" assert n_vars > N_VARS_TO_SEND_GKR_COEFFS - nums = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) - dens = state.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) + nums = fiat_shamir.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) + dens = fiat_shamir.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) quotient = sum(n * d.inv() for n, d in zip(nums, dens)) - point = state.sample_many_ef(N_VARS_TO_SEND_GKR_COEFFS) + point = fiat_shamir.sample_many_ef(N_VARS_TO_SEND_GKR_COEFFS) claim_num = eval_multilinear_evals(nums, point) claim_den = eval_multilinear_evals(dens, point) for layer_n_vars in range(N_VARS_TO_SEND_GKR_COEFFS, n_vars): - state.duplex() - alpha = state.sample_ef() - raw_pt, sc_value = verify_sumcheck(state, claim_num + alpha * claim_den, layer_n_vars, 3) + fiat_shamir.duplex() + alpha = fiat_shamir.sample_ef() + raw_pt, sc_value = verify_sumcheck(fiat_shamir, claim_num + alpha * claim_den, layer_n_vars, 3) sc_point = list(reversed(raw_pt)) - nl, nr, dl, dr = state.next_extension_scalars_vec(4) + nl, nr, dl, dr = fiat_shamir.next_extension_scalars_vec(4) if sc_value != eq_poly(point, sc_point) * (alpha * dl * dr + nl * dr + nr * dl): raise ProofError("GKR step: postponed value mismatch") - beta = state.sample_ef() + beta = fiat_shamir.sample_ef() one_minus = ONE - beta claim_num = one_minus * nl + beta * nr claim_den = one_minus * dl + beta * dr @@ -618,7 +605,7 @@ def eval_eq(point: Sequence[EF]) -> list[EF]: def verify_generic_logup( - state: VerifierState, + fiat_shamir: FiatShamir, c: EF, alphas: list[EF], alphas_eq_poly: list[EF], @@ -645,7 +632,7 @@ def verify_generic_logup( ) total_gkr_n_vars = log2_ceil(total_active_len) - quotient, point_gkr, claim_num, claim_den = verify_gkr_quotient(state, total_gkr_n_vars) + quotient, point_gkr, claim_num, claim_den = verify_gkr_quotient(fiat_shamir, total_gkr_n_vars) if quotient != ZERO: raise ProofError("logup: GKR sum != 0") @@ -660,9 +647,9 @@ def pref_at(offset: int, log_height: int) -> EF: # Memory (data order: [value_index, value_memory] mirrors `crates/sub_protocols/src/logup.rs`). mem_pt = point_gkr[-log_memory:] pref = pref_at(0, log_memory) - value_memory_acc = state.next_extension_scalar() + value_memory_acc = fiat_shamir.next_extension_scalar() num = num - pref * value_memory_acc - value_memory = state.next_extension_scalar() + value_memory = fiat_shamir.next_extension_scalar() fp_mem = finger_print(ds_mem, [mle_of_01234567_etc(mem_pt), value_memory], alphas_eq_poly) den = den + pref * (c - fp_mem) offset = 1 << log_memory @@ -672,10 +659,8 @@ def pref_at(offset: int, log_height: int) -> EF: byte_pt = point_gkr[-log_bytecode:] pref = pref_at(offset, log_bytecode) pref_pad = pref_at(offset, log_byte_pad) - value_bytecode_acc = state.next_extension_scalar() - bytecode_value = eval_multilinear_evals( - [EF(v) for v in bytecode_multilinear], byte_pt + alphas[-log_instr:] - ) + value_bytecode_acc = fiat_shamir.next_extension_scalar() + bytecode_value = eval_multilinear_evals([EF(v) for v in bytecode_multilinear], byte_pt + alphas[-log_instr:]) correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( bytecode_value * correction @@ -712,14 +697,14 @@ def pref_at(offset: int, log_height: int) -> EF: for bus in meta.buses: pref = pref_at(offset_within_table, log_n_rows) if bus[0] == "col_mult": - bus_num_vals[name] = state.next_extension_scalar() - bus_den_vals[name] = state.next_extension_scalar() + bus_num_vals[name] = fiat_shamir.next_extension_scalar() + bus_den_vals[name] = fiat_shamir.next_extension_scalar() num = num + pref * bus_num_vals[name] den = den + pref * bus_den_vals[name] offset_within_table += row_stride elif bus[0] == "byte_lookup": cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [col_pc] - evals = state.next_extension_scalars_vec(len(cols)) + evals = fiat_shamir.next_extension_scalars_vec(len(cols)) for c_idx, e in zip(cols, evals): table_values[c_idx] = e num = num + pref # Push direction @@ -733,7 +718,7 @@ def pref_at(offset: int, log_height: int) -> EF: val_col = vals_start + i idx_fresh = idx_col not in table_values val_fresh = val_col not in table_values - evals = iter(state.next_extension_scalars_vec(idx_fresh + val_fresh)) + evals = iter(fiat_shamir.next_extension_scalars_vec(idx_fresh + val_fresh)) if idx_fresh: table_values[idx_col] = next(evals) if val_fresh: @@ -947,15 +932,6 @@ def _build_p1c() -> dict: P1C = _build_p1c() -_HALF_DIGEST_LEN = 4 -_POSEIDON_DISCRIMINATOR_BASE = 3 # odd ≥ 3, disjoint from memory (1) / bytecode (2) -_POSEIDON_PERMUTE_SHIFT, _POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1, 1 << 2 -_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT, _POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = ( - 1 << 3, - 1 << 4, -) - - def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: return [dot_product(state, row) for row in mat] @@ -1009,7 +985,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: not_permute = ONE - flag_permute compression_last4 = not_permute - cols["flag_half_output"] for i in range(POSEIDON_WIDTH // 2): - gate = not_permute if i < _HALF_DIGEST_LEN else compression_last4 + gate = not_permute if i < (DIGEST_ELEMS // 2) else compression_last4 folder.assert_zero(gate * (state[i] + initial[i] - cols["outputs_left"][i])) folder.assert_zero(flag_permute * (state[i] - cols["outputs_left"][i])) folder.assert_zero(flag_permute * (state[i + POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) @@ -1037,14 +1013,14 @@ def take(n: int) -> list[EF]: outputs_left, outputs_right = take(W // 2), take(W // 2) discriminator = ( - EF(_POSEIDON_DISCRIMINATOR_BASE) - + flag_permute * EF(_POSEIDON_PERMUTE_SHIFT) - + flag_half_output * EF(_POSEIDON_HALF_OUTPUT_SHIFT) - + flag_hardcoded_left * EF(_POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) - + flag_hardcoded_left * offset_hardcoded_left * EF(_POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT) + EF(POSEIDON_DISCRIMINATOR_BASE) + + flag_permute * EF(POSEIDON_PERMUTE_SHIFT) + + flag_half_output * EF(POSEIDON_HALF_OUTPUT_SHIFT) + + flag_hardcoded_left * EF(POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) + + flag_hardcoded_left * offset_hardcoded_left * EF(POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT) ) not_hcl = ONE - flag_hardcoded_left - index_a = eff_idx_left_second - not_hcl * EF(_HALF_DIGEST_LEN) + index_a = eff_idx_left_second - not_hcl * EF(DIGEST_ELEMS // 2) _eval_bus_virtual(folder, extra_data, multiplicity, discriminator, [index_a, index_b, index_res]) for f in (multiplicity, flag_half_output, flag_hardcoded_left, flag_permute): @@ -1091,9 +1067,7 @@ def verify_execution( if len(public_input) != PUBLIC_INPUT_SIZE: raise ProofError("InvalidProof: public_input length mismatch") - state = VerifierState( - proof, poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP) - ) # domain separator accross bytecode + state = FiatShamir(proof, poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP)) # domain separator accross bytecode state.observe_scalars(public_input) dims = [int(x.value) for x in state.next_base_scalars_vec(2 + len(tables))] From 7eb86288e20cbd769e1b11838a6bd3f99e00b0ce Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 01:50:15 +0400 Subject: [PATCH 51/69] wip --- crates/lean_prover/primitives.py | 14 ++- crates/lean_prover/verifier.py | 172 +++++++++++++++---------------- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index 5e6623aa3..09c0fc021 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -21,16 +21,22 @@ class Fp: def __init__(self, value: int) -> None: self.value = value % P - def __add__(self, other: "Fp") -> "Fp": + def __add__(self, other): + if not isinstance(other, Fp): + return NotImplemented # let EF.__radd__ / etc. handle mixed-type arithmetic. return Fp(self.value + other.value) - def __sub__(self, other: "Fp") -> "Fp": + def __sub__(self, other): + if not isinstance(other, Fp): + return NotImplemented return Fp(self.value - other.value) def __neg__(self) -> "Fp": return Fp(-self.value) - def __mul__(self, other: "Fp") -> "Fp": + def __mul__(self, other): + if not isinstance(other, Fp): + return NotImplemented return Fp(self.value * other.value) def __pow__(self, exponent: int) -> "Fp": @@ -82,6 +88,8 @@ def __add__(self, o): return EF([a + b for a, b in zip(self.c, o.c)]) def __sub__(self, o): + if isinstance(o, int): + return self if o == 0 else self - EF(o) if isinstance(o, Fp): return EF([self.c[0] - o, *self.c[1:]]) return EF([a - b for a, b in zip(self.c, o.c)]) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 2fff03639..17d115428 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -54,10 +54,34 @@ 1 << 4, ) - STARTING_PC = 0 # every program starts at PC = 0, and ends at PC = len(bytecode) - 1 POSEIDON_WIDTH, POSEIDON_FULL_ROUNDS, POSEIDON_PARTIAL_ROUNDS = 16, 8, 20 +POSEIDON_N_COLS = 9 + POSEIDON_WIDTH + POSEIDON_WIDTH * (POSEIDON_FULL_ROUNDS // 2) + POSEIDON_PARTIAL_ROUNDS +TABLE_BUSES = { + "execution": ( + ("col_mult", "Push"), + ("byte_lookup",), + ("mem_group", 2, 5, 1), # addr_a, value_a + ("mem_group", 3, 6, 1), # addr_b, value_b + ("mem_group", 4, 7, 1), # addr_c, value_c + ), + "extension": ( + ("col_mult", "Pull"), + ("mem_group", 6, 14, 5), # idx_a, va + ("mem_group", 7, 19, 5), # idx_b, vb + ("mem_group", 13, 24, 5), # idx_res, vres + ), + "poseidon": ( + ("col_mult", "Pull"), + ("mem_group", 6, 9, 4), + ("mem_group", 7, 13, 4), + ("mem_group", 1, 17, 8), + ("mem_group", 2, POSEIDON_N_COLS - 16, 16), + ), +} + + class ProofError(Exception): @@ -214,7 +238,7 @@ def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: return s + math.prod([*x, *y]) -def eval_multilinear_evals(evals: Sequence[EF], point: Sequence[EF]) -> EF: +def eval_multilinear_evals(evals: Sequence[Fp | EF], point: Sequence[EF]) -> EF: """Evaluate a multilinear in evaluation form at `point`.""" assert len(evals) == 1 << len(point) cur = list(evals) @@ -324,9 +348,10 @@ def verify_stir_challenges( for idx in indices: op = fiat_shamir.next_merkle_opening() merkle_verify_path(commitment.root, log_height, idx, op.leaf_data, op.path) + # Round 0 leaves are raw base-field elements; later rounds pack DIM Fp values per EF element. leaf = op.leaf_data if round_index == 0: - packed = [EF(f) for f in leaf] + packed = leaf else: packed = [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] fold = eval_multilinear_evals(packed, folding_randomness) @@ -335,25 +360,6 @@ def verify_stir_challenges( return constraints -def eval_constraints_poly(constraints: list[tuple[list[EF], list[SparseStatements]]], point: list[EF]) -> EF: - value = ZERO - pt = list(point) - for round_idx, (randomness, smts) in enumerate(constraints): - if round_idx > 0: - pt = pt[whir_folding_factor_at_round(round_idx - 1) :] - i = 0 - for smt in smts: - inner_pt = pt[len(pt) - len(smt.point) :] - common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly(smt.point, inner_pt) - sel_n = smt.selector_num_variables - for v in smt.values: - lagrange = math.prod(pt[j] if (v[0] >> (sel_n - 1 - j)) & 1 else ONE - pt[j] for j in range(sel_n)) - value = value + lagrange * common * randomness[i] - i += 1 - assert i == len(randomness) - return value - - def whir_verify( fiat_shamir: FiatShamir, cfg: dict, @@ -374,9 +380,9 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non g = ONE for smt in constraints: for _, value in smt.values: - target = target + g * value + target += g * value combo.append(g) - g = g * gamma + g *= gamma round_constraints.append((combo, constraints)) sc_point, target = verify_sumcheck(fiat_shamir, target, n_fold, 2, pow_bits) round_folding.append(sc_point) @@ -441,7 +447,21 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non round_folding.append(final_sc_point) folding_flat = [r for chunk in round_folding for r in chunk] - eval_weights = eval_constraints_poly(round_constraints, folding_flat) + + eval_weights = ZERO + pt = folding_flat + for round_idx, (randomness, smts) in enumerate(round_constraints): + if round_idx > 0: + pt = pt[whir_folding_factor_at_round(round_idx - 1) :] + i = 0 + for smt in smts: + inner_pt = pt[len(pt) - len(smt.point) :] + common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly(smt.point, inner_pt) + sel_n = smt.selector_num_variables + for v in smt.values: + lagrange = math.prod(pt[j] if (v[0] >> (sel_n - 1 - j)) & 1 else ONE - pt[j] for j in range(sel_n)) + eval_weights += lagrange * common * randomness[i] + i += 1 final_value = eval_multilinear_coeffs(final_coeffs, list(reversed(final_sc_point))) if final_sc_value != eval_weights * final_value: raise ProofError("WHIR final sumcheck check failed") @@ -449,30 +469,6 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non return folding_flat -# poseidon16_compress: 9 scalars + WIDTH inputs + (ROUNDS_F/2) pairs of WIDTH cols (begin + end-1 + outputs) + partial. -_POSEIDON_N_COLS = 9 + POSEIDON_WIDTH + POSEIDON_WIDTH * (POSEIDON_FULL_ROUNDS // 2) + POSEIDON_PARTIAL_ROUNDS -TABLE_BUSES = { - "execution": ( - ("col_mult", "Push"), - ("byte_lookup",), - ("mem_group", 2, 5, 1), # addr_a, value_a - ("mem_group", 3, 6, 1), # addr_b, value_b - ("mem_group", 4, 7, 1), # addr_c, value_c - ), - "extension": ( - ("col_mult", "Pull"), - ("mem_group", 6, 14, 5), # idx_a, va - ("mem_group", 7, 19, 5), # idx_b, vb - ("mem_group", 13, 24, 5), # idx_res, vres - ), - "poseidon": ( - ("col_mult", "Pull"), - ("mem_group", 6, 9, 4), - ("mem_group", 7, 13, 4), - ("mem_group", 1, 17, 8), - ("mem_group", 2, _POSEIDON_N_COLS - 16, 16), - ), -} @dataclass(frozen=True) @@ -480,7 +476,7 @@ class TableMeta: name: str n_columns: int buses: tuple - air_degree: int # max degree of AIR transition constraints + air_degree: int n_constraints: int n_shift: int # number of shift (next-row) columns air_fn: object # (folder, extra_data) -> None, fills folder with AIR constraints @@ -568,7 +564,7 @@ def verify_gkr_quotient(fiat_shamir: FiatShamir, n_vars: int) -> tuple[EF, list[ def mle_of_01234567_etc(point: Sequence[EF]) -> EF: """MLE of `f(i) = i` (big-endian) at `point`.""" n = len(point) - return sum(p * EF(1 << (n - 1 - i)) for i, p in enumerate(point)) + return sum(p * (1 << (n - 1 - i)) for i, p in enumerate(point)) def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: @@ -588,7 +584,7 @@ def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: def finger_print(discriminator: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: """`Σᵢ αᵢ · dataᵢ + α_last · discriminator`.""" assert len(alphas_eq_poly) > len(data) - return dot_product(alphas_eq_poly, data) + alphas_eq_poly[-1] * EF(discriminator) + return dot_product(alphas_eq_poly, data) + alphas_eq_poly[-1] * discriminator def sort_tables_by_height(table_log_heights: dict[str, int]) -> list[tuple[str, int]]: @@ -648,10 +644,10 @@ def pref_at(offset: int, log_height: int) -> EF: mem_pt = point_gkr[-log_memory:] pref = pref_at(0, log_memory) value_memory_acc = fiat_shamir.next_extension_scalar() - num = num - pref * value_memory_acc + num -= pref * value_memory_acc value_memory = fiat_shamir.next_extension_scalar() fp_mem = finger_print(ds_mem, [mle_of_01234567_etc(mem_pt), value_memory], alphas_eq_poly) - den = den + pref * (c - fp_mem) + den += pref * (c - fp_mem) offset = 1 << log_memory # Bytecode (padded to the tallest table). @@ -660,15 +656,15 @@ def pref_at(offset: int, log_height: int) -> EF: pref = pref_at(offset, log_bytecode) pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = fiat_shamir.next_extension_scalar() - bytecode_value = eval_multilinear_evals([EF(v) for v in bytecode_multilinear], byte_pt + alphas[-log_instr:]) + bytecode_value = eval_multilinear_evals([Fp(v) for v in bytecode_multilinear], byte_pt + alphas[-log_instr:]) correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) fp_byte = ( bytecode_value * correction + mle_of_01234567_etc(byte_pt) * alphas_eq_poly[n_instr_cols] - + alphas_eq_poly[-1] * EF(ds_byte) + + alphas_eq_poly[-1] * ds_byte ) - num = num - pref * value_bytecode_acc - den = den + pref * (c - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) + num -= pref * value_bytecode_acc + den += pref * (c - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) offset += 1 << log_byte_pad # Per-table base offsets in the GKR layout are assigned in sorted-by-height order @@ -699,16 +695,16 @@ def pref_at(offset: int, log_height: int) -> EF: if bus[0] == "col_mult": bus_num_vals[name] = fiat_shamir.next_extension_scalar() bus_den_vals[name] = fiat_shamir.next_extension_scalar() - num = num + pref * bus_num_vals[name] - den = den + pref * bus_den_vals[name] + num += pref * bus_num_vals[name] + den += pref * bus_den_vals[name] offset_within_table += row_stride elif bus[0] == "byte_lookup": cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [col_pc] evals = fiat_shamir.next_extension_scalars_vec(len(cols)) for c_idx, e in zip(cols, evals): table_values[c_idx] = e - num = num + pref # Push direction - den = den + pref * (c - finger_print(ds_byte, evals, alphas_eq_poly)) + num += pref # Push direction + den += pref * (c - finger_print(ds_byte, evals, alphas_eq_poly)) offset_within_table += row_stride elif bus[0] == "mem_group": _, idx_col, vals_start, n = bus @@ -724,16 +720,16 @@ def pref_at(offset: int, log_height: int) -> EF: if val_fresh: table_values[val_col] = next(evals) pref = pref_at(offset_within_table, log_n_rows) - fp = finger_print(ds_mem, [table_values[idx_col] + EF(i), table_values[val_col]], alphas_eq_poly) - num = num + pref # Push direction - den = den + pref * (c - fp) + fp = finger_print(ds_mem, [table_values[idx_col] + i, table_values[val_col]], alphas_eq_poly) + num += pref # Push direction + den += pref * (c - fp) offset_within_table += row_stride else: raise ProofError(f"unknown bus kind: {bus[0]}") columns_values[name] = table_values - den = den + mle_of_zeros_then_ones(final_offset, point_gkr) + den += mle_of_zeros_then_ones(final_offset, point_gkr) if num != claim_num: raise ProofError("logup: numerators value mismatch") if den != claim_den: @@ -812,8 +808,8 @@ def _eval_air_execution(folder: ConstraintFolder, extra_data: dict) -> None: nu_c = flag_c * operand_c + nfc * value_c + flag_c_fp * (fp + operand_c) # aux ∈ {0,1,2}: 0=nothing, 1=add, 2=deref. - add = aux * EF(2) - aux * aux - deref = aux * (aux - ONE) * EF((P + 1) // 2) # (P+1)/2 is the inverse of 2 mod P + add = aux * 2 - aux * aux + deref = aux * (aux - ONE) * ((P + 1) // 2) # (P+1)/2 is the inverse of 2 mod P is_precompile = ONE - add - mul - deref - jump az = folder.assert_zero @@ -860,11 +856,11 @@ def _eval_air_extension_op(folder: ConstraintFolder, extra_data: dict) -> None: comp_sh = s[8:13] aux = ( - is_be * EF(_EXT_OP_FLAG_IS_BE) - + flag_add * EF(_EXT_OP_FLAG_ADD) - + flag_mul * EF(_EXT_OP_FLAG_MUL) - + flag_poly_eq * EF(_EXT_OP_FLAG_POLY_EQ) - + len_col * EF(_EXT_OP_LEN_MULTIPLIER) + is_be * _EXT_OP_FLAG_IS_BE + + flag_add * _EXT_OP_FLAG_ADD + + flag_mul * _EXT_OP_FLAG_MUL + + flag_poly_eq * _EXT_OP_FLAG_POLY_EQ + + len_col * _EXT_OP_LEN_MULTIPLIER ) _eval_bus_virtual(folder, extra_data, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res]) @@ -899,8 +895,8 @@ def _eval_air_extension_op(folder: ConstraintFolder, extra_data: dict) -> None: ]: folder.assert_zero(not_start_sh * (x - y)) - folder.assert_zero(not_start_sh * (idx_a_sh - idx_a - (is_be + is_ee * EF(5)))) - folder.assert_zero(not_start_sh * (idx_b_sh - idx_b - EF(5))) + folder.assert_zero(not_start_sh * (idx_a_sh - idx_a - (is_be + is_ee * 5))) + folder.assert_zero(not_start_sh * (idx_b_sh - idx_b - 5)) folder.assert_zero(start_sh * (len_col - ONE)) @@ -965,11 +961,11 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: folder.assert_eq(state[0] * state[0] * state[0], cols["partial_rounds"][r]) state[0] = cols["partial_rounds"][r] if r < n_partial - 1: - state[0] = state[0] + const["sparse_scalar_rc"][r] + state[0] += const["sparse_scalar_rc"][r] old_s0 = state[0] state[0] = dot_product(state, const["sparse_first_row"][r]) for i in range(1, POSEIDON_WIDTH): - state[i] = state[i] + old_s0 * const["sparse_v"][r][i - 1] + state[i] += old_s0 * const["sparse_v"][r][i - 1] for r in range(half_final - 1): state = _full_round(state, const["final_constants"][2 * r], const["final_constants"][2 * r + 1]) @@ -1013,14 +1009,14 @@ def take(n: int) -> list[EF]: outputs_left, outputs_right = take(W // 2), take(W // 2) discriminator = ( - EF(POSEIDON_DISCRIMINATOR_BASE) - + flag_permute * EF(POSEIDON_PERMUTE_SHIFT) - + flag_half_output * EF(POSEIDON_HALF_OUTPUT_SHIFT) - + flag_hardcoded_left * EF(POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT) - + flag_hardcoded_left * offset_hardcoded_left * EF(POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT) + POSEIDON_DISCRIMINATOR_BASE + + flag_permute * POSEIDON_PERMUTE_SHIFT + + flag_half_output * POSEIDON_HALF_OUTPUT_SHIFT + + flag_hardcoded_left * POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT + + flag_hardcoded_left * offset_hardcoded_left * POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT ) not_hcl = ONE - flag_hardcoded_left - index_a = eff_idx_left_second - not_hcl * EF(DIGEST_ELEMS // 2) + index_a = eff_idx_left_second - not_hcl * (DIGEST_ELEMS // 2) _eval_bus_virtual(folder, extra_data, multiplicity, discriminator, [index_a, index_b, index_res]) for f in (multiplicity, flag_half_output, flag_hardcoded_left, flag_permute): @@ -1050,7 +1046,7 @@ def take(n: int) -> list[EF]: for name, n, d, k, s, fn in ( ("execution", N_INSTRUCTION_COLUMNS + N_RUNTIME_COLUMNS, 5, 14, 2, _eval_air_execution), ("extension", 29, 6, 35, 13, _eval_air_extension_op), - ("poseidon", _POSEIDON_N_COLS, 10, 101, 0, _eval_air_poseidon16), + ("poseidon", POSEIDON_N_COLS, 10, 101, 0, _eval_air_poseidon16), ) ] @@ -1144,8 +1140,8 @@ def verify_execution( for name in ALL_TABLES_ORDER: offset = alpha_offsets[name] sign = -ONE if tables_by_name[name].buses[0][1] == "Pull" else ONE - initial_sum = initial_sum + alpha_powers[offset] * (logup["bus_num"][name] * sign) - initial_sum = initial_sum + alpha_powers[offset + 1] * (logup_c - logup["bus_den"][name]) + initial_sum += alpha_powers[offset] * (logup["bus_num"][name] * sign) + initial_sum += alpha_powers[offset + 1] * (logup_c - logup["bus_den"][name]) sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in tables)) committed = { @@ -1162,7 +1158,7 @@ def verify_execution( natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = math.prod(sc_point[: n_max - log_n_rows]) - my_air_final = my_air_final + k_t * eq_poly(gkr_point[-log_n_rows:], natural_pt) * constraint_eval + my_air_final += k_t * eq_poly(gkr_point[-log_n_rows:], natural_pt) * constraint_eval eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} next_vals = {j: col_evals[meta.n_columns + j] for j in range(meta.n_shift)} @@ -1172,7 +1168,7 @@ def verify_execution( assert len(public_input) % DIGEST_ELEMS == 0 pm_point = state.sample_many_ef(log2_strict(len(public_input))) - pm_eval = eval_multilinear_evals([EF(f) for f in public_input], pm_point) + pm_eval = eval_multilinear_evals(public_input, pm_point) bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size previous = [ From 4a40a6d0a810d62e9926ab05ee8a7d0735cfbd5a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 02:31:22 +0400 Subject: [PATCH 52/69] wip --- crates/lean_prover/verifier.py | 363 +++++++++++++++++++-------------- 1 file changed, 209 insertions(+), 154 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 17d115428..dcf7dbc89 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -13,6 +13,7 @@ import math import sys from dataclasses import dataclass +from enum import IntEnum from pathlib import Path from typing import Sequence from primitives import * @@ -40,11 +41,9 @@ MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, MAX_BYTECODE_LOG_SIZE = 8, 8, 22 -MAX_LOG_N_ROWS_PER_TABLE = {"execution": 24, "extension": 21, "poseidon": 21} -ALL_TABLES_ORDER = ("execution", "extension", "poseidon") N_VARS_TO_SEND_GKR_COEFFS = 5 -N_RUNTIME_COLUMNS, N_INSTRUCTION_COLUMNS, PC_COL_INDEX = 8, 12, 0 +N_RUNTIME_COLUMNS, N_INSTRUCTION_COLUMNS = 8, 12 LOGUP_MEMORY_DOMAINSEP, LOGUP_BYTECODE_DOMAINSEP = 1, 2 POSEIDON_DISCRIMINATOR_BASE = 3 # odd ≥ 3 @@ -57,37 +56,70 @@ STARTING_PC = 0 # every program starts at PC = 0, and ends at PC = len(bytecode) - 1 POSEIDON_WIDTH, POSEIDON_FULL_ROUNDS, POSEIDON_PARTIAL_ROUNDS = 16, 8, 20 -POSEIDON_N_COLS = 9 + POSEIDON_WIDTH + POSEIDON_WIDTH * (POSEIDON_FULL_ROUNDS // 2) + POSEIDON_PARTIAL_ROUNDS -TABLE_BUSES = { - "execution": ( - ("col_mult", "Push"), - ("byte_lookup",), - ("mem_group", 2, 5, 1), # addr_a, value_a - ("mem_group", 3, 6, 1), # addr_b, value_b - ("mem_group", 4, 7, 1), # addr_c, value_c - ), - "extension": ( - ("col_mult", "Pull"), - ("mem_group", 6, 14, 5), # idx_a, va - ("mem_group", 7, 19, 5), # idx_b, vb - ("mem_group", 13, 24, 5), # idx_res, vres - ), - "poseidon": ( - ("col_mult", "Pull"), - ("mem_group", 6, 9, 4), - ("mem_group", 7, 13, 4), - ("mem_group", 1, 17, 8), - ("mem_group", 2, POSEIDON_N_COLS - 16, 16), - ), -} - - class ProofError(Exception): pass +class BusDirection(IntEnum): + PUSH = 1 + PULL = -1 + + +class BusInterractiion(IntEnum): + PRECOMPILE = 0 + BYTECODE = 1 + MEMORY = 2 + + +@dataclass(frozen=True) +class Table: + name: str + columns: tuple[str, ...] + buses: tuple + air_degree: int + n_constraints: int + n_shift: int # shift (next-row) columns are always the first ones + max_log_height: int + air_fn: object # (folder, logup_alphas_eq) -> None, fills folder with AIR constraints + + @property + def n_columns(self) -> int: + return len(self.columns) + + @property + def n_buses(self) -> int: + # MEMORY entries expand to `n` individual buses. + return sum(b[3] if b[0] == BusInterractiion.MEMORY else 1 for b in self.buses) + + @property + def mult_sign(self) -> EF: + # First bus is always the ROW_MULT bus; its direction (Push=+1, Pull=−1) IS the sign. + return EF(self.buses[0][1]) + + def col(self, ref) -> int: + """Resolve a column reference (name or already-int index) to its integer index.""" + return ref if isinstance(ref, int) else self.columns.index(ref) + + def eval_air(self, col_evals: Sequence[EF], alpha_powers: Sequence[EF], logup_alphas_eq: list[EF]) -> EF: + folder = ConstraintFolder(col_evals[: self.n_columns], col_evals[self.n_columns :], alpha_powers) + self.air_fn(folder, logup_alphas_eq) + return folder.accumulator + + def boundary_statements( + self, stacked_n_vars: int, offset: int, n_vars: int, ending_pc: int + ) -> list["SparseStatements"]: + """Static row-pinning constraints. Only the execution table pins the PC column.""" + if self.name != "execution": + return [] + pc_col = self.col("pc") + return [ + SparseStatements.unique_value(stacked_n_vars, offset + (pc_col << n_vars) + idx, EF(pc)) + for idx, pc in [(0, STARTING_PC), ((1 << n_vars) - 1, ending_pc)] + ] + + # T-Sponge (compression instead of permutation) with replacement (instead of xoring / adding the ingested data). def sponge_hash(data: Sequence[Fp]) -> list[Fp]: assert len(data) % SPONGE_RATE == 0 and len(data) > 0 @@ -469,44 +501,26 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non return folding_flat - - -@dataclass(frozen=True) -class TableMeta: - name: str - n_columns: int - buses: tuple - air_degree: int - n_constraints: int - n_shift: int # number of shift (next-row) columns - air_fn: object # (folder, extra_data) -> None, fills folder with AIR constraints - - @property - def n_buses(self) -> int: - # mem_group entries expand to `n` individual buses. - return sum(b[3] if b[0] == "mem_group" else 1 for b in self.buses) - - def stacked_pcs_global_statements( stacked_n_vars: int, memory_n_vars: int, bytecode_n_vars: int, previous_statements: list[SparseStatements], - table_log_heights: dict[str, int], + tables: Sequence[Table], + heights: dict[str, int], committed_statements: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]], - tables: dict[str, TableMeta], ending_pc: int, ) -> list[SparseStatements]: - assert len(table_log_heights) == len(committed_statements) - tables_sorted = sort_tables_by_height(table_log_heights) + assert len(tables) == len(committed_statements) + tables_sorted = sort_tables_by_height(tables, heights) # Layout offsets are assigned in sorted-by-height order (taller tables come first - # in the stacked polynomial), but statements are emitted in canonical ALL_TABLES order. + # in the stacked polynomial), but statements are emitted in canonical TABLES order. table_offsets: dict[str, int] = {} layout_offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, tables_sorted[0][1])) - for name, n_vars in tables_sorted: - table_offsets[name] = layout_offset - layout_offset += tables[name].n_columns << n_vars + for table, n_vars in tables_sorted: + table_offsets[table.name] = layout_offset + layout_offset += table.n_columns << n_vars out = list(previous_statements) @@ -514,17 +528,12 @@ def stacked_pcs_global_statements( def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: return [(col_base + i, v) for i, v in sorted(d.items())] - for name in ALL_TABLES_ORDER: - n_vars = table_log_heights[name] - offset = table_offsets[name] + for table in tables: + n_vars = heights[table.name] + offset = table_offsets[table.name] col_base = offset >> n_vars - if name == "execution": - # PC column: pin first row to STARTING_PC, last row to ending_pc. - for idx, pc in [(0, STARTING_PC), ((1 << n_vars) - 1, ending_pc)]: - out.append( - SparseStatements.unique_value(stacked_n_vars, offset + (PC_COL_INDEX << n_vars) + idx, EF(pc)) - ) - for point, eq_values, next_values in committed_statements[name]: + out.extend(table.boundary_statements(stacked_n_vars, offset, n_vars, ending_pc)) + for point, eq_values, next_values in committed_statements[table.name]: if next_values: out.append(SparseStatements.new_next(stacked_n_vars, list(point), values_at(next_values, col_base))) out.append(SparseStatements(stacked_n_vars, list(point), values_at(eq_values, col_base))) @@ -587,9 +596,9 @@ def finger_print(discriminator: Fp, data: Sequence[EF], alphas_eq_poly: Sequence return dot_product(alphas_eq_poly, data) + alphas_eq_poly[-1] * discriminator -def sort_tables_by_height(table_log_heights: dict[str, int]) -> list[tuple[str, int]]: +def sort_tables_by_height(tables: Sequence[Table], heights: dict[str, int]) -> list[tuple[Table, int]]: """Descending by height, alphabetical on ties (matches Rust `BTreeMap`).""" - return sorted(sorted(table_log_heights.items()), key=lambda kv: -kv[1]) + return sorted([(t, heights[t.name]) for t in tables], key=lambda x: (-x[1], x[0].name)) def eval_eq(point: Sequence[EF]) -> list[EF]: @@ -607,24 +616,23 @@ def verify_generic_logup( alphas_eq_poly: list[EF], log_memory: int, bytecode_multilinear: list[int], - table_log_heights: dict[str, int], - tables: dict[str, TableMeta], + tables: Sequence[Table], + heights: dict[str, int], ) -> dict: """GKR-quotient + section-by-section (memory/bytecode/per-table) reconstruction.""" n_instr_cols = N_INSTRUCTION_COLUMNS n_runtime_cols = N_RUNTIME_COLUMNS - col_pc = PC_COL_INDEX ds_mem = Fp(LOGUP_MEMORY_DOMAINSEP) ds_byte = Fp(LOGUP_BYTECODE_DOMAINSEP) - tables_sorted = sort_tables_by_height(table_log_heights) + tables_sorted = sort_tables_by_height(tables, heights) log_bytecode = log2_strict(len(bytecode_multilinear) // (1 << log2_ceil(n_instr_cols))) log_instr = log2_ceil(n_instr_cols) total_active_len = ( (1 << log_memory) + max(1 << log_bytecode, 1 << tables_sorted[0][1]) - + sum(tables[n].n_buses << h for n, h in tables_sorted) + + sum(t.n_buses << h for t, h in tables_sorted) ) total_gkr_n_vars = log2_ceil(total_active_len) @@ -670,44 +678,45 @@ def pref_at(offset: int, log_height: int) -> EF: # Per-table base offsets in the GKR layout are assigned in sorted-by-height order # (mirrors `layout_offsets` in sub_protocols/src/logup.rs). table_offsets: dict[str, int] = {} - for name, log_n_rows in tables_sorted: - table_offsets[name] = offset - offset += tables[name].n_buses << log_n_rows + for table, log_n_rows in tables_sorted: + table_offsets[table.name] = offset + offset += table.n_buses << log_n_rows final_offset = offset # Per-table: walk the bus spec in the same order as the Rust prover. The prover # writes col_evals for new (uncached) columns in `bus.data` order via a single # `add_extension_scalars` chunk per bus — the verifier must read in the same chunks. - # Iterate tables in canonical ALL_TABLES order (matches the new prover scalar layout). + # Iterate tables in canonical TABLES order (matches the new prover scalar layout). bus_num_vals: dict[str, EF] = {} bus_den_vals: dict[str, EF] = {} columns_values: dict[str, dict[int, EF]] = {} - for name in ALL_TABLES_ORDER: - log_n_rows = table_log_heights[name] - meta = tables[name] + for table in tables: + name = table.name + log_n_rows = heights[name] table_values: dict[int, EF] = {} row_stride = 1 << log_n_rows offset_within_table = table_offsets[name] - for bus in meta.buses: + for bus in table.buses: pref = pref_at(offset_within_table, log_n_rows) - if bus[0] == "col_mult": + if bus[0] == BusInterractiion.PRECOMPILE: bus_num_vals[name] = fiat_shamir.next_extension_scalar() bus_den_vals[name] = fiat_shamir.next_extension_scalar() num += pref * bus_num_vals[name] den += pref * bus_den_vals[name] offset_within_table += row_stride - elif bus[0] == "byte_lookup": - cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [col_pc] + elif bus[0] == BusInterractiion.BYTECODE: + cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [table.col("pc")] evals = fiat_shamir.next_extension_scalars_vec(len(cols)) for c_idx, e in zip(cols, evals): table_values[c_idx] = e num += pref # Push direction den += pref * (c - finger_print(ds_byte, evals, alphas_eq_poly)) offset_within_table += row_stride - elif bus[0] == "mem_group": - _, idx_col, vals_start, n = bus + elif bus[0] == BusInterractiion.MEMORY: + _, idx_ref, vals_ref, n = bus + idx_col, vals_start = table.col(idx_ref), table.col(vals_ref) # One bus per row in the group; first sees idx_col fresh, the rest # see only val_col fresh (mirrors the Rust prover's dedup logic). for i in range(n): @@ -770,28 +779,18 @@ def assert_bool(self, x: EF) -> None: self.assert_zero(x * (ONE - x)) -def _eval_bus_virtual( - folder: "ConstraintFolder", extra_data: dict, multiplicity: EF, discriminator: EF, data: Sequence[EF] +def eval_precompile_bus_virtual_columns( + folder: "ConstraintFolder", + logup_alphas_eq: list[EF], + multiplicity: EF, + discriminator: EF, + data: Sequence[EF], ) -> None: - alphas: list[EF] = extra_data["logup_alphas_eq"] - assert len(data) < len(alphas) folder.assert_zero(multiplicity) - encoded = dot_product(alphas, data) + alphas[-1] * discriminator - folder.assert_zero(encoded) + folder.assert_zero(dot_product(logup_alphas_eq, data) + logup_alphas_eq[-1] * discriminator) -def air_constraint_eval( - table: TableMeta, - col_evals: Sequence[EF], - alpha_powers: Sequence[EF], - extra_data: dict, -) -> EF: - folder = ConstraintFolder(col_evals[: table.n_columns], col_evals[table.n_columns :], alpha_powers) - table.air_fn(folder, extra_data) - return folder.accumulator - - -def _eval_air_execution(folder: ConstraintFolder, extra_data: dict) -> None: +def _eval_air_execution(folder: ConstraintFolder, logup_alphas_eq: list[EF]) -> None: # fmt: off (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, @@ -813,7 +812,7 @@ def _eval_air_execution(folder: ConstraintFolder, extra_data: dict) -> None: is_precompile = ONE - add - mul - deref - jump az = folder.assert_zero - _eval_bus_virtual(folder, extra_data, is_precompile, discriminator, [nu_a, nu_b, nu_c]) + eval_precompile_bus_virtual_columns(folder, logup_alphas_eq, is_precompile, discriminator, [nu_a, nu_b, nu_c]) az(nfa * (addr_a - (fp + operand_a))) az(nfb * (addr_b - (fp + operand_b))) az(nfc * (addr_c - (fp + operand_c))) @@ -844,7 +843,7 @@ def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: return quintic_mul(a, b, ZERO) -def _eval_air_extension_op(folder: ConstraintFolder, extra_data: dict) -> None: +def _eval_air_extension_op(folder: ConstraintFolder, logup_alphas_eq: list[EF]) -> None: # Layout: shift columns 0..13 = (is_be, start, len, flag_{add,mul,poly_eq}, # idx_{a,b}, comp[0..5]); then idx_res, va, vb, vres (5 each). f = folder.flat @@ -862,7 +861,9 @@ def _eval_air_extension_op(folder: ConstraintFolder, extra_data: dict) -> None: + flag_poly_eq * _EXT_OP_FLAG_POLY_EQ + len_col * _EXT_OP_LEN_MULTIPLIER ) - _eval_bus_virtual(folder, extra_data, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res]) + eval_precompile_bus_virtual_columns( + folder, logup_alphas_eq, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res] + ) for x in (is_be, start, flag_add, flag_mul, flag_poly_eq): folder.assert_bool(x) @@ -987,7 +988,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: folder.assert_zero(flag_permute * (state[i + POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) -def _eval_air_poseidon16(folder: ConstraintFolder, extra_data: dict) -> None: +def _eval_air_poseidon16(folder: ConstraintFolder, logup_alphas_eq: list[EF]) -> None: flat, W = folder.flat, POSEIDON_WIDTH half_initial = half_final = POSEIDON_FULL_ROUNDS // 4 @@ -1018,7 +1019,7 @@ def take(n: int) -> list[EF]: not_hcl = ONE - flag_hardcoded_left index_a = eff_idx_left_second - not_hcl * (DIGEST_ELEMS // 2) - _eval_bus_virtual(folder, extra_data, multiplicity, discriminator, [index_a, index_b, index_res]) + eval_precompile_bus_virtual_columns(folder, logup_alphas_eq, multiplicity, discriminator, [index_a, index_b, index_res]) for f in (multiplicity, flag_half_output, flag_hardcoded_left, flag_permute): folder.assert_bool(f) folder.assert_zero(flag_permute * (flag_half_output + flag_hardcoded_left)) @@ -1040,14 +1041,79 @@ def take(n: int) -> list[EF]: ) -# (n_columns, air_degree, n_constraints, n_shift, air_fn) per table — all constants. -DEFAULT_TABLES = [ - TableMeta(name, n, TABLE_BUSES[name], d, k, s, fn) - for name, n, d, k, s, fn in ( - ("execution", N_INSTRUCTION_COLUMNS + N_RUNTIME_COLUMNS, 5, 14, 2, _eval_air_execution), - ("extension", 29, 6, 35, 13, _eval_air_extension_op), - ("poseidon", POSEIDON_N_COLS, 10, 101, 0, _eval_air_poseidon16), - ) +EXECUTION_COLUMNS = ( + "pc", "fp", "addr_a", "addr_b", "addr_c", "value_a", "value_b", "value_c", # 8 runtime cols + "operand_a", "operand_b", "operand_c", "flag_a", "flag_b", "flag_c", "flag_c_fp", "flag_ab_fp", "mul", "jump", "aux", "discriminator", # 12 instruction cols. +) # fmt: skip + +EXTENSION_COLUMNS = ( + "is_be", "start", "len", "flag_add", "flag_mul", "flag_poly_eq", "idx_a", "idx_b", + *(f"comp_{i}" for i in range(5)), + "idx_res", + *(f"va_{i}" for i in range(5)), + *(f"vb_{i}" for i in range(5)), + *(f"vres_{i}" for i in range(5)), +) # fmt: skip + +POSEIDON_COLUMNS = ( + "multiplicity", "index_b", "index_res", "flag_half_output", "flag_hardcoded_left", "offset_hardcoded_left", "eff_idx_left_first", "eff_idx_left_second", "flag_permute", + *(f"input_{i}" for i in range(POSEIDON_WIDTH)), + *(f"begin_r{r}_{i}" for r in range(POSEIDON_FULL_ROUNDS // 4) for i in range(POSEIDON_WIDTH)), + *(f"partial_{i}" for i in range(POSEIDON_PARTIAL_ROUNDS)), + *(f"end_r{r}_{i}" for r in range(POSEIDON_FULL_ROUNDS // 4 - 1) for i in range(POSEIDON_WIDTH)), + *(f"out_left_{i}" for i in range(POSEIDON_WIDTH // 2)), + *(f"out_right_{i}" for i in range(POSEIDON_WIDTH // 2)), +) # fmt: skip + +# Canonical iteration order. Holds all per-table data: layout, buses, AIR config, and the AIR eval fn. +TABLES = [ + Table( + name="execution", + columns=EXECUTION_COLUMNS, + buses=( + (BusInterractiion.PRECOMPILE, BusDirection.PUSH), + (BusInterractiion.BYTECODE,), + (BusInterractiion.MEMORY, "addr_a", "value_a", 1), + (BusInterractiion.MEMORY, "addr_b", "value_b", 1), + (BusInterractiion.MEMORY, "addr_c", "value_c", 1), + ), + air_degree=5, + n_constraints=14, + n_shift=2, + max_log_height=24, + air_fn=_eval_air_execution, + ), + Table( + name="extension", + columns=EXTENSION_COLUMNS, + buses=( + (BusInterractiion.PRECOMPILE, BusDirection.PULL), + (BusInterractiion.MEMORY, "idx_a", "va_0", 5), + (BusInterractiion.MEMORY, "idx_b", "vb_0", 5), + (BusInterractiion.MEMORY, "idx_res", "vres_0", 5), + ), + air_degree=6, + n_constraints=35, + n_shift=13, + max_log_height=21, + air_fn=_eval_air_extension_op, + ), + Table( + name="poseidon", + columns=POSEIDON_COLUMNS, + buses=( + (BusInterractiion.PRECOMPILE, BusDirection.PULL), + (BusInterractiion.MEMORY, "eff_idx_left_first", "input_0", 4), + (BusInterractiion.MEMORY, "eff_idx_left_second", "input_4", 4), + (BusInterractiion.MEMORY, "index_b", "input_8", 8), + (BusInterractiion.MEMORY, "index_res", "out_left_0", 16), + ), + air_degree=10, + n_constraints=101, + n_shift=0, + max_log_height=21, + air_fn=_eval_air_poseidon16, + ), ] @@ -1056,7 +1122,7 @@ def verify_execution( proof: Proof, bytecode_multilinear: list[int], ): - tables = DEFAULT_TABLES + tables = TABLES bytecode_log_size = log2_strict(len(bytecode_multilinear)) - log2_ceil(N_INSTRUCTION_COLUMNS) ending_pc = (1 << bytecode_log_size) - 1 bytecode_hash = sponge_hash([Fp(v) for v in bytecode_multilinear]) @@ -1077,21 +1143,16 @@ def verify_execution( if log_memory < max(max(table_log_n_rows, default=0), bytecode_log_size): raise ProofError("InvalidProof: memory smaller than tables/bytecode") for t, h in zip(tables, table_log_n_rows): - limit = MAX_LOG_N_ROWS_PER_TABLE[t.name] - if not MIN_LOG_N_ROWS_PER_TABLE <= h <= limit: + if not MIN_LOG_N_ROWS_PER_TABLE <= h <= t.max_log_height: raise ProofError( - f"InvalidProof: table {t.name} log_n_rows={h} not in [{MIN_LOG_N_ROWS_PER_TABLE}, {limit}]" + f"InvalidProof: table {t.name} log_n_rows={h} not in [{MIN_LOG_N_ROWS_PER_TABLE}, {t.max_log_height}]" ) - table_log_heights = {t.name: h for t, h in zip(tables, table_log_n_rows)} - tables_by_name = {t.name: t for t in tables} - tables_sorted = sort_tables_by_height(table_log_heights) - n_max = tables_sorted[0][1] + heights = {t.name: h for t, h in zip(tables, table_log_n_rows)} + n_max = sort_tables_by_height(tables, heights)[0][1] total_stacked = ( - (2 << log_memory) - + (1 << max(bytecode_log_size, n_max)) - + sum(t.n_columns << table_log_heights[t.name] for t in tables) + (2 << log_memory) + (1 << max(bytecode_log_size, n_max)) + sum(t.n_columns << heights[t.name] for t in tables) ) stacked_n_vars = log2_ceil(total_stacked) if stacked_n_vars > TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: @@ -1116,53 +1177,47 @@ def verify_execution( logup_alphas_eq, log_memory, bytecode_multilinear, - table_log_heights, - tables_by_name, + tables, + heights, ) gkr_point = logup["gkr_point"] air_alpha = state.sample_ef() - # AIR alpha powers/offsets are laid out in canonical ALL_TABLES order + # AIR alpha powers/offsets are laid out in canonical TABLES order # (mirrors `for table in ALL_TABLES { alpha_offset += n_constraints }` in verify_execution.rs). alpha_offsets: dict[str, int] = {} cumulative = 0 - for name in ALL_TABLES_ORDER: - alpha_offsets[name] = cumulative - cumulative += tables_by_name[name].n_constraints + for t in tables: + alpha_offsets[t.name] = cumulative + cumulative += t.n_constraints alpha_powers = ef_powers(air_alpha, cumulative) - extra_data = {"logup_alphas_eq": logup_alphas_eq} - # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (c − bus_den)). The # sign is the direction of each table's unique Column-multiplicity bus. initial_sum = ZERO - for name in ALL_TABLES_ORDER: - offset = alpha_offsets[name] - sign = -ONE if tables_by_name[name].buses[0][1] == "Pull" else ONE - initial_sum += alpha_powers[offset] * (logup["bus_num"][name] * sign) - initial_sum += alpha_powers[offset + 1] * (logup_c - logup["bus_den"][name]) + for t in tables: + offset = alpha_offsets[t.name] + initial_sum += alpha_powers[offset] * (logup["bus_num"][t.name] * t.mult_sign) + initial_sum += alpha_powers[offset + 1] * (logup_c - logup["bus_den"][t.name]) sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in tables)) - committed = { - name: [(gkr_point[-table_log_heights[name] :], logup["columns_values"][name], {})] for name in ALL_TABLES_ORDER - } + committed = {t.name: [(gkr_point[-heights[t.name] :], logup["columns_values"][t.name], {})] for t in tables} my_air_final = ZERO - for name in ALL_TABLES_ORDER: - meta = tables_by_name[name] - log_n_rows = table_log_heights[name] - col_evals = state.next_extension_scalars_vec(meta.n_columns + meta.n_shift) - offset = alpha_offsets[name] - alpha_slice = alpha_powers[offset : offset + meta.n_constraints] - constraint_eval = air_constraint_eval(meta, col_evals, alpha_slice, extra_data) + for t in tables: + log_n_rows = heights[t.name] + col_evals = state.next_extension_scalars_vec(t.n_columns + t.n_shift) + offset = alpha_offsets[t.name] + alpha_slice = alpha_powers[offset : offset + t.n_constraints] + constraint_eval = t.eval_air(col_evals, alpha_slice, logup_alphas_eq) natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = math.prod(sc_point[: n_max - log_n_rows]) my_air_final += k_t * eq_poly(gkr_point[-log_n_rows:], natural_pt) * constraint_eval - eq_vals = {i: col_evals[i] for i in range(meta.n_columns)} - next_vals = {j: col_evals[meta.n_columns + j] for j in range(meta.n_shift)} - committed[name].append((natural_pt, eq_vals, next_vals)) + eq_vals = {i: col_evals[i] for i in range(t.n_columns)} + next_vals = {j: col_evals[t.n_columns + j] for j in range(t.n_shift)} + committed[t.name].append((natural_pt, eq_vals, next_vals)) if my_air_final != sc_value: raise ProofError("AIR sumcheck: claimed value mismatch") @@ -1187,9 +1242,9 @@ def verify_execution( log_memory, bytecode_log_size, previous, - table_log_heights, + tables, + heights, committed, - tables_by_name, ending_pc, ) whir_verify(state, cfg, parsed_commitment, global_statements) From 184b061ce6503a83a91c5d8d1bd5934d3ff3fab4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 02:35:35 +0400 Subject: [PATCH 53/69] w --- crates/lean_prover/verifier.py | 80 +++++++++++++++++----------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index dcf7dbc89..fca341242 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -82,7 +82,7 @@ class Table: n_constraints: int n_shift: int # shift (next-row) columns are always the first ones max_log_height: int - air_fn: object # (folder, logup_alphas_eq) -> None, fills folder with AIR constraints + air_fn: object # (folder, logup_beta_eq) -> None, fills folder with AIR constraints @property def n_columns(self) -> int: @@ -102,9 +102,9 @@ def col(self, ref) -> int: """Resolve a column reference (name or already-int index) to its integer index.""" return ref if isinstance(ref, int) else self.columns.index(ref) - def eval_air(self, col_evals: Sequence[EF], alpha_powers: Sequence[EF], logup_alphas_eq: list[EF]) -> EF: + def eval_air(self, col_evals: Sequence[EF], alpha_powers: Sequence[EF], logup_beta_eq: list[EF]) -> EF: folder = ConstraintFolder(col_evals[: self.n_columns], col_evals[self.n_columns :], alpha_powers) - self.air_fn(folder, logup_alphas_eq) + self.air_fn(folder, logup_beta_eq) return folder.accumulator def boundary_statements( @@ -590,10 +590,10 @@ def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: return point[0] * mle_of_zeros_then_ones(n_zeros - half, tail) -def finger_print(discriminator: Fp, data: Sequence[EF], alphas_eq_poly: Sequence[EF]) -> EF: +def finger_print(discriminator: Fp, data: Sequence[EF], beta_eq: Sequence[EF]) -> EF: """`Σᵢ αᵢ · dataᵢ + α_last · discriminator`.""" - assert len(alphas_eq_poly) > len(data) - return dot_product(alphas_eq_poly, data) + alphas_eq_poly[-1] * discriminator + assert len(beta_eq) > len(data) + return dot_product(beta_eq, data) + beta_eq[-1] * discriminator def sort_tables_by_height(tables: Sequence[Table], heights: dict[str, int]) -> list[tuple[Table, int]]: @@ -611,9 +611,9 @@ def eval_eq(point: Sequence[EF]) -> list[EF]: def verify_generic_logup( fiat_shamir: FiatShamir, - c: EF, - alphas: list[EF], - alphas_eq_poly: list[EF], + gamma: EF, # `γ` from minimal_zkVM.tex (logup quotient denominator challenge). + beta: list[EF], # `β` from minimal_zkVM.tex (bus-tuple hashing seeds). + beta_eq: list[EF], # eq(β, ·) evaluation table. log_memory: int, bytecode_multilinear: list[int], tables: Sequence[Table], @@ -654,8 +654,8 @@ def pref_at(offset: int, log_height: int) -> EF: value_memory_acc = fiat_shamir.next_extension_scalar() num -= pref * value_memory_acc value_memory = fiat_shamir.next_extension_scalar() - fp_mem = finger_print(ds_mem, [mle_of_01234567_etc(mem_pt), value_memory], alphas_eq_poly) - den += pref * (c - fp_mem) + fp_mem = finger_print(ds_mem, [mle_of_01234567_etc(mem_pt), value_memory], beta_eq) + den += pref * (gamma - fp_mem) offset = 1 << log_memory # Bytecode (padded to the tallest table). @@ -664,15 +664,11 @@ def pref_at(offset: int, log_height: int) -> EF: pref = pref_at(offset, log_bytecode) pref_pad = pref_at(offset, log_byte_pad) value_bytecode_acc = fiat_shamir.next_extension_scalar() - bytecode_value = eval_multilinear_evals([Fp(v) for v in bytecode_multilinear], byte_pt + alphas[-log_instr:]) - correction = math.prod(ONE - a for a in alphas[: len(alphas) - log_instr]) - fp_byte = ( - bytecode_value * correction - + mle_of_01234567_etc(byte_pt) * alphas_eq_poly[n_instr_cols] - + alphas_eq_poly[-1] * ds_byte - ) + bytecode_value = eval_multilinear_evals([Fp(v) for v in bytecode_multilinear], byte_pt + beta[-log_instr:]) + correction = math.prod(ONE - a for a in beta[: len(beta) - log_instr]) + fp_byte = bytecode_value * correction + mle_of_01234567_etc(byte_pt) * beta_eq[n_instr_cols] + beta_eq[-1] * ds_byte num -= pref * value_bytecode_acc - den += pref * (c - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) + den += pref * (gamma - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) offset += 1 << log_byte_pad # Per-table base offsets in the GKR layout are assigned in sorted-by-height order @@ -712,7 +708,7 @@ def pref_at(offset: int, log_height: int) -> EF: for c_idx, e in zip(cols, evals): table_values[c_idx] = e num += pref # Push direction - den += pref * (c - finger_print(ds_byte, evals, alphas_eq_poly)) + den += pref * (gamma - finger_print(ds_byte, evals, beta_eq)) offset_within_table += row_stride elif bus[0] == BusInterractiion.MEMORY: _, idx_ref, vals_ref, n = bus @@ -729,9 +725,9 @@ def pref_at(offset: int, log_height: int) -> EF: if val_fresh: table_values[val_col] = next(evals) pref = pref_at(offset_within_table, log_n_rows) - fp = finger_print(ds_mem, [table_values[idx_col] + i, table_values[val_col]], alphas_eq_poly) + fp = finger_print(ds_mem, [table_values[idx_col] + i, table_values[val_col]], beta_eq) num += pref # Push direction - den += pref * (c - fp) + den += pref * (gamma - fp) offset_within_table += row_stride else: raise ProofError(f"unknown bus kind: {bus[0]}") @@ -781,16 +777,16 @@ def assert_bool(self, x: EF) -> None: def eval_precompile_bus_virtual_columns( folder: "ConstraintFolder", - logup_alphas_eq: list[EF], + logup_beta_eq: list[EF], multiplicity: EF, discriminator: EF, data: Sequence[EF], ) -> None: folder.assert_zero(multiplicity) - folder.assert_zero(dot_product(logup_alphas_eq, data) + logup_alphas_eq[-1] * discriminator) + folder.assert_zero(dot_product(logup_beta_eq, data) + logup_beta_eq[-1] * discriminator) -def _eval_air_execution(folder: ConstraintFolder, logup_alphas_eq: list[EF]) -> None: +def _eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: # fmt: off (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, @@ -812,7 +808,7 @@ def _eval_air_execution(folder: ConstraintFolder, logup_alphas_eq: list[EF]) -> is_precompile = ONE - add - mul - deref - jump az = folder.assert_zero - eval_precompile_bus_virtual_columns(folder, logup_alphas_eq, is_precompile, discriminator, [nu_a, nu_b, nu_c]) + eval_precompile_bus_virtual_columns(folder, logup_beta_eq, is_precompile, discriminator, [nu_a, nu_b, nu_c]) az(nfa * (addr_a - (fp + operand_a))) az(nfb * (addr_b - (fp + operand_b))) az(nfc * (addr_c - (fp + operand_c))) @@ -843,7 +839,7 @@ def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: return quintic_mul(a, b, ZERO) -def _eval_air_extension_op(folder: ConstraintFolder, logup_alphas_eq: list[EF]) -> None: +def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: # Layout: shift columns 0..13 = (is_be, start, len, flag_{add,mul,poly_eq}, # idx_{a,b}, comp[0..5]); then idx_res, va, vb, vres (5 each). f = folder.flat @@ -862,7 +858,7 @@ def _eval_air_extension_op(folder: ConstraintFolder, logup_alphas_eq: list[EF]) + len_col * _EXT_OP_LEN_MULTIPLIER ) eval_precompile_bus_virtual_columns( - folder, logup_alphas_eq, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res] + folder, logup_beta_eq, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res] ) for x in (is_be, start, flag_add, flag_mul, flag_poly_eq): @@ -988,7 +984,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: folder.assert_zero(flag_permute * (state[i + POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) -def _eval_air_poseidon16(folder: ConstraintFolder, logup_alphas_eq: list[EF]) -> None: +def _eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: flat, W = folder.flat, POSEIDON_WIDTH half_initial = half_final = POSEIDON_FULL_ROUNDS // 4 @@ -1019,7 +1015,9 @@ def take(n: int) -> list[EF]: not_hcl = ONE - flag_hardcoded_left index_a = eff_idx_left_second - not_hcl * (DIGEST_ELEMS // 2) - eval_precompile_bus_virtual_columns(folder, logup_alphas_eq, multiplicity, discriminator, [index_a, index_b, index_res]) + eval_precompile_bus_virtual_columns( + folder, logup_beta_eq, multiplicity, discriminator, [index_a, index_b, index_res] + ) for f in (multiplicity, flag_half_output, flag_hardcoded_left, flag_permute): folder.assert_bool(f) folder.assert_zero(flag_permute * (flag_half_output + flag_hardcoded_left)) @@ -1096,7 +1094,7 @@ def take(n: int) -> list[EF]: n_constraints=35, n_shift=13, max_log_height=21, - air_fn=_eval_air_extension_op, + air_fn=eval_air_extension_op, ), Table( name="poseidon", @@ -1166,15 +1164,17 @@ def verify_execution( state.next_extension_scalars_vec(nood), ) - logup_c = state.sample_ef() + # logup challenges, named after minimal_zkVM.tex: γ is the quotient denominator, + # β are the bus-tuple hashing seeds (length ℓ = log2(bus_width)). + logup_gamma = state.sample_ef() state.duplex() - logup_alphas = state.sample_many_ef(log2_ceil(N_INSTRUCTION_COLUMNS + 2)) - logup_alphas_eq = eval_eq(logup_alphas) + logup_beta = state.sample_many_ef(log2_ceil(N_INSTRUCTION_COLUMNS + 2)) + logup_beta_eq = eval_eq(logup_beta) logup = verify_generic_logup( state, - logup_c, - logup_alphas, - logup_alphas_eq, + logup_gamma, + logup_beta, + logup_beta_eq, log_memory, bytecode_multilinear, tables, @@ -1193,13 +1193,13 @@ def verify_execution( cumulative += t.n_constraints alpha_powers = ef_powers(air_alpha, cumulative) - # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (c − bus_den)). The + # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (γ − bus_den)). The # sign is the direction of each table's unique Column-multiplicity bus. initial_sum = ZERO for t in tables: offset = alpha_offsets[t.name] initial_sum += alpha_powers[offset] * (logup["bus_num"][t.name] * t.mult_sign) - initial_sum += alpha_powers[offset + 1] * (logup_c - logup["bus_den"][t.name]) + initial_sum += alpha_powers[offset + 1] * (logup_gamma - logup["bus_den"][t.name]) sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in tables)) committed = {t.name: [(gkr_point[-heights[t.name] :], logup["columns_values"][t.name], {})] for t in tables} @@ -1209,7 +1209,7 @@ def verify_execution( col_evals = state.next_extension_scalars_vec(t.n_columns + t.n_shift) offset = alpha_offsets[t.name] alpha_slice = alpha_powers[offset : offset + t.n_constraints] - constraint_eval = t.eval_air(col_evals, alpha_slice, logup_alphas_eq) + constraint_eval = t.eval_air(col_evals, alpha_slice, logup_beta_eq) natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = math.prod(sc_point[: n_max - log_n_rows]) From fe73102fd50d655ee6692e6690f95a8a5db8affb Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 03:07:28 +0400 Subject: [PATCH 54/69] w --- crates/lean_prover/verifier.py | 224 ++++++++++++++------------------- 1 file changed, 96 insertions(+), 128 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index fca341242..9ba9a4d74 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -52,6 +52,7 @@ 1 << 3, 1 << 4, ) +EXT_OP_FLAG_IS_BE, EXT_OP_FLAG_ADD, EXT_OP_FLAG_MUL, EXT_OP_FLAG_POLY_EQ, EXT_OP_LEN_MULTIPLIER = 4, 8, 16, 32, 64 STARTING_PC = 0 # every program starts at PC = 0, and ends at PC = len(bytecode) - 1 @@ -297,6 +298,33 @@ def eval_univariate_polynomial(coeffs: list[EF], x: EF) -> EF: return acc +def mle_of_01234567_etc(point: Sequence[EF]) -> EF: + """evaluate the MLE of `f(i) = i` (big-endian) at `point`.""" + n = len(point) + return sum(p * (1 << (n - 1 - i)) for i, p in enumerate(point)) + + +def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: + """evaluate the MLE of `[0]*n_zeros ++ [1]*(2^len(point) - n_zeros)` at `point`.""" + n_values = 1 << len(point) + assert n_zeros <= n_values + if n_zeros == 0: + return ONE + if n_zeros == n_values: + return ZERO + half, tail = n_values >> 1, point[1:] + if n_zeros < half: + return (ONE - point[0]) * mle_of_zeros_then_ones(n_zeros, tail) + point[0] + return point[0] * mle_of_zeros_then_ones(n_zeros - half, tail) + + +def eval_eq(point: Sequence[EF]) -> list[EF]: + out = [ONE] + for p in point: + out = [w for v in out for w in (v * (ONE - p), v * p)] + return out + + @dataclass class SparseStatements: total_num_variables: int @@ -511,11 +539,7 @@ def stacked_pcs_global_statements( committed_statements: dict[str, list[tuple[list[EF], dict[int, EF], dict[int, EF]]]], ending_pc: int, ) -> list[SparseStatements]: - assert len(tables) == len(committed_statements) tables_sorted = sort_tables_by_height(tables, heights) - - # Layout offsets are assigned in sorted-by-height order (taller tables come first - # in the stacked polynomial), but statements are emitted in canonical TABLES order. table_offsets: dict[str, int] = {} layout_offset = (2 << memory_n_vars) + (1 << max(bytecode_n_vars, tables_sorted[0][1])) for table, n_vars in tables_sorted: @@ -524,7 +548,6 @@ def stacked_pcs_global_statements( out = list(previous_statements) - # Rust uses BTreeMap (sorted by key); Python dicts are insertion-ordered, sort here. def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: return [(col_base + i, v) for i, v in sorted(d.items())] @@ -542,7 +565,6 @@ def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: def verify_gkr_quotient(fiat_shamir: FiatShamir, n_vars: int) -> tuple[EF, list[EF], EF, EF]: - """Layered sumcheck for `Σ nᵢ/dᵢ`. Returns `(quotient, point, claim_num, claim_den)`.""" assert n_vars > N_VARS_TO_SEND_GKR_COEFFS nums = fiat_shamir.next_extension_scalars_vec(1 << N_VARS_TO_SEND_GKR_COEFFS) @@ -570,64 +592,29 @@ def verify_gkr_quotient(fiat_shamir: FiatShamir, n_vars: int) -> tuple[EF, list[ return quotient, point, claim_num, claim_den -def mle_of_01234567_etc(point: Sequence[EF]) -> EF: - """MLE of `f(i) = i` (big-endian) at `point`.""" - n = len(point) - return sum(p * (1 << (n - 1 - i)) for i, p in enumerate(point)) - - -def mle_of_zeros_then_ones(n_zeros: int, point: Sequence[EF]) -> EF: - """MLE of `[0]*n_zeros ++ [1]*(2^len(point) − n_zeros)` at `point`.""" - n_values = 1 << len(point) - assert n_zeros <= n_values - if n_zeros == 0: - return ONE - if n_zeros == n_values: - return ZERO - half, tail = n_values >> 1, point[1:] - if n_zeros < half: - return (ONE - point[0]) * mle_of_zeros_then_ones(n_zeros, tail) + point[0] - return point[0] * mle_of_zeros_then_ones(n_zeros - half, tail) - - def finger_print(discriminator: Fp, data: Sequence[EF], beta_eq: Sequence[EF]) -> EF: - """`Σᵢ αᵢ · dataᵢ + α_last · discriminator`.""" assert len(beta_eq) > len(data) return dot_product(beta_eq, data) + beta_eq[-1] * discriminator def sort_tables_by_height(tables: Sequence[Table], heights: dict[str, int]) -> list[tuple[Table, int]]: - """Descending by height, alphabetical on ties (matches Rust `BTreeMap`).""" + """Descending by height, alphabetical on ties""" return sorted([(t, heights[t.name]) for t in tables], key=lambda x: (-x[1], x[0].name)) -def eval_eq(point: Sequence[EF]) -> list[EF]: - """Length-`2^n` evaluation table of `eq(point, ·)`.""" - out = [ONE] - for p in point: - out = [w for v in out for w in (v * (ONE - p), v * p)] - return out - - def verify_generic_logup( fiat_shamir: FiatShamir, - gamma: EF, # `γ` from minimal_zkVM.tex (logup quotient denominator challenge). - beta: list[EF], # `β` from minimal_zkVM.tex (bus-tuple hashing seeds). - beta_eq: list[EF], # eq(β, ·) evaluation table. + gamma: EF, # quotient denominator challenge + beta: list[EF], # bus-tuple hashing seeds + beta_eq: list[EF], # eq(beta, ·) evaluation table log_memory: int, bytecode_multilinear: list[int], tables: Sequence[Table], heights: dict[str, int], ) -> dict: - """GKR-quotient + section-by-section (memory/bytecode/per-table) reconstruction.""" - n_instr_cols = N_INSTRUCTION_COLUMNS - n_runtime_cols = N_RUNTIME_COLUMNS - ds_mem = Fp(LOGUP_MEMORY_DOMAINSEP) - ds_byte = Fp(LOGUP_BYTECODE_DOMAINSEP) - tables_sorted = sort_tables_by_height(tables, heights) - log_bytecode = log2_strict(len(bytecode_multilinear) // (1 << log2_ceil(n_instr_cols))) - log_instr = log2_ceil(n_instr_cols) + log_bytecode = log2_strict(len(bytecode_multilinear) // (1 << log2_ceil(N_INSTRUCTION_COLUMNS))) + log_instr = log2_ceil(N_INSTRUCTION_COLUMNS) total_active_len = ( (1 << log_memory) @@ -654,7 +641,7 @@ def pref_at(offset: int, log_height: int) -> EF: value_memory_acc = fiat_shamir.next_extension_scalar() num -= pref * value_memory_acc value_memory = fiat_shamir.next_extension_scalar() - fp_mem = finger_print(ds_mem, [mle_of_01234567_etc(mem_pt), value_memory], beta_eq) + fp_mem = finger_print(Fp(LOGUP_MEMORY_DOMAINSEP), [mle_of_01234567_etc(mem_pt), value_memory], beta_eq) den += pref * (gamma - fp_mem) offset = 1 << log_memory @@ -666,7 +653,11 @@ def pref_at(offset: int, log_height: int) -> EF: value_bytecode_acc = fiat_shamir.next_extension_scalar() bytecode_value = eval_multilinear_evals([Fp(v) for v in bytecode_multilinear], byte_pt + beta[-log_instr:]) correction = math.prod(ONE - a for a in beta[: len(beta) - log_instr]) - fp_byte = bytecode_value * correction + mle_of_01234567_etc(byte_pt) * beta_eq[n_instr_cols] + beta_eq[-1] * ds_byte + fp_byte = ( + bytecode_value * correction + + mle_of_01234567_etc(byte_pt) * beta_eq[N_INSTRUCTION_COLUMNS] + + beta_eq[-1] * Fp(LOGUP_BYTECODE_DOMAINSEP) + ) num -= pref * value_bytecode_acc den += pref * (gamma - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) offset += 1 << log_byte_pad @@ -703,12 +694,12 @@ def pref_at(offset: int, log_height: int) -> EF: den += pref * bus_den_vals[name] offset_within_table += row_stride elif bus[0] == BusInterractiion.BYTECODE: - cols = list(range(n_runtime_cols, n_runtime_cols + n_instr_cols)) + [table.col("pc")] + cols = list(range(N_RUNTIME_COLUMNS, N_RUNTIME_COLUMNS + N_INSTRUCTION_COLUMNS)) + [table.col("pc")] evals = fiat_shamir.next_extension_scalars_vec(len(cols)) for c_idx, e in zip(cols, evals): table_values[c_idx] = e num += pref # Push direction - den += pref * (gamma - finger_print(ds_byte, evals, beta_eq)) + den += pref * (gamma - finger_print(Fp(LOGUP_BYTECODE_DOMAINSEP), evals, beta_eq)) offset_within_table += row_stride elif bus[0] == BusInterractiion.MEMORY: _, idx_ref, vals_ref, n = bus @@ -725,7 +716,9 @@ def pref_at(offset: int, log_height: int) -> EF: if val_fresh: table_values[val_col] = next(evals) pref = pref_at(offset_within_table, log_n_rows) - fp = finger_print(ds_mem, [table_values[idx_col] + i, table_values[val_col]], beta_eq) + fp = finger_print( + Fp(LOGUP_MEMORY_DOMAINSEP), [table_values[idx_col] + i, table_values[val_col]], beta_eq + ) num += pref # Push direction den += pref * (gamma - fp) offset_within_table += row_stride @@ -825,20 +818,6 @@ def _eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No az(not_jc * (fp_shift - fp)) -# extension_op/air.rs precompile-data layout. -_EXT_OP_FLAG_IS_BE = 4 -_EXT_OP_FLAG_ADD = 8 -_EXT_OP_FLAG_MUL = 16 -_EXT_OP_FLAG_POLY_EQ = 32 -_EXT_OP_LEN_MULTIPLIER = 64 - - -def _quintic_mul_ef(a: Sequence[EF], b: Sequence[EF]) -> list[EF]: - """Multiply two quintic-extension elements whose coefficients are themselves `EF`.""" - assert len(a) == 5 and len(b) == 5 - return quintic_mul(a, b, ZERO) - - def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: # Layout: shift columns 0..13 = (is_be, start, len, flag_{add,mul,poly_eq}, # idx_{a,b}, comp[0..5]); then idx_res, va, vb, vres (5 each). @@ -851,11 +830,11 @@ def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> comp_sh = s[8:13] aux = ( - is_be * _EXT_OP_FLAG_IS_BE - + flag_add * _EXT_OP_FLAG_ADD - + flag_mul * _EXT_OP_FLAG_MUL - + flag_poly_eq * _EXT_OP_FLAG_POLY_EQ - + len_col * _EXT_OP_LEN_MULTIPLIER + is_be * EXT_OP_FLAG_IS_BE + + flag_add * EXT_OP_FLAG_ADD + + flag_mul * EXT_OP_FLAG_MUL + + flag_poly_eq * EXT_OP_FLAG_POLY_EQ + + len_col * EXT_OP_LEN_MULTIPLIER ) eval_precompile_bus_virtual_columns( folder, logup_beta_eq, start * (flag_add + flag_mul + flag_poly_eq), aux, [idx_a, idx_b, idx_res] @@ -867,7 +846,7 @@ def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> is_ee, not_start_sh = ONE - is_be, ONE - start_sh va_x = [va[0]] + [va[k] * is_ee for k in range(1, 5)] comp_tail = [comp_sh[k] * not_start_sh for k in range(5)] - va_vb = _quintic_mul_ef(va_x, vb) + va_vb = quintic_mul(va_x, vb, ZERO) for k in range(5): folder.assert_zero((comp[k] - (va_x[k] + vb[k] + comp_tail[k])) * flag_add) @@ -877,7 +856,7 @@ def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> # poly_eq: comp ← (2·va·vb − va − vb + 1) · comp_sh_or_one. poly_eq_val = [va_vb[k] + va_vb[k] - va_x[k] - vb[k] + (ONE if k == 0 else ZERO) for k in range(5)] comp_sh_or_one = [comp_sh[0] * not_start_sh + start_sh] + [comp_sh[k] * not_start_sh for k in range(1, 5)] - poly_eq_result = _quintic_mul_ef(poly_eq_val, comp_sh_or_one) + poly_eq_result = quintic_mul(poly_eq_val, comp_sh_or_one, ZERO) for k in range(5): folder.assert_zero((comp[k] - poly_eq_result[k]) * flag_poly_eq) for k in range(5): @@ -984,21 +963,13 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: folder.assert_zero(flag_permute * (state[i + POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) -def _eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: - flat, W = folder.flat, POSEIDON_WIDTH +def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: + W = POSEIDON_WIDTH half_initial = half_final = POSEIDON_FULL_ROUNDS // 4 + flat_iter = iter(folder.flat) + take = lambda n: [next(flat_iter) for _ in range(n)] - o = 0 - - def take(n: int) -> list[EF]: - nonlocal o - chunk, o = flat[o : o + n], o + n - return list(chunk) - - # fmt: off - [multiplicity, index_b, index_res, flag_half_output, flag_hardcoded_left, - offset_hardcoded_left, eff_idx_left_first, eff_idx_left_second, flag_permute] = take(9) - # fmt: on + [multiplicity, index_b, index_res, flag_half_output, flag_hardcoded_left, offset_hardcoded_left, eff_idx_left_first, eff_idx_left_second, flag_permute] = take(9) # fmt: skip inputs = take(W) beginning_full_rounds = [take(W) for _ in range(half_initial)] partial_cols = take(POSEIDON_PARTIAL_ROUNDS) @@ -1063,7 +1034,6 @@ def take(n: int) -> list[EF]: *(f"out_right_{i}" for i in range(POSEIDON_WIDTH // 2)), ) # fmt: skip -# Canonical iteration order. Holds all per-table data: layout, buses, AIR config, and the AIR eval fn. TABLES = [ Table( name="execution", @@ -1110,7 +1080,7 @@ def take(n: int) -> list[EF]: n_constraints=101, n_shift=0, max_log_height=21, - air_fn=_eval_air_poseidon16, + air_fn=eval_air_poseidon16, ), ] @@ -1120,17 +1090,17 @@ def verify_execution( proof: Proof, bytecode_multilinear: list[int], ): - tables = TABLES bytecode_log_size = log2_strict(len(bytecode_multilinear)) - log2_ceil(N_INSTRUCTION_COLUMNS) ending_pc = (1 << bytecode_log_size) - 1 bytecode_hash = sponge_hash([Fp(v) for v in bytecode_multilinear]) if len(public_input) != PUBLIC_INPUT_SIZE: raise ProofError("InvalidProof: public_input length mismatch") - state = FiatShamir(proof, poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP)) # domain separator accross bytecode + state = FiatShamir( + proof, poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP) + ) # domain separator accross bytecodes state.observe_scalars(public_input) - - dims = [int(x.value) for x in state.next_base_scalars_vec(2 + len(tables))] + dims = [int(x.value) for x in state.next_base_scalars_vec(2 + len(TABLES))] log_inv_rate, log_memory, *table_log_n_rows = dims if not MIN_WHIR_LOG_INV_RATE <= log_inv_rate <= MAX_WHIR_LOG_INV_RATE: raise ProofError("InvalidRate") @@ -1140,18 +1110,21 @@ def verify_execution( raise ProofError("InvalidProof: bytecode log_size out of range") if log_memory < max(max(table_log_n_rows, default=0), bytecode_log_size): raise ProofError("InvalidProof: memory smaller than tables/bytecode") - for t, h in zip(tables, table_log_n_rows): - if not MIN_LOG_N_ROWS_PER_TABLE <= h <= t.max_log_height: + for table, log_height in zip(TABLES, table_log_n_rows): + if not MIN_LOG_N_ROWS_PER_TABLE <= log_height <= table.max_log_height: raise ProofError( - f"InvalidProof: table {t.name} log_n_rows={h} not in [{MIN_LOG_N_ROWS_PER_TABLE}, {t.max_log_height}]" + f"InvalidProof: table {table.name} log_n_rows={log_height} not in [{MIN_LOG_N_ROWS_PER_TABLE}, {table.max_log_height}]" ) - heights = {t.name: h for t, h in zip(tables, table_log_n_rows)} - n_max = sort_tables_by_height(tables, heights)[0][1] + log_heights = {t.name: h for t, h in zip(TABLES, table_log_n_rows)} + n_max = sort_tables_by_height(TABLES, log_heights)[0][1] total_stacked = ( - (2 << log_memory) + (1 << max(bytecode_log_size, n_max)) + sum(t.n_columns << heights[t.name] for t in tables) + (2 << log_memory) + + (1 << max(bytecode_log_size, n_max)) + + sum(t.n_columns << log_heights[t.name] for t in TABLES) ) + stacked_n_vars = log2_ceil(total_stacked) if stacked_n_vars > TWO_ADICITY + WHIR_INITIAL_FOLDING_FACTOR - log_inv_rate: raise ProofError("InvalidProof: stacked_n_vars exceeds WHIR domain bound") @@ -1164,11 +1137,9 @@ def verify_execution( state.next_extension_scalars_vec(nood), ) - # logup challenges, named after minimal_zkVM.tex: γ is the quotient denominator, - # β are the bus-tuple hashing seeds (length ℓ = log2(bus_width)). - logup_gamma = state.sample_ef() + logup_gamma = state.sample_ef() # the quotient denominator state.duplex() - logup_beta = state.sample_many_ef(log2_ceil(N_INSTRUCTION_COLUMNS + 2)) + logup_beta = state.sample_many_ef(log2_ceil(N_INSTRUCTION_COLUMNS + 2)) # the bus-tuple hashing seeds logup_beta_eq = eval_eq(logup_beta) logup = verify_generic_logup( state, @@ -1177,52 +1148,49 @@ def verify_execution( logup_beta_eq, log_memory, bytecode_multilinear, - tables, - heights, + TABLES, + log_heights, ) gkr_point = logup["gkr_point"] air_alpha = state.sample_ef() - # AIR alpha powers/offsets are laid out in canonical TABLES order - # (mirrors `for table in ALL_TABLES { alpha_offset += n_constraints }` in verify_execution.rs). alpha_offsets: dict[str, int] = {} cumulative = 0 - for t in tables: - alpha_offsets[t.name] = cumulative - cumulative += t.n_constraints + for table in TABLES: + alpha_offsets[table.name] = cumulative + cumulative += table.n_constraints alpha_powers = ef_powers(air_alpha, cumulative) # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (γ − bus_den)). The # sign is the direction of each table's unique Column-multiplicity bus. initial_sum = ZERO - for t in tables: - offset = alpha_offsets[t.name] - initial_sum += alpha_powers[offset] * (logup["bus_num"][t.name] * t.mult_sign) - initial_sum += alpha_powers[offset + 1] * (logup_gamma - logup["bus_den"][t.name]) - sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in tables)) + for table in TABLES: + offset = alpha_offsets[table.name] + initial_sum += alpha_powers[offset] * (logup["bus_num"][table.name] * table.mult_sign) + initial_sum += alpha_powers[offset + 1] * (logup_gamma - logup["bus_den"][table.name]) + sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in TABLES)) - committed = {t.name: [(gkr_point[-heights[t.name] :], logup["columns_values"][t.name], {})] for t in tables} + committed = {t.name: [(gkr_point[-log_heights[t.name] :], logup["columns_values"][t.name], {})] for t in TABLES} my_air_final = ZERO - for t in tables: - log_n_rows = heights[t.name] - col_evals = state.next_extension_scalars_vec(t.n_columns + t.n_shift) - offset = alpha_offsets[t.name] - alpha_slice = alpha_powers[offset : offset + t.n_constraints] - constraint_eval = t.eval_air(col_evals, alpha_slice, logup_beta_eq) + for table in TABLES: + log_n_rows = log_heights[table.name] + col_evals = state.next_extension_scalars_vec(table.n_columns + table.n_shift) + offset = alpha_offsets[table.name] + alpha_slice = alpha_powers[offset : offset + table.n_constraints] + constraint_eval = table.eval_air(col_evals, alpha_slice, logup_beta_eq) natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = math.prod(sc_point[: n_max - log_n_rows]) my_air_final += k_t * eq_poly(gkr_point[-log_n_rows:], natural_pt) * constraint_eval - eq_vals = {i: col_evals[i] for i in range(t.n_columns)} - next_vals = {j: col_evals[t.n_columns + j] for j in range(t.n_shift)} - committed[t.name].append((natural_pt, eq_vals, next_vals)) + eq_vals = {i: col_evals[i] for i in range(table.n_columns)} + next_vals = {j: col_evals[table.n_columns + j] for j in range(table.n_shift)} + committed[table.name].append((natural_pt, eq_vals, next_vals)) if my_air_final != sc_value: raise ProofError("AIR sumcheck: claimed value mismatch") - assert len(public_input) % DIGEST_ELEMS == 0 - pm_point = state.sample_many_ef(log2_strict(len(public_input))) + pm_point = state.sample_many_ef(log2_strict(PUBLIC_INPUT_SIZE)) pm_eval = eval_multilinear_evals(public_input, pm_point) bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size @@ -1242,8 +1210,8 @@ def verify_execution( log_memory, bytecode_log_size, previous, - tables, - heights, + TABLES, + log_heights, committed, ending_pc, ) From 97e313da2805f36d93c9abb596d0cfa3bef3d037 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 03:45:51 +0400 Subject: [PATCH 55/69] wip --- crates/lean_prover/verifier.py | 159 +++++++++++++++++---------------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 9ba9a4d74..e829a5a74 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -57,6 +57,7 @@ STARTING_PC = 0 # every program starts at PC = 0, and ends at PC = len(bytecode) - 1 POSEIDON_WIDTH, POSEIDON_FULL_ROUNDS, POSEIDON_PARTIAL_ROUNDS = 16, 8, 20 +POSEIDON_HALF_FULL_ROUNDS = POSEIDON_FULL_ROUNDS // 2 class ProofError(Exception): @@ -68,7 +69,7 @@ class BusDirection(IntEnum): PULL = -1 -class BusInterractiion(IntEnum): +class BusInteraction(IntEnum): PRECOMPILE = 0 BYTECODE = 1 MEMORY = 2 @@ -92,7 +93,7 @@ def n_columns(self) -> int: @property def n_buses(self) -> int: # MEMORY entries expand to `n` individual buses. - return sum(b[3] if b[0] == BusInterractiion.MEMORY else 1 for b in self.buses) + return sum(b[3] if b[0] == BusInteraction.MEMORY else 1 for b in self.buses) @property def mult_sign(self) -> EF: @@ -124,7 +125,7 @@ def boundary_statements( # T-Sponge (compression instead of permutation) with replacement (instead of xoring / adding the ingested data). def sponge_hash(data: Sequence[Fp]) -> list[Fp]: assert len(data) % SPONGE_RATE == 0 and len(data) > 0 - state = [Fp(len(data))] + [Fp(0)] * (SPONGE_CAPACITY - 1) # IV = [size, 0, 0, 0, ..., 0] + state = [Fp(len(data))] + [Fp(0)] * (SPONGE_CAPACITY - 1) for k in range(len(data) // SPONGE_RATE): state = poseidon16_compress(state, data[k * SPONGE_RATE : (k + 1) * SPONGE_RATE]) return state @@ -152,7 +153,7 @@ def duplex(self) -> None: def _sample_rate(self) -> list[Fp]: assert self.rate_fresh, "stale rate — insert duplex() before sampling" self.rate_fresh = False - return list(self.state[SPONGE_CAPACITY:]) + return self.state[SPONGE_CAPACITY:] def _sample_many(self, n: int) -> list[Fp]: out: list[Fp] = [] @@ -274,7 +275,7 @@ def next_mle(x: Sequence[EF], y: Sequence[EF]) -> EF: def eval_multilinear_evals(evals: Sequence[Fp | EF], point: Sequence[EF]) -> EF: """Evaluate a multilinear in evaluation form at `point`.""" assert len(evals) == 1 << len(point) - cur = list(evals) + cur: Sequence = evals for r in reversed(point): cur = [cur[j] + (cur[j + 1] - cur[j]) * r for j in range(0, len(cur), 2)] return cur[0] @@ -496,9 +497,10 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non prev_commitment, round_folding[-1], ) - # Each STIR constraint's point is `expand_from_univariate(α, n)` = [α, α², α⁴, …]; check that `Σ coeffs[i]·α^i == value`. + # Each STIR constraint's point is `expand_from_univariate(α, n)` = [α, α², α⁴, …] + # (built by SparseStatements.dense, so selector_num_variables == 0 by construction). + # Check that `Σ coeffs[i]·α^i == value` for each smt's single (index 0) value. for smt in final_stir: - assert smt.selector_num_variables == 0 univ_eval = eval_univariate_polynomial(final_coeffs, smt.point[0]) if any(univ_eval != v[1] for v in smt.values): raise ProofError("Final STIR constraint mismatch") @@ -612,14 +614,16 @@ def verify_generic_logup( tables: Sequence[Table], heights: dict[str, int], ) -> dict: - tables_sorted = sort_tables_by_height(tables, heights) - log_bytecode = log2_strict(len(bytecode_multilinear) // (1 << log2_ceil(N_INSTRUCTION_COLUMNS))) + ds_mem = Fp(LOGUP_MEMORY_DOMAINSEP) + ds_byte = Fp(LOGUP_BYTECODE_DOMAINSEP) log_instr = log2_ceil(N_INSTRUCTION_COLUMNS) + log_bytecode = log2_strict(len(bytecode_multilinear)) - log_instr + + tables_sorted = sort_tables_by_height(tables, heights) + tallest_h = tables_sorted[0][1] total_active_len = ( - (1 << log_memory) - + max(1 << log_bytecode, 1 << tables_sorted[0][1]) - + sum(t.n_buses << h for t, h in tables_sorted) + (1 << log_memory) + max(1 << log_bytecode, 1 << tallest_h) + sum(t.n_buses << h for t, h in tables_sorted) ) total_gkr_n_vars = log2_ceil(total_active_len) @@ -627,26 +631,28 @@ def verify_generic_logup( if quotient != ZERO: raise ProofError("logup: GKR sum != 0") - num, den = ZERO, ZERO - def pref_at(offset: int, log_height: int) -> EF: + """Lagrange weight for the layout-offset of a section of height 2^log_height.""" n_missing = total_gkr_n_vars - log_height idx = offset >> log_height - bits = [ONE if (idx >> (n_missing - 1 - i)) & 1 else ZERO for i in range(n_missing)] - return eq_poly(bits, point_gkr[:n_missing]) + return math.prod( + point_gkr[i] if (idx >> (n_missing - 1 - i)) & 1 else ONE - point_gkr[i] for i in range(n_missing) + ) + + num = den = ZERO - # Memory (data order: [value_index, value_memory] mirrors `crates/sub_protocols/src/logup.rs`). + # Memory section --- mem_pt = point_gkr[-log_memory:] pref = pref_at(0, log_memory) value_memory_acc = fiat_shamir.next_extension_scalar() - num -= pref * value_memory_acc value_memory = fiat_shamir.next_extension_scalar() - fp_mem = finger_print(Fp(LOGUP_MEMORY_DOMAINSEP), [mle_of_01234567_etc(mem_pt), value_memory], beta_eq) + fp_mem = finger_print(ds_mem, [mle_of_01234567_etc(mem_pt), value_memory], beta_eq) + num -= pref * value_memory_acc den += pref * (gamma - fp_mem) offset = 1 << log_memory - # Bytecode (padded to the tallest table). - log_byte_pad = max(log_bytecode, tables_sorted[0][1]) + # Bytecode section (padded to the tallest table) --- + log_byte_pad = max(log_bytecode, tallest_h) byte_pt = point_gkr[-log_bytecode:] pref = pref_at(offset, log_bytecode) pref_pad = pref_at(offset, log_byte_pad) @@ -656,24 +662,20 @@ def pref_at(offset: int, log_height: int) -> EF: fp_byte = ( bytecode_value * correction + mle_of_01234567_etc(byte_pt) * beta_eq[N_INSTRUCTION_COLUMNS] - + beta_eq[-1] * Fp(LOGUP_BYTECODE_DOMAINSEP) + + beta_eq[-1] * ds_byte ) num -= pref * value_bytecode_acc den += pref * (gamma - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) offset += 1 << log_byte_pad - # Per-table base offsets in the GKR layout are assigned in sorted-by-height order - # (mirrors `layout_offsets` in sub_protocols/src/logup.rs). + # Per-table section: lay out per-table base offsets (sorted-by-height), then + # walk each table's buses in canonical TABLES order to match the prover's writes. table_offsets: dict[str, int] = {} for table, log_n_rows in tables_sorted: table_offsets[table.name] = offset offset += table.n_buses << log_n_rows final_offset = offset - # Per-table: walk the bus spec in the same order as the Rust prover. The prover - # writes col_evals for new (uncached) columns in `bus.data` order via a single - # `add_extension_scalars` chunk per bus — the verifier must read in the same chunks. - # Iterate tables in canonical TABLES order (matches the new prover scalar layout). bus_num_vals: dict[str, EF] = {} bus_den_vals: dict[str, EF] = {} columns_values: dict[str, dict[int, EF]] = {} @@ -681,49 +683,47 @@ def pref_at(offset: int, log_height: int) -> EF: for table in tables: name = table.name log_n_rows = heights[name] - table_values: dict[int, EF] = {} row_stride = 1 << log_n_rows offset_within_table = table_offsets[name] + table_values: dict[int, EF] = {} + + def read_fresh(cols: list[int]) -> None: + """Read one extension scalar per column not yet in `table_values`, in order.""" + missing = [c for c in cols if c not in table_values] + for c, e in zip(missing, fiat_shamir.next_extension_scalars_vec(len(missing))): + table_values[c] = e for bus in table.buses: pref = pref_at(offset_within_table, log_n_rows) - if bus[0] == BusInterractiion.PRECOMPILE: + kind = bus[0] + if kind == BusInteraction.PRECOMPILE: bus_num_vals[name] = fiat_shamir.next_extension_scalar() bus_den_vals[name] = fiat_shamir.next_extension_scalar() num += pref * bus_num_vals[name] den += pref * bus_den_vals[name] offset_within_table += row_stride - elif bus[0] == BusInterractiion.BYTECODE: + elif kind == BusInteraction.BYTECODE: cols = list(range(N_RUNTIME_COLUMNS, N_RUNTIME_COLUMNS + N_INSTRUCTION_COLUMNS)) + [table.col("pc")] - evals = fiat_shamir.next_extension_scalars_vec(len(cols)) - for c_idx, e in zip(cols, evals): - table_values[c_idx] = e - num += pref # Push direction - den += pref * (gamma - finger_print(Fp(LOGUP_BYTECODE_DOMAINSEP), evals, beta_eq)) + read_fresh(cols) + evals = [table_values[c] for c in cols] + num += pref + den += pref * (gamma - finger_print(ds_byte, evals, beta_eq)) offset_within_table += row_stride - elif bus[0] == BusInterractiion.MEMORY: + elif kind == BusInteraction.MEMORY: _, idx_ref, vals_ref, n = bus idx_col, vals_start = table.col(idx_ref), table.col(vals_ref) - # One bus per row in the group; first sees idx_col fresh, the rest - # see only val_col fresh (mirrors the Rust prover's dedup logic). + # One sub-bus per cell in the group; the prover sends only the + # not-yet-seen columns per row (idx_col is shared across all n rows). for i in range(n): val_col = vals_start + i - idx_fresh = idx_col not in table_values - val_fresh = val_col not in table_values - evals = iter(fiat_shamir.next_extension_scalars_vec(idx_fresh + val_fresh)) - if idx_fresh: - table_values[idx_col] = next(evals) - if val_fresh: - table_values[val_col] = next(evals) + read_fresh([idx_col, val_col]) pref = pref_at(offset_within_table, log_n_rows) - fp = finger_print( - Fp(LOGUP_MEMORY_DOMAINSEP), [table_values[idx_col] + i, table_values[val_col]], beta_eq - ) - num += pref # Push direction + fp = finger_print(ds_mem, [table_values[idx_col] + i, table_values[val_col]], beta_eq) + num += pref den += pref * (gamma - fp) offset_within_table += row_stride else: - raise ProofError(f"unknown bus kind: {bus[0]}") + raise ProofError(f"unknown bus kind: {kind}") columns_values[name] = table_values @@ -739,7 +739,7 @@ def pref_at(offset: int, log_height: int) -> EF: "value_bytecode_acc": value_bytecode_acc, "bus_num": bus_num_vals, "bus_den": bus_den_vals, - "gkr_point": list(point_gkr), + "gkr_point": point_gkr, "columns_values": columns_values, } @@ -779,7 +779,7 @@ def eval_precompile_bus_virtual_columns( folder.assert_zero(dot_product(logup_beta_eq, data) + logup_beta_eq[-1] * discriminator) -def _eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: +def eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: # fmt: off (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, @@ -854,7 +854,7 @@ def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> folder.assert_zero((comp[k] - (va_vb[k] + comp_tail[k])) * flag_mul) # poly_eq: comp ← (2·va·vb − va − vb + 1) · comp_sh_or_one. - poly_eq_val = [va_vb[k] + va_vb[k] - va_x[k] - vb[k] + (ONE if k == 0 else ZERO) for k in range(5)] + poly_eq_val = [2 * va_vb[k] - va_x[k] - vb[k] + (ONE if k == 0 else ZERO) for k in range(5)] comp_sh_or_one = [comp_sh[0] * not_start_sh + start_sh] + [comp_sh[k] * not_start_sh for k in range(1, 5)] poly_eq_result = quintic_mul(poly_eq_val, comp_sh_or_one, ZERO) for k in range(5): @@ -884,7 +884,7 @@ def _build_p1c() -> dict: mds_dense = [[Fp(MDS_FIRST_ROW_16[(j - i) % n]) for j in range(n)] for i in range(n)] # External full-round RCs: first and last `(ROUNDS_F/2) * WIDTH` entries of the # raw round-constant table that drives the actual Poseidon permutation. - hf, t = POSEIDON_FULL_ROUNDS // 2, POSEIDON_WIDTH + hf, t = POSEIDON_HALF_FULL_ROUNDS, POSEIDON_WIDTH rcs = PARAMS_16.round_constants initial_constants = [[Fp(x) for x in rcs[i * t : (i + 1) * t]] for i in range(hf)] tail_start = (hf + POSEIDON_PARTIAL_ROUNDS) * t @@ -921,7 +921,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: const = P1C state = list(cols["inputs"]) initial = list(cols["inputs"]) - half_initial = half_final = POSEIDON_FULL_ROUNDS // 4 + half_initial = half_final = POSEIDON_HALF_FULL_ROUNDS // 2 for r in range(half_initial): state = _full_round(state, const["initial_constants"][2 * r], const["initial_constants"][2 * r + 1]) @@ -965,7 +965,7 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: W = POSEIDON_WIDTH - half_initial = half_final = POSEIDON_FULL_ROUNDS // 4 + half_initial = half_final = POSEIDON_HALF_FULL_ROUNDS // 2 flat_iter = iter(folder.flat) take = lambda n: [next(flat_iter) for _ in range(n)] @@ -1027,9 +1027,9 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No POSEIDON_COLUMNS = ( "multiplicity", "index_b", "index_res", "flag_half_output", "flag_hardcoded_left", "offset_hardcoded_left", "eff_idx_left_first", "eff_idx_left_second", "flag_permute", *(f"input_{i}" for i in range(POSEIDON_WIDTH)), - *(f"begin_r{r}_{i}" for r in range(POSEIDON_FULL_ROUNDS // 4) for i in range(POSEIDON_WIDTH)), + *(f"begin_r{r}_{i}" for r in range(POSEIDON_HALF_FULL_ROUNDS // 2) for i in range(POSEIDON_WIDTH)), *(f"partial_{i}" for i in range(POSEIDON_PARTIAL_ROUNDS)), - *(f"end_r{r}_{i}" for r in range(POSEIDON_FULL_ROUNDS // 4 - 1) for i in range(POSEIDON_WIDTH)), + *(f"end_r{r}_{i}" for r in range(POSEIDON_HALF_FULL_ROUNDS // 2 - 1) for i in range(POSEIDON_WIDTH)), *(f"out_left_{i}" for i in range(POSEIDON_WIDTH // 2)), *(f"out_right_{i}" for i in range(POSEIDON_WIDTH // 2)), ) # fmt: skip @@ -1039,26 +1039,26 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No name="execution", columns=EXECUTION_COLUMNS, buses=( - (BusInterractiion.PRECOMPILE, BusDirection.PUSH), - (BusInterractiion.BYTECODE,), - (BusInterractiion.MEMORY, "addr_a", "value_a", 1), - (BusInterractiion.MEMORY, "addr_b", "value_b", 1), - (BusInterractiion.MEMORY, "addr_c", "value_c", 1), + (BusInteraction.PRECOMPILE, BusDirection.PUSH), + (BusInteraction.BYTECODE,), + (BusInteraction.MEMORY, "addr_a", "value_a", 1), + (BusInteraction.MEMORY, "addr_b", "value_b", 1), + (BusInteraction.MEMORY, "addr_c", "value_c", 1), ), air_degree=5, n_constraints=14, n_shift=2, max_log_height=24, - air_fn=_eval_air_execution, + air_fn=eval_air_execution, ), Table( name="extension", columns=EXTENSION_COLUMNS, buses=( - (BusInterractiion.PRECOMPILE, BusDirection.PULL), - (BusInterractiion.MEMORY, "idx_a", "va_0", 5), - (BusInterractiion.MEMORY, "idx_b", "vb_0", 5), - (BusInterractiion.MEMORY, "idx_res", "vres_0", 5), + (BusInteraction.PRECOMPILE, BusDirection.PULL), + (BusInteraction.MEMORY, "idx_a", "va_0", 5), + (BusInteraction.MEMORY, "idx_b", "vb_0", 5), + (BusInteraction.MEMORY, "idx_res", "vres_0", 5), ), air_degree=6, n_constraints=35, @@ -1070,11 +1070,11 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No name="poseidon", columns=POSEIDON_COLUMNS, buses=( - (BusInterractiion.PRECOMPILE, BusDirection.PULL), - (BusInterractiion.MEMORY, "eff_idx_left_first", "input_0", 4), - (BusInterractiion.MEMORY, "eff_idx_left_second", "input_4", 4), - (BusInterractiion.MEMORY, "index_b", "input_8", 8), - (BusInterractiion.MEMORY, "index_res", "out_left_0", 16), + (BusInteraction.PRECOMPILE, BusDirection.PULL), + (BusInteraction.MEMORY, "eff_idx_left_first", "input_0", 4), + (BusInteraction.MEMORY, "eff_idx_left_second", "input_4", 4), + (BusInteraction.MEMORY, "index_b", "input_8", 8), + (BusInteraction.MEMORY, "index_res", "out_left_0", 16), ), air_degree=10, n_constraints=101, @@ -1096,9 +1096,7 @@ def verify_execution( if len(public_input) != PUBLIC_INPUT_SIZE: raise ProofError("InvalidProof: public_input length mismatch") - state = FiatShamir( - proof, poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP) - ) # domain separator accross bytecodes + state = FiatShamir(proof, poseidon16_compress(bytecode_hash, SNARK_DOMAIN_SEP)) # domain separator across bytecodes state.observe_scalars(public_input) dims = [int(x.value) for x in state.next_base_scalars_vec(2 + len(TABLES))] log_inv_rate, log_memory, *table_log_n_rows = dims @@ -1193,8 +1191,11 @@ def verify_execution( pm_point = state.sample_many_ef(log2_strict(PUBLIC_INPUT_SIZE)) pm_eval = eval_multilinear_evals(public_input, pm_point) + # Statements about the non-table columns in the stacked polynomial: + # value_memory + memory acc counts (memory layout), public memory (program input), + # and bytecode acc counts (offset by the bytecode-acc section's position in the layout). bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size - previous = [ + previous_statements = [ SparseStatements( stacked_n_vars, gkr_point[-log_memory:], @@ -1209,7 +1210,7 @@ def verify_execution( stacked_n_vars, log_memory, bytecode_log_size, - previous, + previous_statements, TABLES, log_heights, committed, From 3f5d82a6494a043d9423900d7994aaa1ca43f55a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 03:51:52 +0400 Subject: [PATCH 56/69] w --- crates/lean_prover/verifier.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index e829a5a74..d3dcd2ccd 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -1152,31 +1152,25 @@ def verify_execution( gkr_point = logup["gkr_point"] air_alpha = state.sample_ef() + alpha_powers = ef_powers(air_alpha, sum(t.n_constraints for t in TABLES)) - alpha_offsets: dict[str, int] = {} - cumulative = 0 + # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (γ − bus_den)). The sign is + # the direction of each table's unique Column-multiplicity bus. + initial_sum, offset = ZERO, 0 for table in TABLES: - alpha_offsets[table.name] = cumulative - cumulative += table.n_constraints - alpha_powers = ef_powers(air_alpha, cumulative) - - # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (γ − bus_den)). The - # sign is the direction of each table's unique Column-multiplicity bus. - initial_sum = ZERO - for table in TABLES: - offset = alpha_offsets[table.name] initial_sum += alpha_powers[offset] * (logup["bus_num"][table.name] * table.mult_sign) initial_sum += alpha_powers[offset + 1] * (logup_gamma - logup["bus_den"][table.name]) + offset += table.n_constraints sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in TABLES)) committed = {t.name: [(gkr_point[-log_heights[t.name] :], logup["columns_values"][t.name], {})] for t in TABLES} - my_air_final = ZERO + my_air_final, offset = ZERO, 0 for table in TABLES: log_n_rows = log_heights[table.name] col_evals = state.next_extension_scalars_vec(table.n_columns + table.n_shift) - offset = alpha_offsets[table.name] - alpha_slice = alpha_powers[offset : offset + table.n_constraints] - constraint_eval = table.eval_air(col_evals, alpha_slice, logup_beta_eq) + alphas = alpha_powers[offset : offset + table.n_constraints] + offset += table.n_constraints + constraint_eval = table.eval_air(col_evals, alphas, logup_beta_eq) natural_pt = list(reversed(sc_point[-log_n_rows:])) if log_n_rows else [] k_t = math.prod(sc_point[: n_max - log_n_rows]) From 55488b01c713fb5aae64143ca9e2f0b675b0c776 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 04:10:21 +0400 Subject: [PATCH 57/69] wip --- crates/lean_prover/verifier.py | 86 ++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index d3dcd2ccd..aa5e6da62 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -105,7 +105,7 @@ def col(self, ref) -> int: return ref if isinstance(ref, int) else self.columns.index(ref) def eval_air(self, col_evals: Sequence[EF], alpha_powers: Sequence[EF], logup_beta_eq: list[EF]) -> EF: - folder = ConstraintFolder(col_evals[: self.n_columns], col_evals[self.n_columns :], alpha_powers) + folder = ConstraintFolder(col_evals[: self.n_columns], col_evals[self.n_columns :], alpha_powers, self.columns) self.air_fn(folder, logup_beta_eq) return folder.accumulator @@ -744,16 +744,25 @@ def read_fresh(cols: list[int]) -> None: } +class Cols(dict): + def arr(self, prefix: str, n: int) -> list: + return [self[f"{prefix}_{i}"] for i in range(n)] + + class ConstraintFolder: - """`flat`/`shift` = current-row / next-row column evals. Each `assert_zero(x)` - adds `alpha_powers[i]·x` to the accumulator.""" - - def __init__(self, flat: Sequence[EF], shift: Sequence[EF], alpha_powers: Sequence[EF]) -> None: - self.flat, self.shift, self.alpha_powers = ( - list(flat), - list(shift), - list(alpha_powers), - ) + """`flat`/`shift` = current-row / next-row column evals. `cur[name]` and `nxt[name]` + expose the same data indexed by column name (the table's `columns` tuple). Each + `assert_zero(x)` adds `alpha_powers[i]·x` to the accumulator.""" + + def __init__( + self, flat: Sequence[EF], shift: Sequence[EF], alpha_powers: Sequence[EF], columns: Sequence[str] + ) -> None: + self.flat = list(flat) + self.shift = list(shift) + self.alpha_powers = list(alpha_powers) + # Shift columns are always the first `n_shift` columns of the table. + self.cur = Cols(zip(columns, self.flat)) + self.nxt = Cols(zip(columns[: len(self.shift)], self.shift)) self.accumulator: EF = ZERO self.i = 0 @@ -780,12 +789,15 @@ def eval_precompile_bus_virtual_columns( def eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: - # fmt: off - (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, - operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, - flag_ab_fp, mul, jump, aux, discriminator) = folder.flat[:20] - # fmt: on - pc_shift, fp_shift = folder.shift[0], folder.shift[1] + c, n = folder.cur, folder.nxt + pc, fp = c["pc"], c["fp"] + addr_a, addr_b, addr_c = c["addr_a"], c["addr_b"], c["addr_c"] + value_a, value_b, value_c = c["value_a"], c["value_b"], c["value_c"] + operand_a, operand_b, operand_c = c["operand_a"], c["operand_b"], c["operand_c"] + flag_a, flag_b, flag_c = c["flag_a"], c["flag_b"], c["flag_c"] + flag_c_fp, flag_ab_fp = c["flag_c_fp"], c["flag_ab_fp"] + mul, jump, aux, discriminator = c["mul"], c["jump"], c["aux"], c["discriminator"] + pc_shift, fp_shift = n["pc"], n["fp"] # nu_x = flag·operand + (1 − flag − flag_ab_fp)·value + flag_ab_fp·(fp + operand) nfa = ONE - flag_a - flag_ab_fp @@ -819,15 +831,15 @@ def eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> Non def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: - # Layout: shift columns 0..13 = (is_be, start, len, flag_{add,mul,poly_eq}, - # idx_{a,b}, comp[0..5]); then idx_res, va, vb, vres (5 each). - f = folder.flat - is_be, start, len_col, flag_add, flag_mul, flag_poly_eq, idx_a, idx_b = f[:8] - comp, idx_res = f[8:13], f[13] - va, vb, vres = f[14:19], f[19:24], f[24:29] - s = folder.shift - is_be_sh, start_sh, len_sh, flag_add_sh, flag_mul_sh, flag_poly_eq_sh, idx_a_sh, idx_b_sh = s[:8] - comp_sh = s[8:13] + c, n = folder.cur, folder.nxt + is_be, start, len_col = c["is_be"], c["start"], c["len"] + flag_add, flag_mul, flag_poly_eq = c["flag_add"], c["flag_mul"], c["flag_poly_eq"] + idx_a, idx_b, idx_res = c["idx_a"], c["idx_b"], c["idx_res"] + comp, va, vb, vres = c.arr("comp", 5), c.arr("va", 5), c.arr("vb", 5), c.arr("vres", 5) + is_be_sh, start_sh, len_sh = n["is_be"], n["start"], n["len"] + flag_add_sh, flag_mul_sh, flag_poly_eq_sh = n["flag_add"], n["flag_mul"], n["flag_poly_eq"] + idx_a_sh, idx_b_sh = n["idx_a"], n["idx_b"] + comp_sh = n.arr("comp", 5) aux = ( is_be * EXT_OP_FLAG_IS_BE @@ -964,17 +976,21 @@ def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: - W = POSEIDON_WIDTH + c = folder.cur half_initial = half_final = POSEIDON_HALF_FULL_ROUNDS // 2 - flat_iter = iter(folder.flat) - take = lambda n: [next(flat_iter) for _ in range(n)] - - [multiplicity, index_b, index_res, flag_half_output, flag_hardcoded_left, offset_hardcoded_left, eff_idx_left_first, eff_idx_left_second, flag_permute] = take(9) # fmt: skip - inputs = take(W) - beginning_full_rounds = [take(W) for _ in range(half_initial)] - partial_cols = take(POSEIDON_PARTIAL_ROUNDS) - ending_full_rounds = [take(W) for _ in range(half_final - 1)] - outputs_left, outputs_right = take(W // 2), take(W // 2) + + multiplicity = c["multiplicity"] + index_b, index_res = c["index_b"], c["index_res"] + flag_half_output, flag_hardcoded_left = c["flag_half_output"], c["flag_hardcoded_left"] + offset_hardcoded_left = c["offset_hardcoded_left"] + eff_idx_left_first, eff_idx_left_second = c["eff_idx_left_first"], c["eff_idx_left_second"] + flag_permute = c["flag_permute"] + inputs = c.arr("input", POSEIDON_WIDTH) + beginning_full_rounds = [c.arr(f"begin_r{r}", POSEIDON_WIDTH) for r in range(half_initial)] + partial_cols = c.arr("partial", POSEIDON_PARTIAL_ROUNDS) + ending_full_rounds = [c.arr(f"end_r{r}", POSEIDON_WIDTH) for r in range(half_final - 1)] + outputs_left = c.arr("out_left", POSEIDON_WIDTH // 2) + outputs_right = c.arr("out_right", POSEIDON_WIDTH // 2) discriminator = ( POSEIDON_DISCRIMINATOR_BASE From 754555944a8e5056d71184c8ca1f4919f47d56f7 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 04:22:06 +0400 Subject: [PATCH 58/69] wip --- crates/lean_prover/verifier.py | 77 +++++++++++++--------------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index aa5e6da62..07edaba56 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -48,10 +48,7 @@ LOGUP_MEMORY_DOMAINSEP, LOGUP_BYTECODE_DOMAINSEP = 1, 2 POSEIDON_DISCRIMINATOR_BASE = 3 # odd ≥ 3 POSEIDON_PERMUTE_SHIFT, POSEIDON_HALF_OUTPUT_SHIFT = 1 << 1, 1 << 2 -POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT, POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = ( - 1 << 3, - 1 << 4, -) +POSEIDON_HARDCODED_LEFT_4_FLAG_SHIFT, POSEIDON_HARDCODED_LEFT_4_OFFSET_SHIFT = 1 << 3, 1 << 4 EXT_OP_FLAG_IS_BE, EXT_OP_FLAG_ADD, EXT_OP_FLAG_MUL, EXT_OP_FLAG_POLY_EQ, EXT_OP_LEN_MULTIPLIER = 4, 8, 16, 32, 64 STARTING_PC = 0 # every program starts at PC = 0, and ends at PC = len(bytecode) - 1 @@ -84,7 +81,7 @@ class Table: n_constraints: int n_shift: int # shift (next-row) columns are always the first ones max_log_height: int - air_fn: object # (folder, logup_beta_eq) -> None, fills folder with AIR constraints + air_constraints_fn: object # (folder, logup_beta_eq) -> None @property def n_columns(self) -> int: @@ -92,21 +89,18 @@ def n_columns(self) -> int: @property def n_buses(self) -> int: - # MEMORY entries expand to `n` individual buses. return sum(b[3] if b[0] == BusInteraction.MEMORY else 1 for b in self.buses) @property - def mult_sign(self) -> EF: - # First bus is always the ROW_MULT bus; its direction (Push=+1, Pull=−1) IS the sign. - return EF(self.buses[0][1]) + def precompile_bus_interraction_sign(self) -> EF: + return EF(self.buses[0][1]) # precompile interraction is the first, by convention - def col(self, ref) -> int: - """Resolve a column reference (name or already-int index) to its integer index.""" - return ref if isinstance(ref, int) else self.columns.index(ref) + def col(self, name: str) -> int: + return self.columns.index(name) def eval_air(self, col_evals: Sequence[EF], alpha_powers: Sequence[EF], logup_beta_eq: list[EF]) -> EF: folder = ConstraintFolder(col_evals[: self.n_columns], col_evals[self.n_columns :], alpha_powers, self.columns) - self.air_fn(folder, logup_beta_eq) + self.air_constraints_fn(folder, logup_beta_eq) return folder.accumulator def boundary_statements( @@ -115,9 +109,8 @@ def boundary_statements( """Static row-pinning constraints. Only the execution table pins the PC column.""" if self.name != "execution": return [] - pc_col = self.col("pc") return [ - SparseStatements.unique_value(stacked_n_vars, offset + (pc_col << n_vars) + idx, EF(pc)) + SparseStatements.unique_value(stacked_n_vars, offset + (self.col("pc") << n_vars) + idx, EF(pc)) for idx, pc in [(0, STARTING_PC), ((1 << n_vars) - 1, ending_pc)] ] @@ -435,7 +428,6 @@ def whir_verify( def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> None: nonlocal target fiat_shamir.duplex() - # Fold each constraint value into `target` via successive powers of γ. gamma = fiat_shamir.sample_ef() combo: list[EF] = [] g = ONE @@ -497,9 +489,7 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non prev_commitment, round_folding[-1], ) - # Each STIR constraint's point is `expand_from_univariate(α, n)` = [α, α², α⁴, …] - # (built by SparseStatements.dense, so selector_num_variables == 0 by construction). - # Check that `Σ coeffs[i]·α^i == value` for each smt's single (index 0) value. + # Each STIR constraint's point is `expand_from_univariate(α, n)` = [α, α², α⁴, …]. We check that `Σ coeffs[i]·α^i == value` for each smt for smt in final_stir: univ_eval = eval_univariate_polynomial(final_coeffs, smt.point[0]) if any(univ_eval != v[1] for v in smt.values): @@ -641,7 +631,7 @@ def pref_at(offset: int, log_height: int) -> EF: num = den = ZERO - # Memory section --- + # Memory section mem_pt = point_gkr[-log_memory:] pref = pref_at(0, log_memory) value_memory_acc = fiat_shamir.next_extension_scalar() @@ -651,7 +641,7 @@ def pref_at(offset: int, log_height: int) -> EF: den += pref * (gamma - fp_mem) offset = 1 << log_memory - # Bytecode section (padded to the tallest table) --- + # Bytecode section (padded to the tallest table) log_byte_pad = max(log_bytecode, tallest_h) byte_pt = point_gkr[-log_bytecode:] pref = pref_at(offset, log_bytecode) @@ -668,8 +658,7 @@ def pref_at(offset: int, log_height: int) -> EF: den += pref * (gamma - fp_byte) + pref_pad * mle_of_zeros_then_ones(1 << log_bytecode, point_gkr[-log_byte_pad:]) offset += 1 << log_byte_pad - # Per-table section: lay out per-table base offsets (sorted-by-height), then - # walk each table's buses in canonical TABLES order to match the prover's writes. + # Per-table section table_offsets: dict[str, int] = {} for table, log_n_rows in tables_sorted: table_offsets[table.name] = offset @@ -750,10 +739,6 @@ def arr(self, prefix: str, n: int) -> list: class ConstraintFolder: - """`flat`/`shift` = current-row / next-row column evals. `cur[name]` and `nxt[name]` - expose the same data indexed by column name (the table's `columns` tuple). Each - `assert_zero(x)` adds `alpha_powers[i]·x` to the accumulator.""" - def __init__( self, flat: Sequence[EF], shift: Sequence[EF], alpha_powers: Sequence[EF], columns: Sequence[str] ) -> None: @@ -812,22 +797,21 @@ def eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> Non deref = aux * (aux - ONE) * ((P + 1) // 2) # (P+1)/2 is the inverse of 2 mod P is_precompile = ONE - add - mul - deref - jump - az = folder.assert_zero eval_precompile_bus_virtual_columns(folder, logup_beta_eq, is_precompile, discriminator, [nu_a, nu_b, nu_c]) - az(nfa * (addr_a - (fp + operand_a))) - az(nfb * (addr_b - (fp + operand_b))) - az(nfc * (addr_c - (fp + operand_c))) - az(add * (nu_b - (nu_a + nu_c))) - az(mul * (nu_b - nu_a * nu_c)) - az(deref * (addr_b - (value_a + operand_b))) - az(deref * (value_b - nu_c)) + folder.assert_zero(nfa * (addr_a - (fp + operand_a))) + folder.assert_zero(nfb * (addr_b - (fp + operand_b))) + folder.assert_zero(nfc * (addr_c - (fp + operand_c))) + folder.assert_zero(add * (nu_b - (nu_a + nu_c))) + folder.assert_zero(mul * (nu_b - nu_a * nu_c)) + folder.assert_zero(deref * (addr_b - (value_a + operand_b))) + folder.assert_zero(deref * (value_b - nu_c)) jc = jump * nu_a - az(jc * (nu_a - ONE)) - az(jc * (pc_shift - nu_b)) - az(jc * (fp_shift - nu_c)) + folder.assert_zero(jc * (nu_a - ONE)) + folder.assert_zero(jc * (pc_shift - nu_b)) + folder.assert_zero(jc * (fp_shift - nu_c)) not_jc = ONE - jc - az(not_jc * (pc_shift - (pc + ONE))) - az(not_jc * (fp_shift - fp)) + folder.assert_zero(not_jc * (pc_shift - (pc + ONE))) + folder.assert_zero(not_jc * (fp_shift - fp)) def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: @@ -1065,7 +1049,7 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No n_constraints=14, n_shift=2, max_log_height=24, - air_fn=eval_air_execution, + air_constraints_fn=eval_air_execution, ), Table( name="extension", @@ -1080,7 +1064,7 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No n_constraints=35, n_shift=13, max_log_height=21, - air_fn=eval_air_extension_op, + air_constraints_fn=eval_air_extension_op, ), Table( name="poseidon", @@ -1096,7 +1080,7 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No n_constraints=101, n_shift=0, max_log_height=21, - air_fn=eval_air_poseidon16, + air_constraints_fn=eval_air_poseidon16, ), ] @@ -1170,11 +1154,9 @@ def verify_execution( air_alpha = state.sample_ef() alpha_powers = ef_powers(air_alpha, sum(t.n_constraints for t in TABLES)) - # Initial AIR sum: Σ_table (α^o · signed_num + α^(o+1) · (γ − bus_den)). The sign is - # the direction of each table's unique Column-multiplicity bus. initial_sum, offset = ZERO, 0 for table in TABLES: - initial_sum += alpha_powers[offset] * (logup["bus_num"][table.name] * table.mult_sign) + initial_sum += alpha_powers[offset] * (logup["bus_num"][table.name] * table.precompile_bus_interraction_sign) initial_sum += alpha_powers[offset + 1] * (logup_gamma - logup["bus_den"][table.name]) offset += table.n_constraints sc_point, sc_value = verify_sumcheck(state, initial_sum, n_max, max(t.air_degree + 1 for t in TABLES)) @@ -1201,9 +1183,6 @@ def verify_execution( pm_point = state.sample_many_ef(log2_strict(PUBLIC_INPUT_SIZE)) pm_eval = eval_multilinear_evals(public_input, pm_point) - # Statements about the non-table columns in the stacked polynomial: - # value_memory + memory acc counts (memory layout), public memory (program input), - # and bytecode acc counts (offset by the bytecode-acc section's position in the layout). bytecode_acc_idx = (2 << log_memory) >> bytecode_log_size previous_statements = [ SparseStatements( From b3032691a85c705e30a9d6e9223f87de4658fabb Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 04:46:03 +0400 Subject: [PATCH 59/69] w --- crates/lean_prover/primitives.py | 56 ++++++++--- crates/lean_prover/verifier.py | 153 +++++++++++-------------------- 2 files changed, 96 insertions(+), 113 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index 09c0fc021..ba687ab30 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -42,6 +42,9 @@ def __mul__(self, other): def __pow__(self, exponent: int) -> "Fp": return Fp(pow(self.value, exponent, P)) + def cube(self) -> "Fp": + return self * self * self + def __eq__(self, other: object) -> bool: return isinstance(other, Fp) and self.value == other.value @@ -117,6 +120,9 @@ def __hash__(self): def __repr__(self): return f"EF({[int(x.value) for x in self.c]})" + def cube(self) -> "EF": + return self * self * self + def inv(self) -> "EF": result, base, n = ONE, self, P**5 - 2 while n > 0: @@ -280,9 +286,14 @@ def next_multiple_of(n: int, k: int) -> int: # --------------------------------------------------------------------------- -# sparse partial-round optimization for the AIR. +# Poseidon2-16 sparse optimization for partial rounds (see Appendix B of https://eprint.iacr.org/2019/458.pdf) # --------------------------------------------------------------------------- +POSEIDON_FULL_ROUNDS = 8 +POSEIDON_WIDTH = 16 +POSEIDON_PARTIAL_ROUNDS = 20 +POSEIDON_HALF_FULL_ROUNDS = POSEIDON_FULL_ROUNDS // 2 # = 4 full rounds per side + def _mat_mul(a: list[list[int]], b: list[list[int]], n: int) -> list[list[int]]: return [[sum(a[i][k] * b[k][j] for k in range(n)) % P for j in range(n)] for i in range(n)] @@ -318,18 +329,25 @@ def _gauss_jordan_inv(m_in: list[list[int]], n: int) -> list[list[int]]: return inv -def _compute_air_sparse_constants() -> dict: +def _compute_sparse_constants() -> dict: + """Compress partial rounds into per-round (sparse first row, sparse v, scalar rc) triples. + + Output: + sparse_m_i: 16×16 — applied once when entering the partial-round phase. + sparse_first_row[r], sparse_v[r]: row-r operator that replaces the full MDS matvec. + sparse_first_round_constants, sparse_scalar_round_constants: compressed RCs. + """ w = PARAMS_16.width hf = PARAMS_16.rounds_f // 2 rp = PARAMS_16.rounds_p rc = PARAMS_16.round_constants - # Dense circulant MDS: M[i][j] = MDS_FIRST_ROW_16[(j - i) mod w]. mds = [[MDS_FIRST_ROW_16[(j - i) % w] for j in range(w)] for i in range(w)] mds_inv = _gauss_jordan_inv(mds, w) partial_rc = [list(rc[(hf + i) * w : (hf + i + 1) * w]) for i in range(rp)] - # --- Compress round constants via backward substitution through MDS^{-1}. --- + # Backward substitution through MDS^{-1} to collapse each round's RC vector into + # one scalar (the lane-0 RC kept inline) plus a constant carry on the next round. scalar_rc: list[int] = [0] * rp tmp = list(partial_rc[rp - 1]) for i in range(rp - 2, -1, -1): @@ -339,9 +357,9 @@ def _compute_air_sparse_constants() -> dict: for j in range(1, w): tmp[j] = (tmp[j] + inv_cip[j]) % P sparse_first_round_constants = tmp - sparse_scalar_round_constants = scalar_rc[1:] # length rp - 1 + sparse_scalar_round_constants = scalar_rc[1:] - # --- Factor MDS into per-round sparse matrices. --- + # Factor MDS into per-round sparse matrices (first row + v column). mds_t = _mat_transpose(mds, w) m_mul = [row[:] for row in mds_t] v_collection: list[list[int]] = [] @@ -366,13 +384,9 @@ def _compute_air_sparse_constants() -> dict: v_collection.reverse() w_hat_collection.reverse() - # Pre-assemble full first rows: [mds[0][0], ŵ[0], ..., ŵ[14]]. mds_0_0 = mds[0][0] sparse_first_row = [[mds_0_0] + w_hat_collection[r][:15] for r in range(rp)] - return { - "half_full_rounds": hf, - "partial_rounds": rp, "sparse_m_i": sparse_m_i, "sparse_first_row": sparse_first_row, "sparse_v": v_collection, @@ -381,4 +395,24 @@ def _compute_air_sparse_constants() -> dict: } -POSEIDON1_AIR_CONSTANTS = _compute_air_sparse_constants() +_HF, _W = POSEIDON_HALF_FULL_ROUNDS, POSEIDON_WIDTH +_N = len(MDS_FIRST_ROW_16) +_RCS = PARAMS_16.round_constants +_SPARSE = _compute_sparse_constants() + +# Dense circulant MDS matrix: M[i][j] = MDS_FIRST_ROW_16[(j - i) % 16]. +POSEIDON_AIR_MDS_DENSE: list[list[Fp]] = [[Fp(MDS_FIRST_ROW_16[(j - i) % _N]) for j in range(_N)] for i in range(_N)] + +# External full-round constants: first / last POSEIDON_HALF_FULL_ROUNDS slices of round_constants. +POSEIDON_AIR_INITIAL_CONSTANTS: list[list[Fp]] = [[Fp(v) for v in _RCS[i * _W : (i + 1) * _W]] for i in range(_HF)] +_TAIL = (_HF + POSEIDON_PARTIAL_ROUNDS) * _W +POSEIDON_AIR_FINAL_CONSTANTS: list[list[Fp]] = [ + [Fp(v) for v in _RCS[_TAIL + i * _W : _TAIL + (i + 1) * _W]] for i in range(_HF) +] + +# Sparse partial-round constants (Fp-wrapped). +POSEIDON_AIR_SPARSE_M_I: list[list[Fp]] = [[Fp(v) for v in row] for row in _SPARSE["sparse_m_i"]] +POSEIDON_AIR_SPARSE_FIRST_ROW: list[list[Fp]] = [[Fp(v) for v in row] for row in _SPARSE["sparse_first_row"]] +POSEIDON_AIR_SPARSE_V: list[list[Fp]] = [[Fp(v) for v in row] for row in _SPARSE["sparse_v"]] +POSEIDON_AIR_SPARSE_FIRST_RC: list[Fp] = [Fp(v) for v in _SPARSE["sparse_first_round_constants"]] +POSEIDON_AIR_SPARSE_SCALAR_RC: list[Fp] = [Fp(v) for v in _SPARSE["sparse_scalar_round_constants"]] diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 07edaba56..2c0d6198d 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -53,9 +53,6 @@ STARTING_PC = 0 # every program starts at PC = 0, and ends at PC = len(bytecode) - 1 -POSEIDON_WIDTH, POSEIDON_FULL_ROUNDS, POSEIDON_PARTIAL_ROUNDS = 16, 8, 20 -POSEIDON_HALF_FULL_ROUNDS = POSEIDON_FULL_ROUNDS // 2 - class ProofError(Exception): pass @@ -93,7 +90,7 @@ def n_buses(self) -> int: @property def precompile_bus_interraction_sign(self) -> EF: - return EF(self.buses[0][1]) # precompile interraction is the first, by convention + return EF(self.buses[0][1]) # precompile interraction is the first, by convention def col(self, name: str) -> int: return self.columns.index(name) @@ -872,96 +869,17 @@ def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> folder.assert_zero(start_sh * (len_col - ONE)) -def _build_p1c() -> dict: - raw = POSEIDON1_AIR_CONSTANTS - fp_mat = lambda m: [[Fp(v) for v in row] for row in m] - fp_vec = lambda v: [Fp(x) for x in v] - n = len(MDS_FIRST_ROW_16) - mds_dense = [[Fp(MDS_FIRST_ROW_16[(j - i) % n]) for j in range(n)] for i in range(n)] - # External full-round RCs: first and last `(ROUNDS_F/2) * WIDTH` entries of the - # raw round-constant table that drives the actual Poseidon permutation. - hf, t = POSEIDON_HALF_FULL_ROUNDS, POSEIDON_WIDTH - rcs = PARAMS_16.round_constants - initial_constants = [[Fp(x) for x in rcs[i * t : (i + 1) * t]] for i in range(hf)] - tail_start = (hf + POSEIDON_PARTIAL_ROUNDS) * t - final_constants = [[Fp(x) for x in rcs[tail_start + i * t : tail_start + (i + 1) * t]] for i in range(hf)] - return { - "initial_constants": initial_constants, - "final_constants": final_constants, - "sparse_m_i": fp_mat(raw["sparse_m_i"]), - "sparse_first_row": fp_mat(raw["sparse_first_row"]), - "sparse_v": fp_mat(raw["sparse_v"]), - "sparse_first_rc": fp_vec(raw["sparse_first_round_constants"]), - "sparse_scalar_rc": fp_vec(raw["sparse_scalar_round_constants"]), - "mds_dense": mds_dense, - } - - -P1C = _build_p1c() - - -def _matvec_kb(mat: list[list[Fp]], state: list[EF]) -> list[EF]: - return [dot_product(state, row) for row in mat] - - def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: + """Two consecutive Poseidon full rounds, fused as one AIR step.""" for rc in (rc1, rc2): - sbox = [(t := s + c) * t * t for s, c in zip(state, rc)] - state = _matvec_kb(P1C["mds_dense"], sbox) + sbox = [(s + c).cube() for s, c in zip(state, rc)] + state = [dot_product(sbox, row) for row in POSEIDON_AIR_MDS_DENSE] return state -def _eval_poseidon1_16(folder: ConstraintFolder, cols: dict) -> None: - """AIR for Poseidon1-16. Each `post` column commits an intermediate state, which we - constrain against the local computation, then adopt to bound polynomial degree.""" - const = P1C - state = list(cols["inputs"]) - initial = list(cols["inputs"]) - half_initial = half_final = POSEIDON_HALF_FULL_ROUNDS // 2 - - for r in range(half_initial): - state = _full_round(state, const["initial_constants"][2 * r], const["initial_constants"][2 * r + 1]) - for i, post in enumerate(cols["beginning_full_rounds"][r]): - folder.assert_eq(state[i], post) - state[i] = post - - state = [s + c for s, c in zip(state, const["sparse_first_rc"])] - state = _matvec_kb(const["sparse_m_i"], state) - - n_partial = POSEIDON_PARTIAL_ROUNDS - for r in range(n_partial): - folder.assert_eq(state[0] * state[0] * state[0], cols["partial_rounds"][r]) - state[0] = cols["partial_rounds"][r] - if r < n_partial - 1: - state[0] += const["sparse_scalar_rc"][r] - old_s0 = state[0] - state[0] = dot_product(state, const["sparse_first_row"][r]) - for i in range(1, POSEIDON_WIDTH): - state[i] += old_s0 * const["sparse_v"][r][i - 1] - - for r in range(half_final - 1): - state = _full_round(state, const["final_constants"][2 * r], const["final_constants"][2 * r + 1]) - for i, post in enumerate(cols["ending_full_rounds"][r]): - folder.assert_eq(state[i], post) - state[i] = post - - # Last full round: compression mode adds `initial` (gated by flag_half_output for lanes 4..8); - # permute mode (flag_permute=1) outputs raw state. - last = 2 * (half_final - 1) - state = _full_round(state, const["final_constants"][last], const["final_constants"][last + 1]) - flag_permute = cols["flag_permute"] - not_permute = ONE - flag_permute - compression_last4 = not_permute - cols["flag_half_output"] - for i in range(POSEIDON_WIDTH // 2): - gate = not_permute if i < (DIGEST_ELEMS // 2) else compression_last4 - folder.assert_zero(gate * (state[i] + initial[i] - cols["outputs_left"][i])) - folder.assert_zero(flag_permute * (state[i] - cols["outputs_left"][i])) - folder.assert_zero(flag_permute * (state[i + POSEIDON_WIDTH // 2] - cols["outputs_right"][i])) - - def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: c = folder.cur - half_initial = half_final = POSEIDON_HALF_FULL_ROUNDS // 2 + half_pairs = POSEIDON_HALF_FULL_ROUNDS // 2 multiplicity = c["multiplicity"] index_b, index_res = c["index_b"], c["index_res"] @@ -970,9 +888,9 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No eff_idx_left_first, eff_idx_left_second = c["eff_idx_left_first"], c["eff_idx_left_second"] flag_permute = c["flag_permute"] inputs = c.arr("input", POSEIDON_WIDTH) - beginning_full_rounds = [c.arr(f"begin_r{r}", POSEIDON_WIDTH) for r in range(half_initial)] + beginning_full_rounds = [c.arr(f"begin_r{r}", POSEIDON_WIDTH) for r in range(half_pairs)] partial_cols = c.arr("partial", POSEIDON_PARTIAL_ROUNDS) - ending_full_rounds = [c.arr(f"end_r{r}", POSEIDON_WIDTH) for r in range(half_final - 1)] + ending_full_rounds = [c.arr(f"end_r{r}", POSEIDON_WIDTH) for r in range(half_pairs - 1)] outputs_left = c.arr("out_left", POSEIDON_WIDTH // 2) outputs_right = c.arr("out_right", POSEIDON_WIDTH // 2) @@ -995,19 +913,50 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No folder.assert_zero(flag_hardcoded_left * (offset_hardcoded_left - eff_idx_left_first)) folder.assert_zero(not_hcl * (index_a - eff_idx_left_first)) - _eval_poseidon1_16( - folder, - { - "inputs": inputs, - "beginning_full_rounds": beginning_full_rounds, - "partial_rounds": partial_cols, - "ending_full_rounds": ending_full_rounds, - "outputs_left": outputs_left, - "outputs_right": outputs_right, - "flag_half_output": flag_half_output, - "flag_permute": flag_permute, - }, - ) + # --- Poseidon1-16 permutation AIR: each committed `post` row pins the intermediate + # state then re-binds it, capping polynomial degree across the long round sequence. + state = list(inputs) + + # Beginning full rounds, paired up. + for r in range(half_pairs): + state = _full_round(state, POSEIDON_AIR_INITIAL_CONSTANTS[2 * r], POSEIDON_AIR_INITIAL_CONSTANTS[2 * r + 1]) + for i, post in enumerate(beginning_full_rounds[r]): + folder.assert_eq(state[i], post) + state[i] = post + + # Transition into sparse partial-round form. + state = [s + rc for s, rc in zip(state, POSEIDON_AIR_SPARSE_FIRST_RC)] + state = [dot_product(state, row) for row in POSEIDON_AIR_SPARSE_M_I] + + # Partial rounds: one sbox on lane 0, then sparse mat-vec. + for r in range(POSEIDON_PARTIAL_ROUNDS): + folder.assert_eq(state[0].cube(), partial_cols[r]) + state[0] = partial_cols[r] + if r < POSEIDON_PARTIAL_ROUNDS - 1: + state[0] += POSEIDON_AIR_SPARSE_SCALAR_RC[r] + old_s0 = state[0] + state[0] = dot_product(state, POSEIDON_AIR_SPARSE_FIRST_ROW[r]) + for i in range(1, POSEIDON_WIDTH): + state[i] += old_s0 * POSEIDON_AIR_SPARSE_V[r][i - 1] + + # Ending full rounds (all but the last pair) commit intermediate state. + for r in range(half_pairs - 1): + state = _full_round(state, POSEIDON_AIR_FINAL_CONSTANTS[2 * r], POSEIDON_AIR_FINAL_CONSTANTS[2 * r + 1]) + for i, post in enumerate(ending_full_rounds[r]): + folder.assert_eq(state[i], post) + state[i] = post + + # Last full round: compression mode adds `inputs` back (gated by flag_half_output for lanes 4..8); + # permute mode (flag_permute=1) outputs raw state. + last = 2 * (half_pairs - 1) + state = _full_round(state, POSEIDON_AIR_FINAL_CONSTANTS[last], POSEIDON_AIR_FINAL_CONSTANTS[last + 1]) + not_permute = ONE - flag_permute + compression_last4 = not_permute - flag_half_output + for i in range(POSEIDON_WIDTH // 2): + gate = not_permute if i < (DIGEST_ELEMS // 2) else compression_last4 + folder.assert_zero(gate * (state[i] + inputs[i] - outputs_left[i])) + folder.assert_zero(flag_permute * (state[i] - outputs_left[i])) + folder.assert_zero(flag_permute * (state[i + POSEIDON_WIDTH // 2] - outputs_right[i])) EXECUTION_COLUMNS = ( From 6d25d601d49f566a505bb8265b167e3d77f319f8 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 04:51:48 +0400 Subject: [PATCH 60/69] w --- crates/lean_prover/verifier.py | 63 +++++++++++++--------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 2c0d6198d..bb2bcab92 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -107,7 +107,7 @@ def boundary_statements( if self.name != "execution": return [] return [ - SparseStatements.unique_value(stacked_n_vars, offset + (self.col("pc") << n_vars) + idx, EF(pc)) + SparseStatements(stacked_n_vars, [], [(offset + (self.col("pc") << n_vars) + idx, EF(pc))]) for idx, pc in [(0, STARTING_PC), ((1 << n_vars) - 1, ending_pc)] ] @@ -327,18 +327,6 @@ class SparseStatements: def selector_num_variables(self) -> int: return self.total_num_variables - len(self.point) - @staticmethod - def dense(point: list[EF], value: EF) -> "SparseStatements": - return SparseStatements(len(point), point, [(0, value)]) - - @staticmethod - def unique_value(total: int, index: int, value: EF) -> "SparseStatements": - return SparseStatements(total, [], [(index, value)]) - - @staticmethod - def new_next(total: int, point: list[EF], values: list[tuple[int, EF]]) -> "SparseStatements": - return SparseStatements(total, point, values, is_next=True) - def whir_folding_factor_at_round(r: int) -> int: return WHIR_INITIAL_FOLDING_FACTOR if r == 0 else WHIR_SUBSEQUENT_FOLDING_FACTOR @@ -361,7 +349,7 @@ class ParsedCommitment: def oods_constraints(self) -> list[SparseStatements]: return [ - SparseStatements.dense(expand_from_univariate(p, self.num_variables), ev) + SparseStatements(self.num_variables, expand_from_univariate(p, self.num_variables), [(0, ev)]) for p, ev in zip(self.ood_points, self.ood_answers) ] @@ -407,7 +395,8 @@ def verify_stir_challenges( packed = [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] fold = eval_multilinear_evals(packed, folding_randomness) ef_pt = EF(pow(int(gen.value), idx, P)) - constraints.append(SparseStatements.dense(expand_from_univariate(ef_pt, num_variables), fold)) + pt = expand_from_univariate(ef_pt, num_variables) + constraints.append(SparseStatements(num_variables, pt, [(0, fold)])) return constraints @@ -547,7 +536,7 @@ def values_at(d: dict[int, EF], col_base: int) -> list[tuple[int, EF]]: out.extend(table.boundary_statements(stacked_n_vars, offset, n_vars, ending_pc)) for point, eq_values, next_values in committed_statements[table.name]: if next_values: - out.append(SparseStatements.new_next(stacked_n_vars, list(point), values_at(next_values, col_base))) + out.append(SparseStatements(stacked_n_vars, list(point), values_at(next_values, col_base), True)) out.append(SparseStatements(stacked_n_vars, list(point), values_at(eq_values, col_base))) return out @@ -687,29 +676,29 @@ def read_fresh(cols: list[int]) -> None: bus_den_vals[name] = fiat_shamir.next_extension_scalar() num += pref * bus_num_vals[name] den += pref * bus_den_vals[name] - offset_within_table += row_stride + n_sub = 1 elif kind == BusInteraction.BYTECODE: cols = list(range(N_RUNTIME_COLUMNS, N_RUNTIME_COLUMNS + N_INSTRUCTION_COLUMNS)) + [table.col("pc")] read_fresh(cols) evals = [table_values[c] for c in cols] num += pref den += pref * (gamma - finger_print(ds_byte, evals, beta_eq)) - offset_within_table += row_stride + n_sub = 1 elif kind == BusInteraction.MEMORY: - _, idx_ref, vals_ref, n = bus + _, idx_ref, vals_ref, n_sub = bus idx_col, vals_start = table.col(idx_ref), table.col(vals_ref) - # One sub-bus per cell in the group; the prover sends only the - # not-yet-seen columns per row (idx_col is shared across all n rows). - for i in range(n): + # One sub-bus per cell in the group; the prover sends only the not-yet-seen + # columns per row (idx_col is shared across all n_sub rows). + for i in range(n_sub): val_col = vals_start + i read_fresh([idx_col, val_col]) - pref = pref_at(offset_within_table, log_n_rows) + pref = pref_at(offset_within_table + i * row_stride, log_n_rows) fp = finger_print(ds_mem, [table_values[idx_col] + i, table_values[val_col]], beta_eq) num += pref den += pref * (gamma - fp) - offset_within_table += row_stride else: raise ProofError(f"unknown bus kind: {kind}") + offset_within_table += n_sub * row_stride columns_values[name] = table_values @@ -720,14 +709,10 @@ def read_fresh(cols: list[int]) -> None: raise ProofError("logup: denominators value mismatch") return { - "value_memory": value_memory, - "value_memory_acc": value_memory_acc, - "value_bytecode_acc": value_bytecode_acc, - "bus_num": bus_num_vals, - "bus_den": bus_den_vals, - "gkr_point": point_gkr, - "columns_values": columns_values, - } + "value_memory": value_memory, "value_memory_acc": value_memory_acc, + "value_bytecode_acc": value_bytecode_acc, "bus_num": bus_num_vals, "bus_den": bus_den_vals, + "gkr_point": point_gkr, "columns_values": columns_values, + } # fmt: skip class Cols(dict): @@ -772,13 +757,13 @@ def eval_precompile_bus_virtual_columns( def eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: c, n = folder.cur, folder.nxt - pc, fp = c["pc"], c["fp"] - addr_a, addr_b, addr_c = c["addr_a"], c["addr_b"], c["addr_c"] - value_a, value_b, value_c = c["value_a"], c["value_b"], c["value_c"] - operand_a, operand_b, operand_c = c["operand_a"], c["operand_b"], c["operand_c"] - flag_a, flag_b, flag_c = c["flag_a"], c["flag_b"], c["flag_c"] - flag_c_fp, flag_ab_fp = c["flag_c_fp"], c["flag_ab_fp"] - mul, jump, aux, discriminator = c["mul"], c["jump"], c["aux"], c["discriminator"] + # fmt: off + (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, + flag_a, flag_b, flag_c, flag_c_fp, flag_ab_fp, mul, jump, aux, discriminator) = (c[k] for k in ( + "pc", "fp", "addr_a", "addr_b", "addr_c", "value_a", "value_b", "value_c", + "operand_a", "operand_b", "operand_c", "flag_a", "flag_b", "flag_c", "flag_c_fp", + "flag_ab_fp", "mul", "jump", "aux", "discriminator")) + # fmt: on pc_shift, fp_shift = n["pc"], n["fp"] # nu_x = flag·operand + (1 − flag − flag_ab_fp)·value + flag_ab_fp·(fp + operand) From f4da3c661bcc8fc75698eb20bb595a503e3367cb Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:06:09 +0400 Subject: [PATCH 61/69] w --- crates/lean_prover/primitives.py | 9 +++++++++ crates/lean_prover/verifier.py | 31 +++++++++++++++++-------------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/primitives.py index ba687ab30..0e1c8b95f 100644 --- a/crates/lean_prover/primitives.py +++ b/crates/lean_prover/primitives.py @@ -142,6 +142,11 @@ def ef_powers(x: EF, n: int) -> list[EF]: return list(accumulate(repeat(x, n), lambda a, _: a * x, initial=ONE))[:n] +def pack_ef(flat: Sequence[Fp]) -> list[EF]: + """Pack a length-(n·DIM) Fp vector into n EF elements (5 Fp coordinates per EF).""" + return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] + + # 448 raw Poseidon1-KoalaBear width-16 round constants generated by the Grain # LFSR (Poseidon paper §5.3, parameters field_type=1, α=3, n=31, t=16, R_F=8, # R_P=20). Reference: https://github.com/Plonky3/Plonky3/blob/main/poseidon1/generate_constants.py @@ -285,6 +290,10 @@ def next_multiple_of(n: int, k: int) -> int: return (n + k - 1) // k * k +def div_ceil(n: int, k: int) -> int: + return (n + k - 1) // k + + # --------------------------------------------------------------------------- # Poseidon2-16 sparse optimization for partial rounds (see Appendix B of https://eprint.iacr.org/2019/458.pdf) # --------------------------------------------------------------------------- diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index bb2bcab92..1785bcb67 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -106,8 +106,9 @@ def boundary_statements( """Static row-pinning constraints. Only the execution table pins the PC column.""" if self.name != "execution": return [] + pc_col_offset = offset + (self.col("pc") << n_vars) return [ - SparseStatements(stacked_n_vars, [], [(offset + (self.col("pc") << n_vars) + idx, EF(pc))]) + SparseStatements(stacked_n_vars, [], [(pc_col_offset + idx, EF(pc))]) for idx, pc in [(0, STARTING_PC), ((1 << n_vars) - 1, ending_pc)] ] @@ -154,15 +155,15 @@ def _sample_many(self, n: int) -> list[Fp]: return out def sample_many_ef(self, n: int) -> list[EF]: - flat = self._sample_many((n * EF.DIMENSION + SPONGE_RATE - 1) // SPONGE_RATE)[: n * EF.DIMENSION] - return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] + flat = self._sample_many(div_ceil(n * EF.DIMENSION, SPONGE_RATE))[: n * EF.DIMENSION] + return pack_ef(flat) def sample_ef(self) -> EF: return self.sample_many_ef(1)[0] def sample_in_range(self, bits: int, n_samples: int) -> list[int]: assert bits < 31 - flat = self._sample_many((n_samples + SPONGE_RATE - 1) // SPONGE_RATE)[:n_samples] + flat = self._sample_many(div_ceil(n_samples, SPONGE_RATE))[:n_samples] return [int(x.value) & ((1 << bits) - 1) for x in flat] @@ -204,7 +205,7 @@ def next_base_scalars_vec(self, n: int) -> list[Fp]: def next_extension_scalars_vec(self, n: int) -> list[EF]: flat = self.next_base_scalars_vec(n * EF.DIMENSION) - return [EF(flat[i : i + EF.DIMENSION]) for i in range(0, len(flat), EF.DIMENSION)] + return pack_ef(flat) def next_extension_scalar(self) -> EF: return self.next_extension_scalars_vec(1)[0] @@ -249,6 +250,11 @@ def eq_poly(a: Sequence[EF], b: Sequence[EF]) -> EF: return math.prod(x * y + (ONE - x) * (ONE - y) for x, y in zip(a, b)) +def eq_at_index(point: Sequence[EF], idx: int, n: int) -> EF: + """eq(point, big-endian-bits(idx, n)). Specialization of eq_poly for boolean points.""" + return math.prod(point[j] if (idx >> (n - 1 - j)) & 1 else ONE - point[j] for j in range(n)) + + def dot_product(a: Sequence, b: Sequence): return sum(x * y for x, y in zip(a, b)) @@ -336,7 +342,7 @@ def whir_n_rounds_and_final_sumcheck(num_variables: int) -> tuple[int, int]: nv = num_variables - WHIR_INITIAL_FOLDING_FACTOR if nv < WHIR_MAX_NUM_VARIABLES_TO_SEND_COEFFS: return 0, nv - n = -(-(nv - WHIR_MAX_NUM_VARIABLES_TO_SEND_COEFFS) // WHIR_SUBSEQUENT_FOLDING_FACTOR) + n = div_ceil(nv - WHIR_MAX_NUM_VARIABLES_TO_SEND_COEFFS, WHIR_SUBSEQUENT_FOLDING_FACTOR) return n, nv - n * WHIR_SUBSEQUENT_FOLDING_FACTOR @@ -392,7 +398,7 @@ def verify_stir_challenges( if round_index == 0: packed = leaf else: - packed = [EF(leaf[i : i + EF.DIMENSION]) for i in range(0, len(leaf), EF.DIMENSION)] + packed = pack_ef(leaf) fold = eval_multilinear_evals(packed, folding_randomness) ef_pt = EF(pow(int(gen.value), idx, P)) pt = expand_from_univariate(ef_pt, num_variables) @@ -497,7 +503,7 @@ def step(constraints: list[SparseStatements], n_fold: int, pow_bits: int) -> Non common = next_mle(smt.point, inner_pt) if smt.is_next else eq_poly(smt.point, inner_pt) sel_n = smt.selector_num_variables for v in smt.values: - lagrange = math.prod(pt[j] if (v[0] >> (sel_n - 1 - j)) & 1 else ONE - pt[j] for j in range(sel_n)) + lagrange = eq_at_index(pt, v[0], sel_n) eval_weights += lagrange * common * randomness[i] i += 1 final_value = eval_multilinear_coeffs(final_coeffs, list(reversed(final_sc_point))) @@ -570,7 +576,7 @@ def verify_gkr_quotient(fiat_shamir: FiatShamir, n_vars: int) -> tuple[EF, list[ return quotient, point, claim_num, claim_den -def finger_print(discriminator: Fp, data: Sequence[EF], beta_eq: Sequence[EF]) -> EF: +def finger_print(discriminator: Fp | EF, data: Sequence[EF], beta_eq: Sequence[EF]) -> EF: assert len(beta_eq) > len(data) return dot_product(beta_eq, data) + beta_eq[-1] * discriminator @@ -610,10 +616,7 @@ def verify_generic_logup( def pref_at(offset: int, log_height: int) -> EF: """Lagrange weight for the layout-offset of a section of height 2^log_height.""" n_missing = total_gkr_n_vars - log_height - idx = offset >> log_height - return math.prod( - point_gkr[i] if (idx >> (n_missing - 1 - i)) & 1 else ONE - point_gkr[i] for i in range(n_missing) - ) + return eq_at_index(point_gkr, offset >> log_height, n_missing) num = den = ZERO @@ -752,7 +755,7 @@ def eval_precompile_bus_virtual_columns( data: Sequence[EF], ) -> None: folder.assert_zero(multiplicity) - folder.assert_zero(dot_product(logup_beta_eq, data) + logup_beta_eq[-1] * discriminator) + folder.assert_zero(finger_print(discriminator, data, logup_beta_eq)) def eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: From bd3ccfd59932a06f9cb8909c0acf7845bad23405 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:10:53 +0400 Subject: [PATCH 62/69] wip --- crates/lean_prover/verifier.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/verifier.py index 1785bcb67..0d3c1cb84 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/verifier.py @@ -103,7 +103,6 @@ def eval_air(self, col_evals: Sequence[EF], alpha_powers: Sequence[EF], logup_be def boundary_statements( self, stacked_n_vars: int, offset: int, n_vars: int, ending_pc: int ) -> list["SparseStatements"]: - """Static row-pinning constraints. Only the execution table pins the PC column.""" if self.name != "execution": return [] pc_col_offset = offset + (self.col("pc") << n_vars) @@ -731,8 +730,8 @@ def __init__( self.shift = list(shift) self.alpha_powers = list(alpha_powers) # Shift columns are always the first `n_shift` columns of the table. - self.cur = Cols(zip(columns, self.flat)) - self.nxt = Cols(zip(columns[: len(self.shift)], self.shift)) + self.flat = Cols(zip(columns, self.flat)) + self.next = Cols(zip(columns[: len(self.shift)], self.shift)) self.accumulator: EF = ZERO self.i = 0 @@ -759,14 +758,12 @@ def eval_precompile_bus_virtual_columns( def eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: - c, n = folder.cur, folder.nxt - # fmt: off + c, n = folder.flat, folder.next (pc, fp, addr_a, addr_b, addr_c, value_a, value_b, value_c, operand_a, operand_b, operand_c, flag_a, flag_b, flag_c, flag_c_fp, flag_ab_fp, mul, jump, aux, discriminator) = (c[k] for k in ( "pc", "fp", "addr_a", "addr_b", "addr_c", "value_a", "value_b", "value_c", "operand_a", "operand_b", "operand_c", "flag_a", "flag_b", "flag_c", "flag_c_fp", - "flag_ab_fp", "mul", "jump", "aux", "discriminator")) - # fmt: on + "flag_ab_fp", "mul", "jump", "aux", "discriminator")) # fmt: skip pc_shift, fp_shift = n["pc"], n["fp"] # nu_x = flag·operand + (1 − flag − flag_ab_fp)·value + flag_ab_fp·(fp + operand) @@ -799,8 +796,8 @@ def eval_air_execution(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> Non folder.assert_zero(not_jc * (fp_shift - fp)) -def eval_air_extension_op(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: - c, n = folder.cur, folder.nxt +def eval_air_extension(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: + c, n = folder.flat, folder.next is_be, start, len_col = c["is_be"], c["start"], c["len"] flag_add, flag_mul, flag_poly_eq = c["flag_add"], c["flag_mul"], c["flag_poly_eq"] idx_a, idx_b, idx_res = c["idx_a"], c["idx_b"], c["idx_res"] @@ -866,7 +863,7 @@ def _full_round(state: list[EF], rc1: list[Fp], rc2: list[Fp]) -> list[EF]: def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> None: - c = folder.cur + c = folder.flat half_pairs = POSEIDON_HALF_FULL_ROUNDS // 2 multiplicity = c["multiplicity"] @@ -1001,7 +998,7 @@ def eval_air_poseidon16(folder: ConstraintFolder, logup_beta_eq: list[EF]) -> No n_constraints=35, n_shift=13, max_log_height=21, - air_constraints_fn=eval_air_extension_op, + air_constraints_fn=eval_air_extension, ), Table( name="poseidon", From 088f66c3d781f9b054a8a6a9f41783c2344a56b9 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:14:05 +0400 Subject: [PATCH 63/69] move files --- crates/lean_prover/{ => python-verifier}/primitives.py | 0 crates/lean_prover/{ => python-verifier}/verifier.py | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename crates/lean_prover/{ => python-verifier}/primitives.py (100%) rename crates/lean_prover/{ => python-verifier}/verifier.py (99%) diff --git a/crates/lean_prover/primitives.py b/crates/lean_prover/python-verifier/primitives.py similarity index 100% rename from crates/lean_prover/primitives.py rename to crates/lean_prover/python-verifier/primitives.py diff --git a/crates/lean_prover/verifier.py b/crates/lean_prover/python-verifier/verifier.py similarity index 99% rename from crates/lean_prover/verifier.py rename to crates/lean_prover/python-verifier/verifier.py index 0d3c1cb84..2e15188d5 100644 --- a/crates/lean_prover/verifier.py +++ b/crates/lean_prover/python-verifier/verifier.py @@ -1,10 +1,10 @@ """Pure-Python verifier for leanVM proofs. Setup the test vector (one-time): - cargo test --release -p lean_prover dump_test_vector_for_python_verifier -- --nocapture + cargo test --release --package lean_prover --lib -- test_zkvm::dump_test_vector_for_python_verifier --include-ignored Run: - python3 crates/lean_prover/verifier.py + python3 crates/lean_prover/python-verifier/verifier.py Format: - ruff format --line-length 120 crates/lean_prover + ruff format --line-length 120 crates/lean_prover/python-verifier """ from __future__ import annotations @@ -1150,7 +1150,7 @@ def verify_execution( def main() -> int: - vector_path = Path(__file__).resolve().parents[2] / "target" / "zkvm_test_vectors" / "proof.json" + vector_path = Path(__file__).resolve().parents[3] / "target" / "zkvm_test_vectors" / "proof.json" if not vector_path.exists(): print( f"Test vector not found at {vector_path}. Generate it first with:\n" From 3af0f4ea8be3a0e8ea103848b1aac9a19893efd0 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:22:08 +0400 Subject: [PATCH 64/69] readme --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c0e383e5..47f0e25a4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,14 @@ Minimal hash-based zkVM, targeting recursion and aggregation of hash-based signatures, for a Post-Quantum Ethereum. -Documentation: [PDF](minimal_zkVM.pdf) +

+ + Documentation + + + Python verifier + +

## Proving System From 7d000ac1d5642f76810fadf7e87654ec937b19c1 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:37:55 +0400 Subject: [PATCH 65/69] w --- crates/backend/fiat-shamir/src/lib.rs | 2 +- crates/backend/fiat-shamir/src/transcript.rs | 6 +++--- .../__pycache__/verifier.cpython-312.pyc | Bin 84876 -> 0 bytes crates/lean_prover/python-verifier/verifier.py | 5 +++-- crates/lean_prover/src/test_zkvm.rs | 8 ++++---- crates/lean_prover/tests/check_whir_configs.rs | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) delete mode 100644 crates/lean_prover/__pycache__/verifier.cpython-312.pyc diff --git a/crates/backend/fiat-shamir/src/lib.rs b/crates/backend/fiat-shamir/src/lib.rs index 94f2034b6..3ed5fed62 100644 --- a/crates/backend/fiat-shamir/src/lib.rs +++ b/crates/backend/fiat-shamir/src/lib.rs @@ -17,7 +17,7 @@ mod transcript; pub use transcript::{DIGEST_LEN_FE, MerkleOpening, MerklePath, MerklePaths, Proof, RawProof}; mod merkle_pruning; -pub use merkle_pruning::PrunedMerklePaths; +pub(crate) use merkle_pruning::*; mod verifier; pub use verifier::*; diff --git a/crates/backend/fiat-shamir/src/transcript.rs b/crates/backend/fiat-shamir/src/transcript.rs index 31cecf3f5..612c2d109 100644 --- a/crates/backend/fiat-shamir/src/transcript.rs +++ b/crates/backend/fiat-shamir/src/transcript.rs @@ -27,12 +27,12 @@ pub struct MerklePath { } #[derive(Debug, Clone)] -pub struct MerklePaths(pub Vec>); +pub struct MerklePaths(pub(crate) Vec>); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Proof { - pub transcript: Vec, - pub merkle_paths: Vec>, + pub(crate) transcript: Vec, + pub(crate) merkle_paths: Vec>, } impl Proof { diff --git a/crates/lean_prover/__pycache__/verifier.cpython-312.pyc b/crates/lean_prover/__pycache__/verifier.cpython-312.pyc deleted file mode 100644 index 3f0fb043086fc95e69cefb9c4b2ab08b68805461..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84876 zcmd443wT@CbtZ}@2?8KN@cj}6QW8Z_BqdU!thY>wq~6qnlqFG?A&>{8An~OGP!bt1 zVO^&NB_#%9C4yqLp<>;ds+9?yI%9g8naYlPtMukeaRfS~5%rGSrqg<+Ggr3UY3zIN z%=~Mg2M$QcNjshIn`m!Q?MP+ulwMO` zT1Q%+xx>tUr}kRsWw2ObM-gXZ zwH9}jbS&XAJ4$t$T^i2*f`-fTEPWmCqpQQgQnQho^Ez6^(#r6jbdObjPe(b|%jLdc z=vc<}ae25e=lZ#P+*h!&1t?pnl&$a>S!xkdi{;eU#fmi@E2XC;c)DcK(@Ivd6wj7= zDwGyFsn__c;v60`=b*9inB}KR8&`1yTp8L_je9xnHQXS#4ENeJP4a)-d2TtLujY7e z1@3D&A6J2U9p~p(;$F`Uah15Qy~OQD{x;T!1L#Am+=q^KF2o(gv!lt%aueJkJbQ-w26q_uW89IB<5=w{xVDZC zuD#o{nzr zo7}Ugt%v&-cMA72+;#3b+`V{<3-_~}nLEunIQLCGcZBOioMK%Z2W&<$&rN;DIj$Q} z+Cxy9tZ(WxF3g=lt-ZhqUflb*8{Apk`#l5PxpS2o4PU~r=?is90UCx$MFMgZDyahr{C?z`&k*U&(Y)Q_wa7NhjVleJN#!njw3^; z*XQLt4maN7L-lwz)X$-o(|$MKbx~_+y2L49!@9cD+rL^G{4Sb& z9~(hwC|1iLSvxpvHn$A$eQy84!u(KoH$f~Op$olU%yX}&zsG;ZQCGjl@oVb*h*peK z8?(6k`v?3CR(-AboyNKS?yg?9&xaqTHqZGXPk)!ksf+1a24mVoh@>5AJkofe%~iJ^ zQF%0-x{rv|`3O{w<|7jV-a_^3c`GGkQUb-CChzL5KTGvKTKB^P-bZisF7Y9}#8pk%3C-_;iXFQCzH2thWFOBX%{U)SmSX^midy zxg`2V`r*I_jz2dnAzp(8{md|DQ=?Vd2scS~)YqbdRMYgR>xJ z^VmQ)S`o8z=?Js9420Quiw*C~8P0U(#B|LqL#xrW!$aPFzqbpx(eLT^5pF!)GI;X% zDaUF@k1IJ$lzu~BJo~{K1X}cI8bV<}8>dv9oBBch=(5-Nky9}vK`AWo8y11EC zC2Qt>M1FlURgKrljYZ@2)OaE`&V~9`~Ji?kh)- z(D<82Cbh2|4ey$C-`FQ)t&G|#Bc@8RBMPvfjM9-!_@VAdK+~fFo5f?kpxN@cD=oeQ zz`f+7S4ohP*3A#}xjM0Bjaboe<<~A`@s%QA%@5Uumfg$>H@=b^PMs{fVG**HMQzI? zrsZPO6^KQCLXm6^K$avnUF%o!$x_h%LcT`-YT~boTct1eV1P5Es1&@)s5-{x`3WqB=MH|QT?a_L2aPM zK#{Sui<&#~6cG5_F~-bZA0s!g^@uBgtp!@pG5v^l@Q#6ZP+U6@)4F5YPM-n4XxkOn zb*jtd<~S^CGh5ayz7IJbpF@CEZOIL}Z)VL}N*-qChWytrOk4=NCx$2MCPpK9^^^YB zF5J2><(}%C9Kp|M_S%Sb?L(XWcH30W_w(-LP21nfej|H!MZ;{?dSOMwjrQ>2NiAmn z{L2lI6%7xv)<HjS`DoFvpM1Us!^GE8 zz;??;lj$4kR3?+f4^iIr6g-8XD=AKB$CE^ya8_AZcqCl_U9BV(!4ezQ2DBDxaVisq zsFsSvK4<}|+E&mb3Opk2KwbP=1PPl9fUw`wqqLjFk5Z-;2xx1uhyCBV@a+rHtcn{K z!eybxZ`6k`2w4?T+scS(Gp{fu01uH_40GK7O)y7Ae2phx*|c`00YJ^8 zI;>eOVRj5c8XE-FicMZaa@;^`UA1#4Ln&{2qlP%n;q>wz(xf?6jaC?+@C*oGG$mjN z7y~JUR~XLI1&r*j52Vmt-j(9Hi1WN!YS3J*HE2}z8aN}DB1?YoUMK2KMX6M(Wz~R2KR=bJb7f+))*{@Vp4&;revA3?m8{?SUz-c&rk!rt`fJz6R)Ff{G9}ymA`#&Ra zdD@Xa`tP_t!jx;AMxLm!g&5JuyZd`Q&NLAaiS}U7$ge{zW&mr&#kAg-_H0ahE@mJy z$VYD>bjdCSXZ8*CD&WfE9mwYU6@mpgbCP?l`@7xIyqa6xllIX0aNWy8lifmIO*FeU zVy&IGWxsS`{6gsbb>D<9SQpej%*+bay=)9#4BN-Q6f_VCb^cog!N!NVMd7+{9S>>e zOUozgCePn$xN&Z(amszCX}WIu@Ehyy>=R0#3K>F&!}f`z->`hF*W{Hx(rdDcK26bN z<>S3&;UnK#8f=`;&JSxRo(eY27nO$3PuATSy0L1?AQV*xn?m-`;fdVweNTFFdEcW{ zO;*{bCJoxSJj92!p~@?Ph^hFqPjWT+wTy7TZQZ#vaS0NFVoUHiA_+Jq$>x1Y%NJ@j zSUOZ{!G)!hB$Y_tHVsd7D7YOq|NpZP6=P`Y0Y|EqQS<2!-K***rTz2zF z)aH0#tDkIt?U`H8Or8I}?~d=*sfek5F=0i(#yW__nAky0G9)UmUb)erxzD(LXBYu~ zN6WxcO-zC8rpIWkNOp@q`tT|rF^bgO>`>G7mWdWN6^omvpum3CJdOw+#1>vE+ztwK zR7DXcA9uMMKz)TG{(nWo>3&x)kt(`TV1Ll)Bl_l+%9z%PxdA=`K?0sAX1w6;9rA!^ zcOZ$cLXa>!=rZpa`DBfj{eZ;i>vj@3r+0GCusZUAR`UsTRtOXGPSRxd!voxOzE?G(z(DpODC7# zSQ1Jhq_KQrGmwTgJybTnE7&x(p&#kg3DR7l& zCuC6lpBq*-x2#0tY05>!e3D|%B5>yNITX*OfEctGXaa7a3o?Z?m&?~X;P+9qY0rV? z*0wzdTlsRzV}(!(Ja(T4l1YvyVu5Faw2ER46l|n`_7`Gcc}CsxyX2n?ar1j9%K-}7 zC^$huA{QnOKg|Kv>H8f7FK9k6Z2T}YZ*0$erhRPBV}s7P^pQ!ElJ$vBYh3zRkJuxM zJxY<%oJyKgNn4?$t@tEGZ`|;SnFU&7C9-7ZkL~`%U^SLJDv{z^BG{p+pFfv8-((PX-~9#%Zz8Xv}xATER;4)a3Oct9&8M0uQW$Wn;ux2 zBZlVBqLya1^SxzjG^ZpGpUNy8!n@S75GVEGBdJPL$Uho48P9Sm)KjF|@u)*FL$BB8 zk6D}c>}qancQx;CKF~(8oL)~ikS*`+Ipa@2u-Qq5h^Ou0tN8QL3MLw68PdgUd?-3B9=oD!=c6d4A7v6Q1>q+wUs`^VQvItBZ{r~ER{$rp3{KZ z*mVxbK@x2d=3`f!FjXUD3yzE91fx7|*vTwPIkZf?RO7DjIU|UP=$r#uRuQ`(4MCnO zZx28u>L>XqyNXm~azGQ%srrc36wm?HBVS@_yp`%19@So5Z-7_=wG+~37tJ(n3e-sc zX$nYy#pFg#lel6GM;hCkd5)3^7WlIiG$M$Zb~YYr+_|Uy1W&9y7LI|$)qD?SXhx8@ zZp@6b@b!AJN@%kfryMsC5G-T_{Z}?#^9z>Z@c9Q82eJ3Bo_KS`gB4Fj3{OQYPd&6& z-p!ff-fW&;KmF8HU(~rJYTY{4{4hQ1+S-u+<&C!uLiX~>^FnszRQXir&(j&9TRnAj zdgGhBBQ;y!Gu&&xU-nb(CzX+Bjzv073eTLJb#)0xyCTh8^l6V!0mmx}tIV;JCJ5o6t z2gz`UiOVE;a7P-X$R^y)knX18Zh@TGjJtK%0x7R~=NX*u-~_?Lk6dda5$dHH-{1g# zJ6gPMe@)vNcb}K9b(|jP^m+ITp31H>L;dHRryUX=^Epm4Gfq;YMI=$IU z%*JV#&)tWLv2FQ1l_<)4+?-NQF1G2^X&jbyd8lH?X^yo7nQId^1j3Mv+S!4K8WpL< zeThJ-mKt+9{8H#5$dDxoR15ZU!g3<`5n#p4)L2(Huq{!c- zj710lYYwnCuNuP_@8;h4O3-lCJnvWm0kTb;X=!$mQzg)x!Vh(y;*#-rk5rqSf?Cch zOPbZk0uaZh_%AtzB}wRwU0CpRzQV5fo74*#W2L-<$ZePM9wvQnmsT!6h&p%=1>F>| zb$Om*YzbdPOp=$xj0`LiaCoW&S0h=f@s!eTps0^f0EQDn`GzYaH`^cB$|9z+h^_2l zu468@Qpl}@gs$jL(e$1-k4K!_-pje?`Ejw}+#k(7z(&2OH2J7gUl=4kKtw(2Tms7= zqu)5atU{=J7a~-mjpI3KEaNDWsN^`pPk>itlvSIjA{`;RoXIyZTc|^GR@nwny>tbX z+*-}m{RYjj<_=Cr2*{i%42LolD^3)f6b9_nKA8ef^>w*>-Mo+Ai9~)F!2%lGD$R+j z&)q*flD~Me*n{7rf`qX!W5n^_To&$mrTVVn=k^+~{y7EV{7L)d;k!AvjzqdBXC z>0Kwa&2pWl!kXZNqKfRKu$yf}>8ztrx8I^fdFO{p0(?x$j$-ehj9)cJY3V-ASCt z-H2Q#o?F=+ksw|1flWT}G{EvyLv=vI2T%~~3JsBn85oc+Km@TA5lxNQRl^}B-9|xQ z0RnJ&ChJSqajV?RoBoOC-Zx371L~$K9T6Gar(uIFy2j+ zR-j6GtcjDuY7&DTLJid-8!$Ok9SHM@N(%iz`F@9tiyfUF96Wm*44XqMg0Lp7ZgHK* zm;J&~va0ca0_dn8yhiUQQ7}df!lbWU#yQrZh~rSybSU_h`J$y*%{Ea+uyBS>6BC2KF{vyShOofnuh~OJ3Moj1(QMw|6 ziJ`mK?Pu710i>8E;tYB$Be^O(7Ek^m>hgUF0b7}KHiuwyOqSj5e69CZuTWn9X1h?n zai&oy-}>Hip{ylp+ZEZ}0V=rj&wGE;E9~uv*iJ(JyJ72$`@QUY?LRL3>5f0&|C9Z* z8%{)R9m0ka({<(xxbe2`HR~;_P*NSqu6aMRHnQP_VC#sOIzD?~>+sQz^-gWY zj(kIMg0KqDp9EELoX_bBkq_&OxB{n^zlz&J@-CJlc0B>Bi%D-IyL9T#aMbu$!sny5 zvdJ<*b>gegm&i=5WxEJxD;EbHV((M{t^@c2v>>)HNTBBc z=o!=}5J^vQC3J-SLpy>NifP9iG>qWr2@?@r|yWaW{Vp{+=Np28%m6- z`8aB*ajW1nEg0QXFREWY(nCvNPJq$wgXmE z7b;OMX{l%*QzPRlP*9lHdY}Zg1*x1KF$2Yhjn0%<`T-AruGe#L(9;hJgr`A_r9rK* z+eI>g&okI)$b)_g2VrjinN|$`0-3B0$^vl}I0#f1bo_TwFHga{`129Q{i^0eTYl7# zKW{FWH57>bp$z|%{V_6SG_fCMKURUL)j5*Q`jF7sQ!!f~>%40~?7gp*zl9boZ2N6W zqu@UN9i;U$xE0}VF(Q>8``m4VqIF4j&xpQ#x z{8eA50g4G5u5X*zHfJlpbMP}pP5*ZJ3hl_UJ^eTj_NvBYlcRs2zlKnZgWdzY1Cbdc znuzfdWio70X5(i}55bDa>(NFHYQsU97>{Sfv5%We5X(F*o)TGc#H@5>FsPN3%G8%qGb&aEDaB{N+L_@ zqFMD3YyEs?&UM>_ZPM_X<(6fN`~JB*=VqFP+Qvxc4pad#hvl**G$fcxzGi`_J({uQ zZu4~6^wGNq5uLScVO8yjX6=kvcOv(ksZcN#hF8p*9P_rEpmj0JNMOWP`vydwMBJ;G zEM-l~z{5m2n^73_7^8j&4+o14qS z7hkD)U)EcdmLT%5j)#k?=4T1`dkVEScMz^-;V3s+UN9X`-Bh%{f(exAXhG zcY1}YO;OwCeseNFnWte|&G;3eP zx{vmW^vmgCyI?B%S~`hQM#o3P?cX_dz2JitiEYdK_$9n?z8D>L!A^E+C3BXNyb=Z~X2{lDqP-vQL~Uw>YU>^7 zsdsgGyuGd=pBGf;pCJQTnn?Pf(C-Qz{zgi$E@D^%^9B2jqjM#-LP>43WDQalqfA;d ztl7YJa_CO%Z2v)X*HUtVG6_wv2DNvMt;wSe8T#OBRoaBF!bRrHS-e z*HW{Me=DG36~pzLV?~ERnL6s1$mXkyFxZEyqzTE@)1;#2n?6VEoMpE_5~8Z4TGojpW)|`DT`*5-aV%IG(%&u z1h+<03&R}`QdfdAduYzN)*h~j=2k|{&as`p$aVx%=htqSse5DNJ?#&+&1RHME}OhK zo3VO!S4ZSzXN2pC?m83H&znfa)&gxOqb8^9(-e(0lR?6|N&PDu1vnf&5LYzUubQWL|3^;IARvHRPms%r&j^3EYvi@q3c)aS=4y{1O(v)AL}zlt>e=M?-7f`tbid5}QBrXT7=Fen`A_p)+O{d|bLStPY!B51 zv&VKq-z>EC=jmlZ?YznIl6l-5T6@(tpJ5N}9QTgvF`yacIA^um#`cPX|0#Q~GY&+eIY5<5V^D3(&UM{37{H&b4sGDte9 zS`QW^Ex6=WGn%Syh1_C{O*tDIGDQ>npU`LacKO{jA_;Azr!)qmsaKa%zem-AU}aR) z!eRNnR0{9LY>lP`Oo22uP9`z|45S8_@f&fQBU=y9;R?B$aPulJ0HDiG4?DIxE@7?J zopw}qdV9z{ir0a5$M2v#+6z7)2@TutSRihCCXuIwZ-BqW z(*|0o%?O^jP|entD#k0WR00iSCv3cuGS&z~ose;C5259{uOBC@Y@ag~;8-JE2+OmQ zDf?92ot$ayblDq*JH<2l8TUQy+uYmcd%pXJ@1Ork`#U4TQ%3~n5uvc{eN#J_q>Q`| zvvNbZuw`@KSTV75Y%i-hbbg{CTpos6F(Li}$vjRdaEkVELcyBpoik-Kjc+f1W3N!K zJ!l5wmQJ;m|I%cBDRn&cN*V|*rHwFgNeh=vSSR(9?kVjp?v`1|Ts7sM*52WS%=+n$ zd)j-A?-+%K79q1mu(phA#fDij#+n|c+hG%P?3zE^`0~Zs^inhj)>-cHOXCL0wDdy| z)&B7fV+R*+KPHSZ+rd9YBsl_tfG>_b6qzki!m3S3+0)}xE|3mDUAz+LfK32keSBeo z_Kw%D2m#cp@QSVk);LiR0Rvm?xT}`Fl**8f;`adB5yb|Oexrt~TWGP#3MG&h7qyEO<^MDC?xE%J0wUt_ zfK5r*ezR+G#VcoTEPY_9j2J2-mdb}_vNl;obV_c>^K}=jJvTpn&;H)d`}!a6zVDvh zd?=c6SlE1M+8y+T>c%gGvqgUMM$u%~RN1X=p|E;dD-_g0Vi(zbD5w`Q4ii`{`H*(b z@%3W|KKn$ku^rKhNcEx038ldY3uS*(4~qP8|{@YY-PH$)cyt6U4_Ajn`Dr2 z37``&iH$1dt9k~b@Cy_FHJXAA6`KLU=z?;o<4ZZ&s#$k7$+;fvse$B09}@Bwl2`}a zY1rjv;;kl`1;SXRNxDXOj;#ar1k$K=R8L|Z7?Er?B5B`+_mI13avh_F;8L7gnqM%* z-w~g^@ul(>#idZ+xKu7RzER7YX_F?TJB8KpEZ#b59<_{GN7F|${<-4?%Wtgauj)8$ zH&YT*e(M+fPFe8Vvf#H>{M~J2?Xrz#j@n1FMzhJ!&S;KK6UYhJ0%m0#a-|fDoWg!* z#&ggnoj9hWc~Tkcf-*`SREFiTe_=iOQa$Ml>QU-d)w8(ntS_v)K&m@qLEXx`RCOM3URWG!f8 zc49pX%9OA&+3_-?OMpR=uIB~tn@b}cC}QD~#TWu)t_bfkeCM&8#S|vCLCGmYe!f&Q zR%W0?s{hHQV?)la3Ce8F3!^%E#$`AZhJyvLY`NkY1-MiAg6CTAC60K!JVQ>7A~ zB5_TrKUd12@sqk2y8@*$rjhT!SOpAYDNhtVNGRY+`+S-bE5(kVF|-+#K_DAJjXIUxT-B#u-NN#N>(LKQnz zcKcnP?j%|g$B0~@vHm-zBw?$*qn|8?j^&Q)yC-&EPrsuV^&_5NTBe0xFeB$5hDs9A zGNSz-aY{%xxG#Bq2_2_>oOK*h3AF+VcGOX=tTrNb&63zPnp!9^QP=pL2x7WHf1>Wv zKc$16)IrFZG?E%f=^mkW_mn-%PnP}e5TcU)hb>FXKTU6>g_)qSL{kIE)IKgkgk;8| zrxibbi5Knikl7Lh;b?+5XG}~LZah;#q4Ki$&a|c1-Pg&vw~eg$;^~pHbyMT5s9;PZ zvKqJRzFo{ncBeIFWbi6-WJ~FFB?w}vZl9ay-NPhq^sisX+QST$yUvNaDU5kTlf^R{ zrvpb4q;?=ukpGFQ(DlP-;GifB!yIZvmn2$_|9kq;%(C!nVmiJKfA#oVi@$X-9fvdy zX&llxq;czF+CI!4k_VAIh~z;e44@`xNn?`YOm>nH zyB8HlXrRIb6o1;FI>wN<}wuAcS?XJ%fpA^o4`AE9_(hCIe2(H_Zq~f_+c^!>&J-+MQxSimGfpx&>e#7 zpo!( z69h~U9$K;=YxI_sv7L|2nx%WScRg?JoYDQ^t{L|m`|f4kJ2G3ldk$c3Dhld?J3_k9 zj<9YX)^~NGb?~jQZhSbrEbI<*llCxo!%3cAB&pRk_xL{iSW^1!^OI#^|BYjl?my5@ z)lC_0^-P`rfqlAhI_J*C8STF*n>jqQ{O#0x_IK;h)CuY&<=r|VvwA+s)7x$#b7isztk_h!V6T2tD`eI^G0PX% z{K%35$ji`VWk0fOZ26BhX*Tm^{i7uWexI(c2XQxh6coQ6M4ZyyND)HJp)lgR@K>0=5!oDqXFVsU<8$(*;rMc+8tb&&D_%LYSzNTbpmlX+9dshr6r)5dA{bmw$h zG;Pz^4l&&@Wn7RxZ%GHjI2x{-I6jfCKFdWDl#3_~T=3D4FwyjY{I7bzXOn1`Ga$x< zyj%)msVvO|oBK5UGSe?l3heAHD!;3#9uq9?t*VqXq-25ri+f`?BPAO?SK}#`VH>RR zZEb@v!u2q<_&!Y1$iU8Cx3|whr?LqXDGQCrQ}pRdI6GzTsH+_$Uh^n)2!M(3p(8rzo2?XVEAK9Y0_@p4XJ20} z*fvH?8y8DL71Xm1CDo`$-9@=Oqy`dz<&x*R=oAUw3UhrZ>j1xxwc^d#7B|HbeIxdbXai&3K!Gj8TLrY!d9!Few!Lk^fz#a4fX?*om0)z z4O819Oye{V=W(Y%T}r6&>l z+`e+bJ61w{oVAWhii68yiRJhz>W4aK{0-D2zJxkRZxC}@7Z^v0W@F!_=QJ!a6I2Vp zwfTp62NgU`59rXEub`lk0^+$?-$cyM0G5Iv{(Kkk@T;1S4O-(W99tGGi5g1gE!n?) zaNg>O8XWUEg=4#d?NLMFhsJdBIko9Ad`@MMtwF8ZN*T1$z{5!KULC3TCExJ60Z zDdr?2wVjgH88svU57B1F@X&HZBz-LKnS4r|DZ?I5x!9#F#8~mM-FuF>_O$M4-veLy zEeH2E?P=ZRYH8fre(*@Fls#-ax}&Z6@X_Yh_65%YWW@id6}C#44|9?fkOD`yn?5Mu z0;kOn!0A7~8qI!{#*d^H&||7vHFf?EDrPMe5kvdz`gVjO=p&1A5m>XHkuC@IEZ!r+ zm*nX*-mi1$ArDarp&jU=MmtL^q)c4lc0cQ(h`BdrYW7dt#0QjmKTB zM-RB3X*{y0amW7VHdp&WS6g#ylWXU}=9ZQ=z82ZQrT+r;gJHZNY9kT;0RbT%+d0_i zz%rCZ!v}Lcs84wNu`9vyi)l3SYf;B*`19>YIgC(R-fZDIA#EKPuhg_zYxRTFYH|Qq z9vli<=dBsD_Bz2@7ctjk^aOKz#B>xq6%MjGLv^$H4MKKFCaOxPz0RNgf(A zs5l-FfwLqLd=iEzpA8Y|2yp{_Zdl5a+8-c@e+r}WeHxiH;>cJE!kYxkipa6!k!Mdm zICg5*x$nMPa2|{tX%n1ng2fXtcp_b#7-3-LEM(`lUX=feSEtc_*}M0@)t~X+{P{0T z-nvJXX77!Yd!O}=b-d;DUcS(vy8FOd@6B&EUG#qbf4+3V`vY#{8t;M8 zGuyoGjNfnejvQFs@4Yd*InUdCd2g@x#;>;Jc)$1Xz+vwzAGhxI{~!sFZf%BH-@zko35!_)`3jU!WBWLu2beckd8d_$ z2A$~FgS-)7ZBC%HdTnMw+0EtVyj?EoGaUmwzX@6YEsd&(-5*-gXET;hwks@(xE>X} zff`Ag!@%aRSmQwBYv`DY9arH~P^0=M`HO(iSMR5zRCdWtPv-h1E~-^>B@qt4zMv+Y z`U9=7!zKAxi5-D_6RE35ECj>>$lzhCHVZqI9F zhqh2uH5`I&SPJHEq(Vvy-Y+FY%s~riH=hl%HXRYIi zNB&?6qlPy*)ZZ5Q&Auq&Op6)lQxR~&ht~CaJNd1c&X}=x0RE`RuVS~Cef%MfDM8Y= zA7XZf)ps6jZP~M{&2^};eRnL4SzP&{8W}U~^9-{ONq{TbhN>7oym{5s zeo##t_uQ|!zx0ttdqUf)#VxHB`z2&Sk2HE?l5ZOjw!x)+ybWO@leE}An=>LUg)vAJ z6Imtt2nAe~4W~IRvCKon+i*LXZXg=he6fDWUdCsHV89vFae9vx--Vitv3uk2O!tmM0U5KY^i;7A#H+;*K9zOR8yb{1`V4NOqnZSNaESsX0r+ z)&-Z$_Qx$Ual%lC69(pMRXST>@r#laIF-4LRJ zbe;=|WNIcx%qM>91_sX#Gc6ULA>RR5`7lU_1sEfcI|v*jCR>@!VsTUv!Jtk;858TifRgw|_z< z09tA0&meKkG_$2}dtGBmitJh8^IXPCE}(NyVL9bcPl@zlPE`5tQtTiN#sXTC6c%_2 z{+!xREFdiPv-6^cym@ohZ@12ihVS-55SX7D3`Ws04J(i!rYA^enEE+Hu#F%TuVl)l zGB!<6NKrJcfLn>yB0WatR9*ZW03+S#^|zHu6PrHV8SWk+g5&lN~Yfiksr0gG1veZT;iyK+pCQ-Q#Xqe*jJN{uuH(VEZ zxJ`IP{7q%@o1k0Qh}QG}jsl`j_&=fG>lDN&AV6_im~*~ZirBhXNKM#Gmh5$UdflQ{nu= zF><9DTL1M^fJ%Gb94K;o-E;+1$*ZFFt#I3H#W}5X zowHEN<;~38s5P2(Fk(Fj74tchLohie;a{K%p5(|^+?7_;ZAvGXYoWt%8D=k=te7f` z+N;M?9_4B>iXY`^toBevaBDM>??q z9Y7!40~B_Q$(4yD>jbTQd>u$hl>UNydxGw)^dT6gq!tHgvQD_V3BYSe2KsW_uyjx_ z18z9OBsEP&_(LwiV22^8m6ZB33J8>9`oV!gz*`R`v}1T;7V^hIt~F@4Hukvt`rL5! zIMCNQ5Yzkm{Oo*gu^x3c<$zMDdY|ISzrjBe(D+b0c)HBo>(&YD%NYbXwwIn6f9A^Z z;NG$3U)YLZVPMa>UUaPpfRbAr_Dt@$;hid*YP_>tD6JK8YlAHkK!kI{{EfoNqe3A$ zDf<{s%1QuGnZ*D|qxr=>zqRLuee)K3s6ho0YE+^S8SNkxUeu{s43gRtVw2_cq<}u6)OD;)=?`^b$Y-82zwt8 zmVx@!JwOZTl4pF>rXpHWlaVZev_M9{b~ZySrw);tss1g-Uzd90v&5A!IK@^6AM>Jp zoL%5+tjTMZ`yNo#%tkZ)P}>l%voy>LE8;DShZb{Vgcy zfgm2_?J0R}0C zZ>GrR_Y5!5P>b6h(S5uNfzv?l!rk35ZMUfXVPV@em{1r`$<)-CxMNudp{pY^79-X2 z{^h4nP|8k`eQ~81b?z+8D>L`_6)Sx)ohZYZXQZ~&)HFhA&mHI|3obP@r+)4^Se=>`U41}X_9>24R)fwE8p zeyL>wM!Jhok(V3}c#;n3Xxa^kVA?;;(_}fPvZq{vbL))n-Zo+Tkx08MYCj$6IxpDI zL#N+d67GnaVMj1;NSia12!@jIbF+r(`HbwjjAc+_nA|&UfoGJlUH@Rv!=e~pps1=I z+xH7w9=5jhthw|GA-y6>Y7P(KVzBD9)wfnpZ^D~){OSI8_D7Di&mB1-962#->tH0& z#%R{2h;`FL+lpJg)Aes{d1Fho;_1ka_NeVB{6b`A(KdSX$Zh@Yom2Ynr`<_=wRL*u zkL*#$=ID|w(af#nza@LlR0K_oo7?A#*9gUsIqbS`ygwSPI~8$tMT@!jO`b>T_<{q@ z@vZq0gZRyc1SX2`72_W?7$WAKDd`cPWHHRX^N6iVLXjw|T;jH>X0PCSOryq7$x1z; zF1U}2AHFZv+2*ywsxME`36YGdFYDtQCG)YA@`4HPNW$YUt`%rKl^xaZB=!Ja${3|j zoWaSTy;1G>*&Aea__H@;v%^!TnGZ6Y>x)P9zLVZl@4of3H=grS5q7AHvsoIcgbjiy z*R`}k)FYzk2SWbG?CQj?R){>Pj(ikI3Ul@+E6kxXQC}-To*=3Ou96l;oY<380^%rA z@t#yc3x)RLzhioV=2oHnz>Ve5aykr+q4VPnp~kOmLegiC4aoC+ZoZZgovkeiy`yKW zceRM9yhvdJL?u9u33h2ilzNtdo@vQT6i!NsdTC|S0`S^z;s+x`V`*f5DXOc{=qV1( zeuy6<|CBB|sf$E-C=Q~8s?C#!gTvuQSoJi$4>ze*n0CTu$u+pH36-p(S_D zQY~0ucJaUh`+o=v>*lR_bJh~US`t1iSeJsnPBjbW*4xV_&(2k>7b@1znCCX^7B=j@ zZ@-@v*|2}M{6N&)I-A-`7QvaMkdYhBTn(Ej=ICjvYOZSYgR0FyXHm>F5Uqz}gd-85mOL5->W3K_&h8JQX*ci5vK zt{|*P0TunXfG`vC(|*gYxH+*Z&6Erk$TiZ@533cHb;&4cJ})j&M~);&C7>8f^ykLu z>R*=^+=c0Ej@<6}ebj<_q;OIRkrVmbflcHfxZsbFJ5@Dv%9#Ztrgb6h)l&NCFx83Up&C@>%uvba&LRO8 zh#*qf2iw`vtV-L2PRW^VA+rP)m-)|7L^NM)q}nnWH6kv4MlgNKs+4F`kSO@vgiO%j z+r@nl!R(|OoY&V`4{n?YC;&Th(L^J&3I|o_^vB^fELoygPtqJ(6(~cPj8-cZWl}| zf@wnbiqPIk!@MOYbpGS@SpUj~km+F|)I)x+B-A)x43u_b z-6TAG+}bm(y>&n+uAg=bMNfrx&M#g0#?GnwDfb^V{HXJfyWj5qqciuj?l=A7n zEIl+qMYt9lwv*7Y8 z*iwFDz{=Z^7d0)sVBRCOs$UB=C^26We3_U}RyR^(&vBBwnUnVct!MZQcN0|}FpU>+ z%)st-BC&CeX0TaE|H4_wAl6zvMx2>a`)wh5J)g!CS17Y7$8^$F` ze>ro&Mh3ic-H_cV(izrMW5A|riBc!#)XI9O<3!I)7LBNswl3DsD z9N>O5H;{`qVf6wk%@g{oDcEDuVHTV>nmcOtD>9?;!a$zPOpNCHSIYGVGW||Du96oi z@08!Ilr4bKV1A&0_BYAbSiHZPa&8=xOX-x{WkULK&K5TOoWgy=e!;J}yMacu5H#p5A+mDkC;`e#7Pl}mIHBUV9;7?J4 z$=4;GEywKQ4y@V_W9ed&Bxs0BxmCUe#&C(k0)x-*85}8ONBSZ)9%qK=(IMJXkD;AJ zgo}p~S2G@#tcD)*g_$o!OZLG7D(v{1=S_vuNydt);z(J;tZDtj;worqR?T$ZbKftB z79WAPfuJc=F>lJ9GnK%{YWVp3rV6&XFupKW$W(=xCYhvBrbtjySeK-ObkXO&kN09E zc$)dR&_9+bi&vDQ>7ve+ESt%R|BOp$MoL>qG0V}`E~<4%M*)sLudmPT?>fWgYedxg zVC5b18Ig2&CP}PJB9U~6+H{$MLo~rOg<|X^{k(<%QY@zMOR^-U1*W3a*ctI^D$__$ z+41m?D0Ys5s}!&^G7{%7K1q~u5%7p$M5GW?TNgbteS{Lu5)lk~@F-?y8rmFqUvkmJ zq*7@ZdcTKhH2e*+$4s);S}a!_FS);xG8_LARb+!diwo*%Ebt%9`y}#U5*_|zO&W6u1ERR%foU7a=RPLfLikNmqY`bu5laUYQgq6FraILXj*mW{m z@vLB2HnuyGv24C&cf^o8zr14X038(_8$WjC#8}He8d8iaA88PLoT)LUv2|7@n5v?t z>ZxJDwBey?`Q+iKX(jwDMXW0YQ{|_r24l)2t0up4?BG0{bPV9@>|^`r)N%tkI+#z- z3>A!@3d7iE3Hk z2NhMpW{Kx+pS6}x>pob&iBhTsYxNXT>hI|urx}aP!HiF>n#>B6#&>o=MWZHsm~hG- zwXT95V{jBoKe$J1O9iWA-da3wEew|oTo`ck)k>4D#5xcYOR?z2-bD1 z0(>MlH)>tY3PW?F9Gy)wL+)7Y9Jf7cfCq+08#FmfXUn$EoQan060&!V?Vrysn9E)( zWTTfeCBnMqXm-ok{)cu9SUSdR-jq3S%Ar9#9yQe>A$Sm|i4A7CU|KeB$_Ks*bpp$w zaak#2dmn>b#?jqF+sY~Z)U)r~HZkGl)@atYh;1`shqcr0GQD8uE}W>{ZIU9Ugqw~6ex+uQsBO8#M9peP**N#FVsF>kECC#Aw^%c zkc~+s4XI%hw9?Yr$&1(lkKc_0Q$LS258X3{^yoV(T?4&Cef_>zdgqYO#d&#{dcc3k z$V$-wLuH1ciq_WFo;p?Spj#}pcVOV$(BKn37eQMzPr(gZi==7~1sZeuZ|{N8AM|HL zRhsm5vj)+tF3XxArc>CNZ?Pc=s86Ogf<#L2_96-hRL7!{iIYqyfG}VRi6Wa2XJO!NHHcyB_-^5KkfXDP|B;{&Rpj+c-zUxGURu~KobW{2 zkKSAhrN{gmBd=_~+bquk|Mnv@s2~maN>zHj_2wW7s(61{*(3*Q8~^ zRx-4HqVQo}A>`isZq!EeobyG+;pQ8albeO2)emzDg~IO03HVIp9-Qz*3%jE^-TyVs z2!w1!B_C6Dk2EG@%4eTw5M{b^U)|Q2V(3yIqAH8;E}HXVc7k?-assbX;!UZh`bLg-J7dI6X=*4!9dGDgx=^8;So!I^&4DVSa?tfHTHB zWI(c*mQ3d%{<+h$G*Sz$fMy&{Ma254rI1%5^wY@!yUr@I133jx6$wJXCNDwdE{&x8 z8)y7Sz)c$ijK@WLA=Y^oc|?~mfh>OguWdCgxCIxiI^uJqaDR{oSilu4?oMPbE#8(V zyi1Yed_lb`H!6$kz1nM_aZC|=nM}qkjWA~5a`x5FNxp#NF|)|Wv2z-cgMru6Ue{n( z!jU2!HZyMZ5+dyTJ6L++g`+qHa^%hqv)N-MWtMJi?OKQ1y;5rvy2=aSyitGioPnNL%4e#ZT5_4E#Ol0}QmKB%+}loEeTcxE#y1q=Cb!?v08e|+)n zi+?mcyJ63)ZLhFl&l?6fo~sL0gjQa7JHb1bGez>G`GHt3xaBPT_Y=pCkinmYRZxps1xIY{@ z!3hUFkk3 z-q~~C8#&>MHl2>*Hp6$v!;$y7Ue> zWtBw7g!1BwY1ovo97O>f9hPFVO8Fw8vU0>`xJqt#HOaQsc+X@c47aCo!9EV>WD)^n zMm!yxzLK667u&~cOK=|iJ`=ts73j--W@?m4Hc;Le9T)T~eI@nFWHYXiQFQDxD*1&Aq^pG5sc|n~Xp5a4ZDz617GQVQ zvP|&_#ghtz6(Wx5&3{qSro*7f(|kB3Jk+&M@WI!VxwhJkSPdHaWajH^@a zO!25F<*mJLP3scugor4AGJP(bLz~BN&nQ*Xy#hzq^RP*ELbvI`qWI`&kLT06|gLVSMIbrj2 zLcZ&P#l=KljnS+f5$g_U)w~oK4_x^&M29(f*IOo9Ufvz{PaF)U&F3tE7x31}_Ng7W zPSELhvXsn%a8r1_kXZ(I`-PgqM(v{#jWq{8Q1(m?M=bI0dtkzpi-<3_d;LBl35=tn zXrt2X-GH^uj6+4sW{}Nb$Czq|mQ*|VoVdMhvO^phm-J_=@+n6`N}CiVbr9n)UFgy& zq&Q;u{51}Lh&uy6--w$i_J?prw4;Ka7wJ~S(&!^8T`<#;zIRQ?7%(lVcjB`a@Ko&* zJyk<57{YT8PKy2(o%j{O^KRq@rQLQsa-ws#tt)Ebgto5x?r`qp#;NnSwocax%hm~n z4Kwx!1)Hw!c&TN)JS)eVd;uds9E%{D)@f-DC(=+oLO=jutfvHhjOPF=A+3tb#{k z1H29DqIfTPO;$mjvgU~zWh-4rA1RV)SdN*5|E8;)K5G< zj%R=gC4^7s0I7V93Tr6B4|(F_Og|*VqQfizXdFmTi$SI=8T}&XLIvdbLM)SzU(bY1 z38e^RhIXKc3^ga$oT}<)pUUw$R;_ZJtUFbOFNfgcOrlkvlYNRp%HV{4?*eFIGLKdW z5eP^YV6s@A1XuJS9-qs9#?$Y@b`6n3ELEye1S*^~N~~rcrZi^o!$wdMK8P5P|9=S5 zC@>ME-NxInUE4CT-ivw781$EN*vo||cp=IszHJ0b?rRZfE9f&#q} zbRl8CQq&BR!E>Aw`^{h|LAewryfWl~Q-T1C@dEV05^~SRK3T%3VEt7aXk3{5O6MC2 zwofLMj3X))I?@fTL${y+4!ReL;S|ER8-3KAqN|YP>UI14ED_F9c`ryeHyp-*;N6ab z32+prQ&-U=d^Md@7vXuN#xj^*vCk!a%|x_0#<_!riGP^ub&(Jh_f0xnhK?aV!T{o% z|E~b2JOU>U1w^-QUyh2Oj(xT8fu$;9sESys)YizL>{=(AexB)+NISAg4r?cxC7Wb> z6>O42rR1t`+3n8PdSS${HCoXq*b|T$-l|;6Cb}OVY1 zmoE}db*OqK$RzrOwoDOHEVPIJ4XsepHp3yK?%V}ZP0&b-_rD@KvNEZFO?6r$`Fky+ z8KJ#-OmpwZeeF9(-)Ui+0IJoW^9=L<78T=U=+AIdzvDf6Jq1gtJRRH6jM}eNL<+0s z3fBmQYodkq(X6$?#=WzaeG$XH#RP$(-2Em>V=@r_kJjDUp8xYbW$H z(4PTi^T^Y@6~4Z#bG-5VVQxGYQdhs`XB%Ka*}qVSRv>gq_>K(2Xx2-U`h#~ zRRMsO7Q|g00}spmLHzbZuQQAZ)uYBe16PK|FH|$rC2Cs@j4H_X3Io?6OOm#@kz_Dqv?T z$qqHBnK=vA_B(}vL$lg94}bgk{o}uTa$bA?IkBv1z7V)Hq5PMw38igQ*Mfyp z@sF(SI3ZabOv3*YD9EyPKy@W@4^_81jr|G@2qDA}LD|}sbcDW`@M6=8%`diKW=b{z z$Rvy;w+~1;-y;%Nafc?1rgOsTRu$+uvF}~Ccr8PQAAKf$}gaH?Ew{nOj=P@&y zoU|m+3XkV0tYrA3K*p(NZo|N5UCZoc-vzs>Zg#oGFSV$sUYh9^dFf$vA;(KiekTBi zj`3uv!#89xZ%1?Uu z(>;l*kWA3S6v~<0rs$r{pY<8fv(?l#U$%-~mLd1(5K%Ox83z6MslH5K7W=khG4!Vb zI+=;11=E2qe{ub3@?3ykc(Yt9SZ0Ws1@g%F^I}?SQ^MWUT3;?$fzn*@ldS*>$x}k| zKcjsGQShoyKc27D#Vxlk8WybuXt--cifP6vr538jwNTD8dY3vX=sl z551R3>odSz{$gLz=gZMo%qRm`bNoeQ7|U1aE23UkjhoUktk#O%5UM?NhcLkqRY}Fs zI?FyVIi;0G8O;(l<^^lUyhN@G%RN5|!Ij_pXpdqRD`O`2qLP1_FBd8D*b0Dk)1N!6 z@t5Mi4FBcy_Cxe2B<&6>{FT!Qz6zT1{wh4L_EmTi7-RdlKVv4;;CZ>+%IF<}K=K+} z>#Olq`^+tP=&Qb18I2?R#;awYuU1Y4Do))i_5Ja~!~(@!XM5N>e?3^=b*x|NUyUR9 zGFYGW{yI68mMnzdC#Ls)SGTO7B z(MKCU<9Wl{TC=&aQ5j3V9yBPw(|j9!rM?Q*rwzU`$w%X@u2t2=8o6hqcf1>4m-=&) zzwt9_w%NDISA9b(ov1fs95+#!su63H`yTm@D&w@}GxFW)+oH;MEArh!`L68;)#zD# zTYZ~9{Vh{{&6PPJ*N?_CX3hPTNBQMzfXsgbt3$P~ocB0{u)c}kYM6ZPbdx;y*bGX; zNIKVi@e~*a_#J6J*P7=FL;}yVeObN&jCNUc_M!~w?edj-Qtl=wtbe=<*cUn`_JJ2r zEz`Ym5*y?AJ5m88d2t3{Z0cH>JzWcZsT^HZ?gm_sNpxOksJ)tq>k5dD0QvFX3b-!? zm|;*XhcA;p6o&Wn65lzJ2{!iitGyB*Jmwr1$>z`GQh^Or9Sk`iz&tL@K@ zZbB9+NoiG&$3b~W)My?KNoe%CJN&T(85HWE`1!m@$EQBnz^7j#AqVzLb zDfjel>S@+Y-qlIX_s`xt8p7db`eHEU_T}KUcdhRGXCkp!}1Nl>8TPwzD;+F|N5 zf`VddEINxq6QZ+zDiL}fLW6TAv7~9fJ8bI$*9h9N$3v%PC*~4`;-;{zS=Nosl{!th zrH+(@SVz5MtC*k@1yT$!7e^)H(&{)cO>JTHMrl2YB&&b*!gOxL&O?+niNX>HxQv3i zhnSa%)IY4#B35a+lL(n|KNCfqbvB1*o}r&acP~%ps3AtQ_%Vet`st6z_X8R%$R}zT zcQQ(*=OP}&_V7NSctpKow0CCkh0A#`E)s31IKA`_;folqUj+efz)RR0{;qH1yxVN{ zloQarzr<5+shcm-5vS64m5nDxb@9?7;r$6EfanJM4A5#1ohLD?{}^BTjkKOqPZL-f zKFZenrz|msiv1)@QjtXZ#WLdxSAiJ6)lax4X)Z#s^ze;=UQnjJkE#4ca^9zW;%QLg zF(6(WdM#o=S;Rlq(bV&%P)8=S@Im$gVlMzMf~o@Z;U0`A^MU8grYnNHc1mJZ`hd*` zQfuIqfa}cSawGwHQk;lInjAF#WBeB?#C~~U=#GP~c>j`E%?n_cUnc&wg)d1g`>}|L zkyJ*n5SGo%JC;HuG3vji#>Nd^jX33KY-FyEz!b*#ElJD;iHY1ANo2Z$Xj~#DmOCFJ z6Az{g2rxE+9w&S3QWOgICQ)@w0Je(3OOQtpPK!V2LPQZDWDV5N?}JwG?fB&N56yGVAma(-7ORu5)bCRE2euV zyU<)`TEG`N3ubchCdj52ZwWiLB6n2jR;UVfpWH=NY79b~M%iS?`?l1;ji3*`UC%u#}|%1C!FmU2CjsUj)o6h6?a?>yRJ>{UrKSj zo;s8IYWn1!#UzK|tbVOOSn}$nP`8*`9Zsr&RFf<3jSb&wyw~{6Ewg?1T0~dfw28=w zwE$WqrUjuDsBp=Y6C4VgN@jECEn<16XzHY4?9x6lr3dYzDf_PX8@@Zf+3s*&qiAZR zQQb@fXaTqmgnjf(xtLronxL5U3B-m>;4uY;W-3KfE@hMV#AIJ^0W30?Emm#&?#+jZ z!jazaj&osGpJ?i%%+8Z_V!aW$>B;;RO)XKEH`#>>78YZ|7Zky%tw&q4%L%uko4QEm zWIWTpDcxd8*_7ppgkn_p-_)MP%)G~$O=4!#d`mcU_tf6S!s@A>U@DWVN`GvDfK}f2 zw>~_x(0*#ceOhck^>)L2{d`|2cP?Md-#B+j%x@IhPd##mBkPjx-XD|(#QB<3}R^B|X$t)7)r7Cs>+!nO*u4xE$@A*)t&*23f!R7cV8V2kK1 z6|JTCnNvSy2^@u9r^TFd*=MBgpO~o?jrmiCC&lI8dhU(qpvZTsJ20`Te;nB%h*Z@H zmO8;vw^&k#-pLRxg-h=8*^2kvn`SJ~1-x}Jr{HnU7BOc_IHxIKT1<1#T131&$x!dm zuvpO$umn%xwRk@*H+Um75l-6>&@H+07oFLdcA?BS&wqR5{>XPX%=i7lrf^NGnBOWm z+ZLS#D+#((dms)Yo|YNxn7J5;UxXw@N2ql+?q1iO17d0=LXx3+EeJD0m|HW3rx}|3 zMkr+{*|1!saaT=Q1LI+1_VO{UCZ}QvqAo9g{4hcrm+bF}H5fk%zw}S4Ob>)!xO-GL@Ld3JS_U!-dBFWJB^{gQXuI z0Oi7ZO_z414sKLZ%%g?5A!sWhE!iHB*>Q()As8Ljoi^aN9`}S>hH_IRxm{5V)CC#M z0q=D`GCDU1In8sm=f*A!500M$h@t;FzOz@xp`G>xcdeVi$jr+(S&XFtvp}7F{TJNl z`o_j!)`M}d%Wjr)6G43*50q80oelP9q;Z>RqPls`JwD1T2Vp;V9Uo6(J=XNoIZwXm zV2SUMcIDr~qvpp&PaW%>`cz>GK(ZfFZiXt!)Z~|i!%5X)sO#afK{X*P9eJ> zy`t$=KnS-aWr9Y&0lbYUP^?1z7_q23Ai9r?m{qO!wj%+6NN(J`l-S#fYI>POEAiI#i0KXOMAU+y&ow;m!+RyEUa-Fs4J)XiE2+k^*<7vydfq7XGy>FTq1x$BVeH zz7VQTBRxi_dU&N!q7C3Ig~6tiKe|&2C;v_DdMSYF0L8RFE;@+lc;a~COy4f|80$>$ zVPvKT$)LZZQ*q_!kLL?k0ewdjOwCmggu?S6-%-k%i-z zo1gC$DlbSXOs?8p<{iM7^agO{F=dar>sk5@=l_Y!2lira$!(yEvLB zof{IkG3*_`4goZ(2Hzog4uY1d##g`a$Xp=k3Iub3T5TBcK3zfJeexx|PqGV#q1HS4nIu4zf#EKA0xj%aFi*NC zlmk?=XsMa(Bska>n0yS5hzvr+7dLth>MH7#&cG*`EuVs|)1=$nbdP@B;_2c&6kN3WVfdk#) z<~Wf55XS2rz3RP!9HAwWtsG2F-N6S5mK=MdQhTZDz-a1-Ga3#thk;?w=#~0LF0e*l zSs(}Xj9|iP7o0Xidlm0~nllDc`+?0KUQvx|nmDNJer4auitwI6f+sXXE+8@_(r}W_ z4Gu$F$87PpYwyJIb zs7bKAc=5{@pC;lN6tX)qg2kcCkMXM-GtXjwY|c>|@RWv%bv=C%*;M!t?z6Tw9;}&} zn&%LgfIoidgY;wF@+bODezPAUJ$^_Ib7@>ZbU{ITQ|dFO3WJqC;~87+{OQo}q)gRM z6~)ScaEzU8pE8J0ZPxfR0X&Ee$?}ny51Qzc<76Y1nbul>8I&~_62CfxBA&7e=O8Z& ziS2^dc>PlHq18WcZEA>b8TE+$Vd^%N__% zlYLAwP)Jf$lJ|Vn+mGcHN;)Y|c1ND$XaczokwsqFe?c>Av3WM6p@ z*!z3=>C_0)IrA}jG7#|xp*H5q;CXWTNu&%Vk$axG{m8=unRP7h1A~zy80L-cfFf~y z6Op)!sNj{+@!pI5V++!`939h@7SA70ek0*-(u*dEGxl9Bg~>^s?^ zzB_rdCEu>PUp05_e(lr+>^WfDsARh2HWaFp{Ffte8q~Q^A*f}7sRYCvkk8hJoDwa$ zA%|!w5=_O213Mqx_@IR1Lqqpq8oGMUG=KiV*0-;Sb^Ao~K9W%Tl_Mn1AAV5&_VZ$0 zk7(|h>_FAb*4N^=y7iFuYmi7I>Tjrjw*B6)XsV@(+5#Jcj+w1c+4M%-w=DN8Z=MXh z8%0Yac0eF;wa*NPipA8@*&;EuX3im|HVDp5llz_|+8!rniHT6f`o7MvkdPNL&F-7u z_OL-XbWAvLM%eRQIN|Jq?s+_%9Ge;p#syE^v4R9BCX_Ac%767x4~FCD%q7uUASM(p z=!$p_p1@HMUeNMuxQ3ACgd%^%xfaZ z;nTeClisPLf%fUsVnX_YE`y?ar%ncS)4gIs&Vnv?O#o@tYM;J2bq0ibM7|M74;~H` z-Z?3zl!ytX&;=!jw$0?Aw0CuGexM@JVlR#>nqRod*$F3xbxhwfwu88cm;c3>?c}tw z&PxV2tg2I$A}SN3$X*g7fj9}<570UZKZD5P&fu_87}sJdDKnxlf*?cWCYpmBgP)_M zn7;TSu(p*P`Hn=^i7J(f=S)Qsi^+Thh{pFve$E72*wa;cQ!Ez#jIw=kL;4}TU+*za zX#jdn00h+F(*s(T&>zQiYfwADd!;j>{PltmpM?qW$)K7ze*z#iiPwqHa}Y`+7^3>x zbsW9`*ubI(0W9Ts!;M)$JPVdMVPO2fe);#^ePeDZa<#H^cEj{JY2F(vuko6ZE&n>~ zBp>Ypm<%veKo|p& z`JGbHr_HZ@*$yldFy`etE;Yt0Llp!7zm;yfNIYn?T*#=QRrC8iw4jEHtl`iAfmr)^a$yi`}J4*lz9L02M}OBT~W@R}KiEU@;DXXXo`)==NQ4&ZYR-EX+JcP?eF{w?Rc zc0T`Y-CJ1!-4sNThz(gk?VP#>>7wz#p}@77qrrNn6xt~mOBSE`G$mAjPaR_%t6c+F zmtD8H2KA0bMZZ0&@tT}i2u#qAst!e z)fGclw3y*UzL#rjtAhvxx=+6FUh!4xtJuvv>zqTP64F@*UBCLILnir1(h_$zU2*QQ zz|Rb0;I8xN*)zSz0I|DdoQOz;0fuMw1&c^2s#5IonF-+Vbbkh(u~GmN{aoiDRjhSR z6~8ML?$7pTVYSf!)~&kJ=HT#m*JAq);W+{A$ zgIH(J0_5b+Ln*XMt-1hQ#ydn8G)}p&@aO=4$GkCXd=A;?&&S!z%C%<=X`@h3@R}53 zhgi)ERI7ObPMig7QFK#XmDRj}YVR*p<)N5ST$4klltX?r2Y->jKm`UbRDtJ<*U$58 zCCa07-8wxCdt$tfh0@)|@2sDTqJH-6O`uw+`l=clqG^d^7y3c!Mf7&@=P0pQDsgow z>~t>Yg%K%zRw+tv7qZgi{G>Z;fz&Eid%0y8Szm$2$ROo$zN{;NwNhM+PccNCO3+i8 zlHPFE>-?>0fXivDMgTbVRQ)GDTaMb&fp&+56e zpC{*JRnAOQGd5@JRGfG_&~vbQt@GL8Ciq-%jlNX4iM}*wnWHxkXfHwQM6zQP4P@8o=5+cco&OXY6YPB}MXXcdw8W8eYmCu^`JR?2nZe#03 zZ0&Dhx2(m#zd$j-_6%RSdB2yI-e0iIHZ8Khz-k4PE9A?8v;N*|JDz-&tYB0^EGjiC z52J+A1T~dyQ?N*IjbgwI{{{SN5l4o_EsK&!mF*)4uhIQfizG+61K+O|s{Et|z@nC! zmN();_21MlI8^$!RU10nx&#QUAexB1xAWxD-UA1FyLwu8^>!cWZR?CA$AqkP%Aq}-`+E`XX!pUM?#{zex&~)O(e{#UugWV{lR0Q0z#t%| z19;t}{|(wKr7%rB0^YL*wxXITi{tBq30%O3+x8s0akJD+f*ukomF3?% z>Ltnhh*f@w#$6v7=geFx=rai=N(4?ZIZibsr-Amqsc=9s0m{nuih0@$9S=SG9-e=A z?O~^=gYlKAO@Y$jme9#r-~6!$or_Kv077wrUBN@JXnJg_6!Z#*GjJF|TPO&MB2I>` z&Dv+%LN}*6rs5{s7L8W2gNtm>^$F)aLcd2iHz4ZH0y%dfXb72S&&=08+#(!1%Z?6# zf~ld&U5fm^f|+wpw!fcPGTBDew9dpWIEur`B_j6GCi~3c1*pYw5wMGJTou4^se=IA zsvv?$+8z5sHl&E_MN>Ut`!;~oZ?@00Eu>Y4!TT}Qfc~FYi_-`YUREHy9_vy?VB3t) zZk-y`htg+vi}?+6J~6*tOz(Km|FBbldY03D!rt>x8JB4KIG*e5wvoo0-JuI}mT=NG zpxTU9ZqY7S|E@6yxmyXzlT#3Czq2=3GrN0k?EZcz%$sTf-34m3uT6JOZGQ9ET>V_% zn`gqVt@qE&Y6ItS@E8aVyn1=|jOf}bnzv4NEHyy&&+qL9X7a7wv)2M?h>;l_38z%d z?VjuwlD92cvH(*m2wRFLJ3oZp@cQ36sjf>0v?A<+s-o6=FU;BJTHkWr`_g>IgZj6# z#nMhOyX#?@n05GF;}NQE>NAsHpFMQ%=3M!F>swV~>2@)D$Ac}vHN0!=VacFEyBAtC z1kb?2^MhggkoY|5nGI}0O}7LGLalc$h>nt3=WO5oG+?{tU>)bJO`>DVgZhWs?=`;N z{%}0(I4V3pIJsA_4=twV1p8(#hFZkb+PN|@wMlR_FPt9|E({BoMi$Or4sX9Ap1&-d zeSZFh2iM-7c-Rx(aZ)&cSwyhw3gCDy;6vwX!md{MpVb*xfTSzgEM{+>ZxOS0y=&b4 z>5ccy*54Zvv#aM^Vpik3M#33dQ!%4VFzX!LI8!AU^A;1W0i$5dSWHX_011!Z^4%5q zl3*-dF&k10oHL0D2g(|=d>CxOq4L`OO=1=%%!Xwf;w;;h=?K8)&ADQ+l(ai-`r{1` z`~GCpJKMzOgJS6+G41ez?#QCf#3!y2^X-E9Lw|rIJH)h|3%b^5gqxwwvq$c26VvM7 z)ou9Xqh?KN(wO{;{aa+!;N%y1FR6oNm95od2EylstMnq$|RKV7?Uqmg;8B>>M}gZcTJ$%q)2p|78Em)uCP=&HO@HS6?{;$2*FGYqhb&jim=$u zjD^bDX|+r&0)V4LRG5=-*d~Zb3HSu@j}O}hrS@K}?_ay=xk=w60y|A=-qtRpHNCB! zbKH*)r&ryN57pnb2V1}Hnx&uf+HhLaCy&yrKEa(?8vBjJTEi>$3d=oxt>NF{Kc-Va zwBVh>cYC~3q(dfkfQrTz1qv!15N$N905fEjcOKtPXkDPju`j(EFvexBp3%Tt)WA&E zzh4Syl1?#`A4_$kdR{jb8R}e+>>-J1tBpC*P2YUyvkBmq*JFo*FZMgtT zNXArB2)#$Y8GoE9kF{|F8PNdK7&8{qd%1Twk}UZLhrMItB*=hG5r29klD+ELb?}m+ zx#UPs$Dnqk&&$hKJx;vrfms0H2nJyN<_~Cl@m+HM6FGlK&i_l!19IMh6S2h>*00n| z&cEOLeZ(Uu0!^izJb{z|w zj$kx;gn14Zn$?`Ic;HVUO%#log=cJuqr%-}}AhoYjdzM2R+;Hb5ReS=aT0n@NJmlJde8)D zYrp`~1#(3v#gZl9bW!vx*L)d4B#41Quk$#Uh5B^_@b%`ukng{dLu=3&0d2AJ(8Wo> zCYl(S6TO$tdw+r%@Y^&44j>jt*UnUEJOLp(e#x5r%Fy)C?O}MGX+eE(?5hW+JEmHv zu5l7JL5M0D<=RyQNrG2Q)26`2-?Rbto$MgUh;5P!NygAnl}{w8t;Wg>*d|jD^F`L> zQqw`Lqs5Y4@+XZW?o^#k5Fb>XF0ACbSM<{Aj%KiKC)H58D0paH^-qd|6FTMpYO!fB zfAthtP7krKh~BTLH1lg-o+RpoBJPZ5;LSk6B76DuY=roWN`Up~5gP(}LkPgz-d|77 z23RTT=fe&{iAE83ltbepfkY1JDHQXaH@8bZP8$DQF;fkwKP#^3u3+uYTe7HK(V8@ja_&G*#=gR=sRP6#Ui~k zpbW9{fW-2dKi%p=RLwHA4D%VBBXBv8Qe}@JUZQ$N=Qx$mY77NL7)5?X)rX+X-=qG0 zR%<U|k&Hg#dIp97YOjeHhOZzE1jlJ{VS7-v|EDPvah)F+0w$c9hKwF) zyg#6*>}>E|^3l8O{Uh=zM+J6#h}b35AnL=AL>ERIuS0}8nNe)niD~ruIO-9x@cS5q z&LK{fB>(ab4_z4FUvA_33OXipJ*7o zadbBaz$N$YZtdyn?daasdE{s>Xyc3^4kM9-@gp)d#K0NKSMj2= z@pB*9v-JKA)vuk()zLUd_c&yfO*#!m3;<5|j@1D{2Lp{`b=(NIj#PDeA5-yWB0vKZ z$T}*8h87fL2KlBqz)uWea*WY(P19oS#kbCO&bo+_A%N}vD`fvWw3qsQeB^<{1|}}$ zcGqMliH$h}7iMz97B_xb?SY1%ab|niS}?h5(OoQ*Y!ThC-yyomY{+C!poyrAE$=0{ zr?gASq;YtYP~IXIZkz9WkpK38SlIDkKzxdgA3J%7A%Lw>O)*J$kd_N?5}6fWC#0R%?l^Fh3Z3-oucLN zk|i~8J@}lMRx#TdwrrT}e3I-EQcECb3jObB(AM{0pplb+nZT4QcH?*EmOCG_0#@G zW)j4O;@g{{Vxg=_ENGr@6$`eDY1^q$)4LZ5a5Dp~@g#AbHk0;h26|Tm&@=wcWd8EsX$_>+!7dt z%@iwJFo2ai9%vu5zM~f_yTpPnF|AALS>#c<>;fwP<4kCR3^awbGuz)wDo_n?lURtF zlm1AtuniLRd(Ma*kQ{#=BP^a}gM4;S>=+6sUr>g(NzA9l=5K#5ZO7*vUYJ0~@D?s7 zYb;3t6MD@mn)5(Ex6rZfx=`FG=53zWiFw=RuM3^W#T_Svlg~l(zIgJSxT7y@IS;bD zBQ0nZ9i?Q((fNAGOv$Td7;<|#3^=A2hCD*$HnC*8n2w3;!kE9d1Llt$rSBz`Vc>J~ z778{z%H1&i1z19Yl(fPNXb5eZO}V!PAk@D57sbLY^9>KIVl$fn&4@0Z z{1$Y0%Ka^Kp84%!mNVx zImQyO`clDa=mfs%lD7#_x^I8{OSuFX!j_F4Q=rocx$;8^#F4Q*_UK;9^O&Y`r}Cit zQ4ioBfn^ds$}#0Rz<1UPR8X9#5qG~MDkk7lQsM}W&#eLZd9AOrYwmI_l;&bJjCgrQ=Etb>rI>d(gGyIUR@)_3xjC^($u58WZT$F7tbb=t= zvw=m>6oTi^1Qr2;!)&gwScHCO*XmLrK%#^3I@p-C{BM?x%vI&7E;n3VI3tznK_Qwf-~ZnuY!kdQluJ75#G655yI*F!?fhKy6P_t zOcZ>211QcD+T)ZUgMXaVJi!M6VuZkfb_RAp6@>O)=adm2%WF$EN1*fe7X)J_Zz6R) zr-Re03H0g$wfI_>_O4w_;P^mZy8zpQk*q9NekPTeKB6DEaj(*h$ zk|ZpQBVe7r07Xa7eUOwA$iIDJvTLbk<6O@|O=~#2O{{5!H2l#}{?|_k zHL&uLA!N6`kC#GE43P9%uB524ZZ31a@2%X0%AMhiRbhuz(xshi*(d$fe_$Q9h1MAN3X;})9Fhqv^L zrhY(GSOa1W6`r%rqzBRzcvr zfBbC3(tEVEtq04@!Or$0{pxL>8Hw0dvJ6z&7h7L!Q*R}~%Rtd|`4fBz>#6FGQjMZ| zI^{H=;jEuVDIXSPiHgoHnPME1nrgS|f>9b3{aB7o6dc))&7D!&Gy+{6V?xk)QQ2rk zvz60fJDGv)WFqLjiORl8K-0GPOy^r%BjD-^sn`m|7vg^jFY z!x$rNeZ?jzuErLbaSO=Sv-M(;96x&Z*`UVS$@pB5V0fv`ht26L*8VuA_8PKakZM>z zofYX|`4^H8)~JH;={C7%AoXCLZsSyX>c26o{0kIYCS&Dcbo4cStEegeHLZtaxA7i& zEUGcm#-^9Yh^fLxv&PHiwoSD`;>z|FBem#vr9ioM^k#rITq(!(Ma>MfPPdL%$>HNQ zvX4@5>@k_fi5U@v4w0X@kcuxn6#!&%x=>UMJFQlAlNK z@^d-G6)jldFnpj&wK*DF_Q}sm7h*@9o8_m`yWEfdbUfb{ z4VQiLb8YOH=_RVRo>P+Leqp1Kv3@&MW1|=>kZTWFnH_S8zDy-FYrItnO~A1P<_vHY zGfcNZW?%oilC%ttaoHXxose++`t&yDepVYlBYob#FumJTPoFaPn)l<*Q_4jWWCngNLe09DJ&^1vc_o9EJuhyHc>Mw9NrI?kzTC+xN z?9qM0&&N-keL zaxm(E`A?UxLH4nk7d50%Gk%80K%8~^M6Nw%OQSSfNIJ36*N7fzq+Z4dn=rzg{f(-; z>Q(8sNaqhzgigZc-g^v zrE1OIFITP)_9?w`9?|`dnG)n6u6H40#ny46#7H(tTV8Wnsj_JIn?F6t{$!WtFd7RA>B z(q8d??@kmsQPF(`!XU$*gWl26LD-MJejXa5p|%k$PB-LOU@AWb_X|#Ha*V_1wK{CToQm&n7jO|1q8u~AR)WBEb4F4JVv}KJqwmdnWD9w#MQSyoAKECs92-0k!bvt^T{NufvjABIww?0FznqEB z!)jb?DXPOE3=#N%hOVFhM*RMD^1Vk6Q7~8IpbcEP?u7fZSfa4>CUp~$bWM5TjVX|X_YRNVW)}8->G1?DN%XnlaFGJMu7(D*xUWP7FGl>Af z=VjS>@krmpzBjYMq|KYpd$+tBzonHZHC>C>p$Ek?eTQC371138vc>D5KyV9VUs$EJ zI}uZ9E!bmhKTFnV5Y8`ko1wTCfl|wXhPxHVTF&WpY;D=^wNc}xcax1e8*3^MG-WPx z@wvO;CUrqElJYJn=@kz>|HsaUDGyuU@XfW}`_kM^v2^Ey`gcn^@IdN|>|8B3lJGy# z=!qhNDve#g+<#%9|5B5?f20rQggmS4e8jw01{BBMt2%gMtpQEPsUWP%#uo1owGNle z@`>|i(kn~L9(v-har6EuIe)ZfmHK-GPZT_R!pq{fLFx#Qd+397_e-;1gm(MRKlsAK zu7$iKoVi0rDY$tGa9^p^?NkF;9^|D0q0C-G#<^jmLOXn}(Buw$<(s^-6!0I(p?Wjb zi;?6b`&$q1?d>?QuN7b0Bb^7m-=$|n|Kc`C4v~*Yf)N*ek7>1IA7xT6cDVIuCxTl~ zt_}A72TDh_Ng@vUJHD@T-+{xYdXIFU?nEF`(Z^jK$VzBCb+nWCK1$6PVK91B8!hku#qy&JenL(rl}npOF6WDCT^@^0fJm}BE!jMQ z6_md7u+mPsa|0q?_7$ZqQ^d+QAh1Nzd!_HPWL+hRoA8l7a^4{-bq_gv$>|~It8gGW z%}jIkN0R7kMmkj4CyZH0fypke>osEIu@ud(5j_iF-#D(YHIcMzvgeU_(gmA@H;m~K zhvL`qK1mHbN3g-P2^TuTTh5hx(t-nnvOHcyH#R(UC8CFUqQ0Bnm#L;pR8tam1~hSe zz!R}tzST?RF;OLN9d%|sIkbRyPf?%PqI?L(oQ;@ZX!^Prl1?xU6Mw09oMYk=U^p6^ z<1vnSOC(M6+=NS#Gy;651-*5!DMR{o880r9fS=5UO5Ej3%xKJw;nC}3k(8m4(eo&R zVI`S9C$@m_y-qvYi1qYec1esqm@I;MutCVP-yG<_P8dsX3;H{Q)|}=9#^w9iu1U%a z7Bl$VqAu~VE<@C1+%19uj?AhBU9}RlkXb!@d12eWa3-;mo)nZmF4!g(05|vj!Uu!@ zx$^DNg@XNI^8vA7fAG-D?E&rU2{Q@5*&`I}pX?CK2NpYa3%XX|?<&B$6Y5(R6Vjmf z$QRD4dX!MbEE05vtzCk#Ysu^m^(>gX7aF=BnY-~Mm=m^^LWmV$G0PsUDqtzWB(e>B z*wnU=(8e;^61Hv?j9V8IQeJDn8xL05T;D?a7NKcx*xB7wXy{ncLZuondRMU~Hw5I;bQeLkd}h?zXJDM+tQ-usLjnk&2cNYd6d_3e9^L zYI_#a_ldPV_r}3x%nzn~BlS)y#GW&R+MY-0`+(MZzq~f=s=J>WY7O+gJ~%T75#!<6 z)XDvVt8OW3h6ei=BbbE6p-ZB3!=r=^LgTK7X0h?8aKb}C#1kZiES|XdsPQ7J#i_9M zv|v0f*`NVv@3nYJ?1ywQgm$%JX;V14Su{5jD|6e2rlc_Ke&K{oyNH#!n{y_iTR*ip zbT-V4=Onlw-@X8RU3%BUY%%@Vql9Cus6%1vVZnHK$>a)*g!YF`8%eid@?&#}XfAof z7dAIQG{0k)u>0Jjj=q5FaZ0|J!er6FMclEFvQrouhOL~v`-KBTkM;s%_t;n_8p~!K zLUpHD-nC%dC0x0RKG!D$)R|EZlPk{oiHFVM^b_d1CsxNRBhw?duYhl=OMJ2C%ROJ& z56u8~c47HkSdN8uX>=Jth)+QN=@;OQXNJrYi> zoIJ2(aW5K^7LB$=qjS-iwrI33!rTZ%r;T89rmQ$jNruVJq8 zY2WQH!WfNGfn*nHJbcv(i$F@4$@WUsbk*&e$@UeU9_p{G8pzp|zi~ulySs4N`EW7A z8cIfm-cLyj#@{^>$_eMxhEwWbtEOV>gPX$fvtr8glY7t;4%h3YGo`Nr{kN2xAF7_q zd@r{Jc1@-er%2P+=Edxa*#D z1WX09c@fH|%SB6e@Th1h2*D|z-94|L9~QUle&`gp>=zE56t|odtDrve)YJI5V$)Mi zT$*XxjBRz^HpmZQeaNTehaj#r*}0TahAy1?La_6VjUwB(g-d$Fg}qQtkZFJ-15&pD zy{3EZBs3?(MofcPz7sO?;pDdOcRX|n2YSVBz-6-W)+0H9H?({+r+BY&@~uo8U~tmWM+p@)jE|R*xQzNbUZlm z-7li*Z+~$IB(P4i!zN)(*?F_{BJ(~$Jf6^*&jow;l7 zl#sXMJ>$;hd`(%GcDX=fbOvrdO2}g`_>Qo3r(oRqp>y-RCG70PTV!)Sw&jVoyajjt z?-kBn4sYlPyE|ce4~j{xMQ{7V#rxj&&*#5!{#zsWM&{~&XLQ~#7Vmp_Gi*IMx$EbN zM<&}oFq&T866g+2gv}MReRHn4FN*bDLN%;XJUk{G84&jk3f)6O)rGKe_(_az_8$@fv+P;5LTK+?@}1QQJCgM@@+ z!{qLdlQkKQ>@CI{jn_Guxhc|nosyVgz-scb6RS)>9~cv|tA$!pQt%|ZY~Cas_lV{J zK{xQpM+bBo%R%iYE4iAC{m@L{s{7=lJ#m1Ek_tlt(`Vi94cI<1`Cbe!6~s@^J_iANk=RqwCDTr!?h*26Ox>^3zt<+YVo>d67&R5(s1o7dz-31O0NKcld^PJGyO-_B`j1Y*O=q+4fp{Q1@y# z-mZP2Lyw##T=U{mqIs(IwreVZ%%n`VG7TOu9#-_~a46-yq(VT-p@LWYM#HRb_7D^? z9(v0)YYP`{30t;K(|c?!f|<9;-R#uXkAjp7qy|3ROQ=VT&I-+l){o(X?uxj}px!V= z+;J)xiPZJ$F`;Fu&w6_IJxtxHM>k={^wBfdKrSgxya;f>>PyaREIFz&+>_vo|C+{Q z{JM%hbSqKy+=RU7+`8&Q8nZeLXo0R(QScLK)7FN{^^m6=8UG-4wW}|WMTw#J>DNQ| zBLB?QsZgqp3+ho{tS8*Hx_kgwASTMMD7qWDFo8DJYni_`mfrvn16L9YmFQfoO{q00 zXrxdkvZjP4Mj3k2b0$Sort6YZ9 zldxJ z68ZfFXzREce@X*FVZKU_3KvW}ieZbSOkBEgxfha8I5fhZHq6mZn646+g{f7U$T*58 zppDWKH$a^bz$3x8zH#@&g&OxRGVx53b;Ec-7*h8O!{Zm4Omsl(@AD49QW+($uJ%GX zaNpQ~yZS2FHGL4;@{Z#4hX?Gr2MX4ESv{nJs;lwn3S{uE;=DV+*$2j+(LN7E=iC$V zH8nL6ZFeL?Ay_b7#UsO42B4MWYTx*UNcy^uK!k|-)+t1{?HCqWPmDD67+Xm7rBZZF zW3quQL%pXb5-B0KPK#Rhp=Dt0gQFY%Q zYQ0+!JoNSAS^J}ms!#Cm{g3o*$-ycefdN$Ty_gjmPLuf^r1btdfk-I1~s#L|y=O&w9-X9WuCLWb$R4`^kcLa4};R8ld!@tebfF8u>*73{3SZ0E+{7sV_X zcmQR-r^ODXy9TO5J*piOj^ zg)Qaa`v@>DU@2O(q~k!lg!AZ;Q2L#-VM`_17%=02x@d8)*o=15lmW7UdK1ie&ZW*7 z=e2^VX+hTv5((jx+qLJhzmG>n{_*27jV1LXO&kg{CIt+EYk~f$kp*Mck}dh4mtn^t z<)6lgQ}F%DtX(?G4-B@h9qB)Cnc)AU_^Mqd%^&U1!~a85;;zd0A3ChNO5%T5qK6+g zI6#pczS7$Z(U2jUlV2r=;3Lcjiv0-rPm^<5@kO8O_(qpKSBKJxLuDYG==><*PuI!8 z;uR^BmHS9Z6;^x;N}6}!o3JaU<>&=F({=UiSzg^7yi73+hMy@}u!=DTgMlkc-$QFB zvDm!K$DU{5r<|UJ{`tDk|4Iq{YsIHd|IY}WNKz_^1?ep3y{suIUrE1SPC72-OdZGD zCw1Y^C_o9Fu#H~_-}VfOsUq3-vk<6>#%QF~i^ct$m{cqb!!Z*ZN}oUU0OQ%PRMDKy zP|Qp$MR?BYO}(jN=CJ2klwQlvMixGs7(0jd6`Y3wL#$GTjw`+sijOjxaPS7P!G-a_ zDp(kxMzOiMDz`H%B{}Mpon2LKIrZ6crz$1m7f>U-9;M8mQ9D>;FDPNdijOw*{OKj~ zF$T>D`B?L*RPRCjM5jzpDC4)~Q)u+LG%cyF6IHBUs~gAykKofZHgJR#5)hI7EM_&E z714~ckT3LzMLQze9AbL#%DqlLsuG_wlp5pw9V*Z#9$TFg)JXCyt%zWe!(BYq~QDFx*%l`y{6WgPK>o%4>X0w$IZp=?=H0>My$r;Tg zT0M!$M8l`<_-KDbCl@`R$m0F1o(?pb_=GZe4^i7#Q~!cm#C$AY)@KIqFDMk>T?sT0 zea_DJx@Wx8%SvABz0*c+7LXAP7=3`P3BRN?t5ydc>tgRwxwR86Rc|u+66*4^7~56l zu-dC9Ju%_=%LhSOX=1Vx+00_BS=Khjz55zProrVUvrXJFssm^6$+h7tfr%SvVlNa4 zqVZBbrqo%KPqc-sUnvap_aw!nV-e$N@piXS80!IM@`>dbO=IL@aXTndH1(>OuTk!w zvbwW=iQ_XzTE~7&8GeIm%H|O34^}FRD=j%J^rsY)eV_!zr;f?Q_E`P?jGnXluUEfh zzCmU=v0XCjdD_GC4=mdmvTZ2!{DedKimdw1U$ zT0f$vj4S?A@-33HM9v4~{3SVzUGAV1E^^p1UrWA?X^Q zv#-Wsdd_(Ez2qZ#VXu#z7s(+8Jbx+JfrByS7bxuKlsFZXaKiwExE{}Syj|^=0_8}2k zqZ#`imfPictycR{oL#H?Wtm1>_;XG2pK0>{Oq2DOnwme?Y>BE1Gy`{8J5l;AFN1uKlY{FldV6 z5aNs%G|9^rO4b2^xpsDqHO65jCxXCSAy^B_^=5RM4b+Y|v^G z0~sG{aQg>ir&gQ#RKwhZaT;5CFp*Tbm{$yO+Ai&iNvl1iT`}sl?te(G(eBhf)sVY< z9)wf#Kg7jpGnO@QR^sta=V^fWGb70TMNWb|hl3T3P{Q=^rq|6)( zwuY^_VJPn~uApl5E518;9GGUw6+y{0vuI2Ry(Nu$fPfh6Evy8#1)NR8@FQDYO7FsocYDPFXw#}XVMxdb;>gN zmNQw#Caq~%14qi1e3?3}Z8=$UNP8I6>tt=lqB))43yS!?rk>xMxAXg+eeC{3pT43; z{FM?TIy^N;o5oV&{vc974hF=VZ{;9J?LP0(&zaXPdq%T-6U+H_VRd02_$L6PZY%L&YjGk5WF zBJ-Lw2J4EMyic#hp(6rC3)-}Qh|_5|Eo!$kk3wSaF7qHRZgy; z$yhODV^VF`Y6mc+TI~j!&TZP2WHSwVwt@WiIBgFmyPk5|6Q?z>=91@0LgKQXJg8I> zdYE~Q8duJ8BJ-LwDb8gx^P*lw%Sp^@rOMivH<_xK!n}5>ql0 zs)4goXVF$J=UTM6i)KuD+@IL8mi4%!a7W&90{M)Z6xVVh`OwqG6*GJ*xy9OiYHezo zps_<|Kb8c5Imu1S5H!xkj4Vvu6@y({vQp8k-MEY;Tb{XgW#+=STmdHdq)o~p&n;-O z7dLl`ng&6WCS)`$$HgDk#)+E!f~Hs~-M?aCua-fZvSP2(UeK-#YBwW~8`SG_anRM2 z(z=-Ep4;%CKy-F4mXytJc$hC{^}sT3e%z#GYIFtjA#T}vG!F0Aq9bcLp81}nWiIQP zPii#x8=+GZGmPS*;YsdgGxuAlPm{RcN&{fy{$%Rl6y{&KqBCfdR_q(JNy|72=`2$d wkK!|arb~UIOPcI>vHQ#2)1KRd@9HqR8eQfmA04T|s><|?z7uELvt9SU0OU=~z5oCK diff --git a/crates/lean_prover/python-verifier/verifier.py b/crates/lean_prover/python-verifier/verifier.py index 2e15188d5..1fa4810bd 100644 --- a/crates/lean_prover/python-verifier/verifier.py +++ b/crates/lean_prover/python-verifier/verifier.py @@ -24,6 +24,7 @@ WHIR_INITIAL_FOLDING_FACTOR, WHIR_SUBSEQUENT_FOLDING_FACTOR, WHIR_MAX_NUM_VARIABLES_TO_SEND_COEFFS = 7, 5, 8 MIN_WHIR_LOG_INV_RATE, MAX_WHIR_LOG_INV_RATE, RS_DOMAIN_INITIAL_REDUCTION_FACTOR = 1, 4, 5 +_WHIR_CONFIGS = ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) # fmt: skip WHIR_CONFIGS = { (c[0], c[1]): { "log_inv_rate": c[0], @@ -36,8 +37,8 @@ {"num_queries": r[0], "ood_samples": r[1], "query_pow_bits": r[2], "folding_pow_bits": r[3]} for r in c[6] ], } - for c in ((1,7,1,10,220,16,()),(1,8,1,11,220,16,()),(1,9,1,12,220,16,()),(1,10,1,13,220,16,()),(1,11,1,14,220,16,()),(1,12,1,15,220,16,()),(1,13,1,16,220,16,()),(1,14,1,15,221,16,()),(1,15,1,16,221,16,()),(1,16,1,16,73,16,((222,1,16,11),)),(1,17,1,16,73,16,((223,1,16,12),)),(1,18,1,16,73,16,((224,1,16,13),)),(1,19,1,16,73,16,((225,1,16,14),)),(1,20,1,16,73,16,((227,1,16,15),)),(1,21,2,16,32,16,((229,1,16,16),(73,1,16,9))),(1,22,2,16,32,16,((230,1,16,12),(74,1,16,10))),(1,23,2,16,32,16,((234,1,16,13),(74,1,16,11))),(1,24,2,16,32,16,((235,1,16,14),(74,1,16,12))),(1,25,2,16,32,16,((241,2,16,15),(74,2,16,13))),(1,26,2,16,21,14,((243,2,16,16),(74,2,16,14),(32,2,16,14))),(1,27,2,16,21,14,((248,2,16,15),(75,2,16,15),(32,2,16,15))),(1,28,2,16,21,14,((256,2,16,16),(75,2,16,16),(32,2,16,16))),(1,29,2,16,21,14,((262,2,16,15),(76,2,16,12),(33,2,16,17))),(1,30,2,16,21,14,((270,2,16,16),(76,2,16,13),(33,2,16,18))),(2,7,1,13,109,16,()),(2,8,1,14,109,16,()),(2,9,1,15,109,16,()),(2,10,1,16,109,16,()),(2,11,1,12,110,16,()),(2,12,1,13,110,16,()),(2,13,1,14,110,16,()),(2,14,1,15,110,16,()),(2,15,1,16,110,16,()),(2,16,1,14,55,16,((111,1,16,10),)),(2,17,1,15,55,16,((111,1,16,11),)),(2,18,1,16,55,16,((111,1,16,12),)),(2,19,1,15,55,16,((112,1,16,13),)),(2,20,2,16,55,16,((112,1,16,14),)),(2,21,2,16,28,16,((113,1,16,15),(55,1,16,10))),(2,22,2,15,28,16,((114,1,16,16),(55,1,16,11))),(2,23,2,16,28,16,((114,1,16,13),(56,1,16,12))),(2,24,2,16,28,16,((115,1,16,14),(56,2,16,13))),(2,25,2,15,28,16,((118,2,16,15),(56,2,16,14))),(2,26,2,16,19,15,((118,2,16,16),(56,2,16,15),(28,2,16,17))),(2,27,2,16,19,15,((119,2,16,13),(57,2,16,16),(28,2,16,18))),(2,28,2,16,19,15,((120,2,16,14),(57,2,16,14),(29,2,15,19))),(2,29,2,16,19,15,((123,2,16,15),(57,2,16,15),(29,2,15,20))),(3,7,1,9,73,16,()),(3,8,1,10,73,16,()),(3,9,1,11,73,16,()),(3,10,1,12,73,16,()),(3,11,1,13,73,16,()),(3,12,1,14,73,16,()),(3,13,1,15,73,16,()),(3,14,1,16,73,16,()),(3,15,1,12,74,16,()),(3,16,1,13,44,16,((74,1,16,11),)),(3,17,1,14,44,16,((74,1,16,12),)),(3,18,2,15,44,16,((74,1,16,13),)),(3,19,2,16,44,16,((74,1,16,14),)),(3,20,2,15,44,16,((75,1,16,15),)),(3,21,2,16,25,16,((75,1,16,16),(44,1,16,11))),(3,22,2,15,25,16,((76,1,16,11),(45,1,16,12))),(3,23,2,16,25,16,((76,1,16,12),(45,2,16,13))),(3,24,2,16,25,16,((77,2,16,13),(45,2,16,14))),(3,25,2,16,25,16,((78,2,15,14),(45,2,16,15))),(3,26,2,16,18,12,((79,2,15,15),(45,2,16,16),(25,2,16,19))),(3,27,2,16,18,12,((80,2,16,16),(45,2,16,15),(26,2,13,20))),(3,28,2,15,18,12,((82,2,15,15),(46,2,16,16),(26,2,13,21))),(4,7,1,8,55,16,()),(4,8,1,9,55,16,()),(4,9,1,10,55,16,()),(4,10,1,11,55,16,()),(4,11,1,12,55,16,()),(4,12,1,13,55,16,()),(4,13,1,14,55,16,()),(4,14,1,15,55,16,()),(4,15,1,16,55,16,()),(4,16,1,13,37,16,((56,1,16,9),)),(4,17,1,14,37,16,((56,1,16,10),)),(4,18,2,15,37,16,((56,1,16,11),)),(4,19,2,16,37,16,((56,1,16,12),)),(4,20,2,13,37,16,((57,1,16,13),)),(4,21,2,14,23,15,((57,2,16,14),(37,2,16,12))),(4,22,2,15,23,15,((57,2,16,15),(37,2,16,13))),(4,23,2,16,23,15,((57,2,16,16),(37,2,16,14))),(4,24,2,15,23,15,((58,2,16,13),(38,2,16,15))),(4,25,2,16,23,15,((58,2,16,14),(38,2,16,16))),(4,26,2,16,16,16,((60,2,15,15),(38,2,16,17),(23,2,15,22))),(4,27,2,15,16,16,((61,2,16,16),(38,2,16,18),(23,2,15,23)))) -} # fmt: skip + for c in _WHIR_CONFIGS +} MIN_LOG_MEMORY_SIZE, MAX_LOG_MEMORY_SIZE = 16, 26 MIN_LOG_N_ROWS_PER_TABLE, MIN_BYTECODE_LOG_SIZE, MAX_BYTECODE_LOG_SIZE = 8, 8, 22 diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index b8c7d270d..91b3f76ba 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -218,6 +218,7 @@ fn test_zk_vm_all_precompiles() { } #[test] +#[ignore] fn dump_test_vector_for_python_verifier() { const LOOP_ITERS: usize = 5000; @@ -236,9 +237,8 @@ fn dump_test_vector_for_python_verifier() { .join("zkvm_test_vectors"); std::fs::create_dir_all(&out_dir).unwrap(); - // Sidecar: raw u32-LE bytecode multilinear. - let mle_path = "proof.bytecode_mle.bin"; - let mut mle_file = std::fs::File::create(out_dir.join(mle_path)).unwrap(); + let bytecode_path = "proof.bytecode_mle.bin"; + let mut mle_file = std::fs::File::create(out_dir.join(bytecode_path)).unwrap(); for v in &bytecode.instructions_multilinear { mle_file.write_all(&f_u32(*v).to_le_bytes()).unwrap(); } @@ -250,7 +250,7 @@ fn dump_test_vector_for_python_verifier() { }) }; let out = serde_json::json!({ - "bytecode_multilinear_path": mle_path, + "bytecode_multilinear_path": bytecode_path, "public_input": public_input.iter().map(|&f| f_u32(f)).collect::>(), "proof": { "transcript": raw_proof.transcript.iter().map(|&f| f_u32(f)).collect::>(), diff --git a/crates/lean_prover/tests/check_whir_configs.rs b/crates/lean_prover/tests/check_whir_configs.rs index 7009cc904..2f64f98c8 100644 --- a/crates/lean_prover/tests/check_whir_configs.rs +++ b/crates/lean_prover/tests/check_whir_configs.rs @@ -67,7 +67,7 @@ fn check_whir_configs() { let expected = expected_whir_configs_line(); println!("{expected}"); - let verifier_py = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("verifier.py"); + let verifier_py = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("python-verifier/verifier.py"); let src = fs::read_to_string(&verifier_py).unwrap_or_else(|e| panic!("failed to read {}: {e}", verifier_py.display())); From f8705ff8a9a6081316b5fa11afd2f734edfb9c1e Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:40:53 +0400 Subject: [PATCH 66/69] w --- crates/lean_prover/tests/check_whir_configs.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/lean_prover/tests/check_whir_configs.rs b/crates/lean_prover/tests/check_whir_configs.rs index 2f64f98c8..60266ef21 100644 --- a/crates/lean_prover/tests/check_whir_configs.rs +++ b/crates/lean_prover/tests/check_whir_configs.rs @@ -1,12 +1,4 @@ -//! Ensure the WHIR parameter table hardcoded in `crates/lean_prover/verifier.py` -//! matches what the Rust prover would compute from `default_whir_config`. The -//! test prints the expected line (so you can paste it back if it drifts) and -//! asserts that `verifier.py` contains that exact string. -//! -//! Run: -//! cargo test -p lean_prover --test check_whir_configs -- --nocapture - -use std::fmt::Write as _; +use std::fmt::Write; use std::fs; use std::path::PathBuf; @@ -63,7 +55,7 @@ fn strip_ws(s: &str) -> String { } #[test] -fn check_whir_configs() { +fn check_whir_configs_in_python_verifier() { let expected = expected_whir_configs_line(); println!("{expected}"); From bf031e62584dfa40023c02005f7d7e88ec2853bf Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:46:06 +0400 Subject: [PATCH 67/69] w --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47f0e25a4..e351fab11 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ Minimal hash-based zkVM, targeting recursion and aggregation of hash-based signa

- Documentation + Documentation - Python verifier + Python verifier

From 1b952f6e9f245691b2159a52b5a918b3df6aecb6 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:48:02 +0400 Subject: [PATCH 68/69] w --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e351fab11..d8a401445 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,8 @@ Minimal hash-based zkVM, targeting recursion and aggregation of hash-based signatures, for a Post-Quantum Ethereum.

- - Documentation - - - Python verifier - + Documentation + Python verifier

## Proving System From ec1903bf0cd0b6d8da73528f177e63409329dd43 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 27 May 2026 05:50:42 +0400 Subject: [PATCH 69/69] w --- crates/lean_prover/python-verifier/verifier.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/lean_prover/python-verifier/verifier.py b/crates/lean_prover/python-verifier/verifier.py index 1fa4810bd..7afe12f60 100644 --- a/crates/lean_prover/python-verifier/verifier.py +++ b/crates/lean_prover/python-verifier/verifier.py @@ -1,6 +1,6 @@ """Pure-Python verifier for leanVM proofs. Setup the test vector (one-time): - cargo test --release --package lean_prover --lib -- test_zkvm::dump_test_vector_for_python_verifier --include-ignored + cargo test --release --package lean_prover --lib -- test_zkvm::dump_test_vector_for_python_verifier --include-ignored Run: python3 crates/lean_prover/python-verifier/verifier.py Format: @@ -1154,8 +1154,7 @@ def main() -> int: vector_path = Path(__file__).resolve().parents[3] / "target" / "zkvm_test_vectors" / "proof.json" if not vector_path.exists(): print( - f"Test vector not found at {vector_path}. Generate it first with:\n" - " cargo test --release -p lean_prover dump_test_vector_for_python_verifier -- --nocapture" + f"Test vector not found at {vector_path}. Please follow the instructions at the beginning of verifier.py file." ) return 1