Small proofs - #4
Open
TomWambsgans wants to merge 101 commits into
Open
Conversation
Two >128-bit alternatives to F128, NEON/PMULL-optimized: - F192 = GF((2^64)^3), y^3+y+1 over x^64+x^4+x^3+x+1: Karatsuba-3, 9 PMULL per mul (+ schoolbook and scalar-reduce variants). - F160 = GF((2^32)^5), y^5+y^2+1 over x^32+x^7+x^3+x^2+1: Montgomery 13-mult 5-term formula (+ Karatsuba-15, schoolbook). Both ship deferred-reduction accumulators (F192Unreduced / F160Unreduced), square(), Fermat inv(), portable fallbacks, and tests: NEON-vs-reference, independent Python vectors, field axioms, and computational irreducibility proofs of all four moduli. F128 gains a 2-PMULL square(), now used by its inv(). src/bin/field_bench.rs benchmarks mul latency/throughput, deferred inner products, add/square/inv, and all mul variants. Verdict on M-series: F192 mul latency ~1.04x F128, deferred inner product ~1.5x; F160 strictly dominated (PMULL wastes half its width on 32-bit coefficients). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F192 is the chosen field; the GF((2^32)^5) alternative is dropped (PMULL wastes half its width on 32-bit coefficients — it was strictly dominated). The benchmark and integration tests now cover F128 vs F192 only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WP1+WP3+WP4 of the 64-bit transition: - field/gf2_64.rs: first-class F64 (1-PMULL mul, primitivity test). - field/tower_f128.rs: F128T = K[y]/(y^2+y+x^61) (Artin-Schreier, trace-1 pinned). NEON-resident kernels: mul 5 PMULL with a vectorized pair fold, mul_base (K x E) 2 PMULL, square 5 PMULL. - ntt/additive_ntt_f64.rs: LCH additive NTT over K mirroring the F128 twin (interleaved SoA, fused 2-layer sweeps, cache-blocked parallel), inner loops on a NEON lane-pair butterfly kernel. - pcs/ligerito_k.rs: full recursive Ligerito prover + dense verifier committing F64 codewords (8-byte symbols) and folding/opening over F128T, including the sparse transposed-NTT basis induction and LIG_K_TRACE phase timing. Secure-profile shapes reused (soundness constants are F128-era pending a K derivation). 10 in-module tests: roundtrips, determinism, tamper rejections, dense==sparse induce. - bins: ligerito_bench (bits committed+opened per second, both per-bit and per-word framings) and commit_parts_bench (NTT/Merkle split). Measured (M-series, Secure, rate 1/2): per committed bit the K pipeline runs at 0.66-0.92x of the F128 one (2x elements per bit: one extra fold level and NTT layer at equal byte volume); per machine word (the VM view, where words halve from 128 to 64 bits) it is 1.04-1.16x faster while committing half the bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of recursive_verifier_with_basis_succinct to the K/F128T types: closed-form residual evaluation of the induced bases (K-side W-hat recurrence lifted by mul_base), byte-identical transcript replay against the prover (tapered fold-grinding indices, query sampling, alpha_last/beta_last after yr, index-consumption checks). Tests: 13 in ligerito_k (succinct roundtrips incl. the sparse-induce path, tamper rejections, dense/succinct verdict agreement over randomized tamper kinds); 35/35 lib-wide. The throughput bench's K correctness gate now runs through the succinct verifier. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
phi: F128 (GHASH) <-> F128T (tower over GF(2^64)), constructed offline (root of x^64+x^4+x^3+x+1 inside GHASH's 2^64-subfield via Cantor-Zassenhaus, Artin-Schreier solve for y, matrix inversion) and verified multiplicative/additive at build time and in tests (2000 random products). The 64-bit transition's flock boundary: the zerocheck (phi_8, GF(2^8) NTT, univariate skip) stays entirely in GHASH and its output claims map through phi before the K-committed ring switch, which is exact on bit-valued multilinears since phi fixes F_2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 64-bit transition's single ring switch (f = 64, e = 128, the note's unrelated-degrees case): pack_k (64 bits per F64 word, bit-identical to the old 128-bit packing reinterpreted), tensor_algebra_k (rectangular 64x128 transpose s_hat_v -> t_w, plus the square E-tensor algebra for succinct evaluation), ring_switch_k (prove/verify/verify_succinct with generalized prefix weights: plain eq for point claims, phi-mapped phi_8 Lagrange weights for flock's univariate-skip claim; eval_rs_eq_k via Theta = sum eq(q,y) (x) eq(z,y) folded by the r'' tensor). End-to-end tests reduce a bit witness claim through ligerito_k and verify dense + succinct; a claim-preserving s_hat_v forgery is rejected. 51/51 lib tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The K/F128T analog of open/verify_opening_batch_mixed_ligerito_stacked: point and strided stack claims plus ring-switched q_pkd claims (ring_switch_k) gamma-combined into one b_stack and target, opened by a single recursive_prover_with_basis_k over the full stack; succinct verification assembles the combined eval_b closure with prefix-amortized eval_rs_eq (an improvement over the F128 layer's per-position recomputation), handling both residual regimes. Tamper tests cover every claim kind; proof determinism pinned. 54/54 vendor tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The main-crate half of the 64-bit transition (transition doc WP5+WP7): machine words, memory cells, registers, immediates, table columns and the committed stack move to K = GF(2^64) (g = x, ord 2^64-1); every challenge, sumcheck/GKR value, bus fingerprint and evaluation claim lives in the tower E = F128T; the transcript stream is Vec<F128T> over a byte-identical sponge (shared lane helpers keep the GHASH-typed Challenger path in lockstep for the vendored flock code). - PCS: commit_k + the stacked batch-mixed K opening (stack_open_k), LOG_PACKING 6, Secure profile, 120-bit target kept. - BLAKE3 ISA: operands are 4-cell quads (12 value slots, 12 bus ops, PIN_SLOTS [0,1,2,3,18,19], stride K_LOG-6); q_pkd is the lo/hi flattening of flock's packed witness (layout unchanged, M_BASE=640 is 64-aligned). - flock boundary: the zerocheck stays GHASH; its (ab, c) claims cross into the tower via the field isomorphism with phi_8 Lagrange prefix weights and the full x_inner_rest ++ x_outer suffix (invariant asserted). - Instance caps: log_mem <= 32, per-table rows < 2^32, bytecode <= 2^32, keeping all g-power counting arguments exact under ord(g). - xmss: MD IV size element in GF(2^64); WOTS encoding re-budgeted to 21 digits per 64-bit word (grinding unchanged). cargo testall green (41 tests incl. xmss_vm and hash_chain); vendor untouched (54/54 flock-core, 17/17 flock-prover). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doc.tex: the machine field is K = GF(2^64) (g = x, order 2^64-1) with the two-field convention stated once in the preliminaries (K-valued columns, E-valued challenges, |E| = 2^128); PCS section rewritten to the actual Ligerito Secure profile (K-commit, E-open, 120-bit, per-claim batching coefficients); BLAKE3 table at 4-word quads with 12 memory reads and the single f=64/e=128 ring switching crossed through the GHASH-to-tower isomorphism; instance caps and the counting bound R < 2^39 << ord(g); public input pinned to the shipped [F64; 2]. All pre-existing em dashes removed. transition-to-64-bits.tex: Status paragraph in the plan section: all work packages landed, testall green; two deltas versus plan (zerocheck kept in GHASH behind the isomorphism instead of retargeted; no flock-prover re-layout since M_BASE = 640 is 64-aligned); measured demo numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The compiler lowered every indexed heap access as SET(const) + MUL + DEREF even though the DEREF instruction carries a compile-time offset exponent end to end (executor, constraint, bytecode bus). Constant g-power indices (1, GEN, GEN ** k, power-of-two literals, products of those, and the constant factor of buf[r * GEN**k]) now fold into beta at zero instruction cost across all five access paths (reads, stores, runtime blake3 and hint_witness slices). Also: GEN ** (expr) with compile-time integer expressions in the DSL, a branch-scoped constant cell cache (one SET per distinct constant per scope), x*1 / x+0 / if x == 0 identities, and direct-into-return-slot lowering. The XMSS aggregation program drops its now-redundant cursors (walk and Merkle tweaks are direct constant-indexed reads) and shares slot pointers per signature. Semantics, hint streams and the scheme are byte-identical. Cycles per XMSS verification (N=64): 4692.6 -> 3214.4, which is 7.4% BELOW the pre-transition 3472.7; at N=1024: 3200.8 cycles/XMSS, 108.3 XMSS/s, 666 KiB proof. Remaining above-baseline opcode is DEREF (+276/XMSS), the inherent 4-cell heap bridging width. testall green. LEANVM_PC_HISTO=1 now prints an executed-count histogram beside the disassembly (env-gated, zero overhead unset). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Profiling the XMSS aggregation (N=1024, 2^28-word stack) against main showed proving at 0.5x the old wall clock despite 7% fewer cycles; the gap was almost entirely allocation behavior, not arithmetic: - recursive_prover_with_basis_k now borrows the witness (&[F64]); the stacked opener passed stack.to_vec(), a 2.1 GB copy per prove. - b_stack is alloc_uninit + parallel zero fill; vec![F128T::ZERO; n] bypasses the calloc fast path and zero-wrote 4.3 GB single-threaded. - Point-claim folds: parallel gamma-seeded eq builder (build_eq_table_ext_parallel / _seeded_into) writing into one reused scratch buffer, then a multiply-free parallel accumulate; was a serial per-claim build_eq_table_ext (2.4 s of the open). Proof bytes pinned identical (bincode hash) before/after. - Removing the copies also takes the peak working set below the paging threshold, speeding every other phase. Also: F128T::mul2 batched NEON kernel (8 PMULL + 3 pair folds per two products, 40% faster on serial dependence chains) ships in the vendor for latency-bound callers; measurement showed the GKR loops are throughput/port-bound so they stay scalar, with rationale comments at each candidate loop. Two redundant unsafe blocks fixed (zero warnings). LEANVM_PROFILE now splits reduction/ring/stack-open and the bus phases; LIG_K_TRACE covers the stacked opener. XMSS N=1024: 105.9 -> 157.7 XMSS/s, proof 666.0 KiB unchanged. testall 41/41, vendor 56/56. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ring_switch_k::prove: the suffix eq tensor now builds with the parallel builder (was 502 ms serial of the 714 ms total at the XMSS 2^28 stack); fold_1b_rows_k gains a method-of-four-Russians kernel (8-word transpose groups, two 16-entry subset-sum tables, one 8x8 bit transpose per byte position, density-independent) dispatched for multiple-of-8 lengths with the scalar bit-scan kept as fallback and an equality test across lengths. Ring-switch proves: 714 -> 205 ms. leaf::decompose_prove: claim values are transcript-independent within one decompose_formula call (no challenge sampled between claims), so all column MLE evaluations now run in one rayon pass and replay through the transcript in the original byte-identical order (debug assert re-derives serially). Decompose: 316 -> 39 ms. Proof bytes hash-pinned identical. XMSS N=1024: 157.7 -> 180.2 XMSS/s (main before the transition: 229.6). testall 41/41, vendor 57/57. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New section benchmarking F128T (tower mul, mul2 pair), F64 (K x K) and mul_base (K x E) against the GHASH rows, closing the WP1 leftover. First numbers show the K kernels are implementation-bound, not math-bound: F64 at 0.64 ns/op throughput barely ties GHASH's full 128-bit mul at 0.70 (shift-XOR folds and NEON-to-GPR crossings burn the 1-PMULL budget), tower E x E at 3.41 vs 0.70. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Why the tower is not fundamentally slower than GHASH (measured field table, the three implementation gaps, the one structural serial fold), where the initial 2x wall-clock regression actually came from (memory cliffs and serial builds, all fixed transcript-byte-identical), the witness elements-vs-bits accounting, current phase profile vs main, and the ranked remaining work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite of the GF(2^64) and F128T NEON multiplies at GHASH kernel quality: PMULL/PMULL2 products on register lanes (no GPR round trips mid-kernel), reductions via PMULL-by-0x1B folds with a TBL variant for the 4-bit second-order overflow, and a parallel 192-bit fold for the tower that eliminates the serial reduce-then-multiply-by-x^61 chain (word 1 folds by x^64 mod P, word 2 by x^128 mod P, the Frobenius square). NTT butterflies switch to the PMULL pair reduce. All variants exposed in field_bench; agreement tests (10k randoms per kernel) added; proof bytes hash-pinned unchanged. Measured (ns/op, latency / 8-chain throughput): F128T mul 10.10 / 3.42 -> 6.91 / 1.28 (GHASH ref 0.71) F128T mul_base 6.50 / 1.16 -> 5.14 / 0.70 F128T mul2 6.18 / 2.18 -> 3.49 / 1.46 F64 mul 4.84 / 0.64 -> 4.82 / 0.50 NTT (64 MB codeword): 6.7 -> 5.3 ms. End to end (XMSS N=1024): 178.6 -> 210.5 XMSS/s; gkr 1327 -> 939 ms; commit 969 -> 759 ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scoreboard at 210.5 XMSS/s; new PCS head-to-head section: per-bit 0.79-0.88x (open-side element scaling), per-word 1.2-1.34x faster, and the padding accident doubling positions at the XMSS instance size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add MiB witness sizes per row and the plausibility decomposition of the 18 Gbit/s = 2.2 GiB/s figure (bandwidth-bound NTT + parallel BLAKE3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PCS tables and scoreboard now report GiB and GiB/s (bit-based rates read as bytes caused confusion); ligerito_bench prints GiB/s and MiB witness sizes. The field-question TLDR now states plainly that GHASH E x E is 1.8x faster per multiply today, attributes the origin (reduction granularity + tuning lineage, partly closable, ~1.2-1.4x probably inherent), and gives the design's actual answer: E x E is rare, surviving only in GKR at ~19% of the prove. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port main's features onto the 64-bit GF(2^64) machine-word design:
- Flexible BLAKE3 on 64-bit words: the four input chunks (ins[0..3], each
spanning two consecutive cells) are addressed independently; the output
spans four consecutive cells. Five bytecode operand slots, five address
bindings, twelve memory reads per row. The DSL's copy/zero alias
forwarding becomes pair-aware (chunk_src): a chunk forwards only when its
two cells stay contiguous at the source, else it materializes.
- VM-native hashing over F64 words: vmhash::compress = the Blake3 opcode
(8 words in, 4 out), hash_slice's length-in-IV marker is now a K-field
g-power, and the vendored merkle leaf hash follows the same convention
(so a recursive verifier replays transcript, program digest, and PCS
leaves with one routine).
- VM-native Fiat-Shamir sponge: 256-bit chaining value = [F64; 4] advanced
only by the opcode compression, domain tags in the last block word,
E = F128T scalars as two K-lanes per block ("leanvm-b/transcript/v2").
- Program digest via vmhash::hash_slice (3 F64 words per op).
- Zerocheck/GKR keep the tower field but adopt main's free node-{0,1,g}
interpolation (new mul_by_g_e: lane-wise shift-fold on F128T), scratch
reuse, incremental eq-table folding, and ping-pong fold buffers.
- Compiler: main's @unroll inlining, GAddr/fconst compile-time folding,
alias forwarding, and compile-time if-folding coexist with the branch's
const-cell cache and beta-immediate folds (heap_addr falls back to
array_ptr's constant-factor fold).
- xmss_aggregate.py: 64-bit two-cell-value layout parametrized by the
host-supplied placeholders; branchless Merkle child ordering ported to
two words.
- JUMP inverse hints deferred to one batched Montgomery inversion; GPowMap
fast hasher keyed on F64's single limb.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ot loops Field level (vendor/flock-core tower_f128, the GHASH-parity move): - F128TUnreduced: the three unreduced Karatsuba sub-products of an E x E multiply, XOR-accumulated, one 5-PMULL reduction per sum (reduction is GF(2)-linear). 3 PMULL per term instead of 8. - F128TBaseUnreduced: the two unreduced lanes of a K x E mul_base, one reduce_pair per sum. 2 PMULL per term. - inner_unreduced_neon: register-resident deferred inner product; 0.62 ns/op = exact GHASH parity (was 1.36 per-term-reduced, 2.2x). - Three new single-mul kernel variants (vector-resident karatsuba/ schoolbook, schoolbook shift-tail) benched via field_bench --variants: none beat the default 8-PMULL parallel-fold; kept as alternates. Prover wiring (bit-identical proofs; reduction commutes with XOR): - GKR round message: the outer eq*(even*odd) products XOR-accumulate unreduced, one reduction per accumulator per round (bus 440 -> 385 ms). - Zerocheck round messages: same deferral of the outer eq*C products. - Bus leaf rows: the alpha-weighted coordinate sum accumulates raw mul_base lanes, one pair reduction per row. XMSS N=1024, 11 threads: 395 -> ~410 XMSS/s. field_bench is also simplified to the single F128-vs-F128T table (F192 and the old per-kernel sections removed; preflight now checks the two representations against each other through the explicit isomorphism). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The round messages (u_0, u_2) of round_msg_lsb_base/ext and both fused fold_and_msg kernels accumulated per-term-reduced products; they now XOR-accumulate unreduced parts (mul_base_unreduced / mul_unreduced) and reduce once per chunk. The fold writes `(1+r)x + ry` likewise pay one reduction instead of two. Bit-identical (reduction commutes with XOR). Initial sumcheck 129.5 -> 110 ms at XMSS N=820; the loop is near its memory-bandwidth floor, so wall-clock effect is small. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The K ring switch predated two optimizations the F128 layer got and never picked them up: - Split eq tensors (build_eq_split_ext + fold_ext_elems_split): the suffix tensor factors as eq_lo x eq_hi (2·2^(n/2) entries); entries are reconstructed on the fly for the rs_eq_ind fold, and the full 2^n tensor is never materialized. Field multiplication is exact, so reconstruction is bit-identical to the full build. - Precomputed s_hat_v: flock's reduction already captures the 128 bit-slice MLEs (WitnessClaim.s_hat_v); ring_claim now recombines them into the 64-wide K-packing values — 64-word y = 2y'+b is the b-half of 128-word y', so s64[i] = (1+c)·s128[i] + c·s128[i+64] with c the first inner-rest coordinate — and ring_switch_k::prove skips fold_1b_rows entirely (values hard-checked against the claim, transcript identical). RingSwitchClaimK gains an optional prover-side s_hat_v field (verifier bundles leave it None); the no-precompute fallback reconstructs the tensor from the factors and runs the standard fold. XMSS N=820: ring-switch proves 93 -> 39 ms (eq split 0.03 ms, s_hat 0.00 ms — matching main's front-end), stack open 576 -> ~522 ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconcile main's 8-crate workspace restructure + one-shared-transcript
protocol with this branch's 64-bit field transition (K = GF(2^64) machine
words opened over the tower E = GF(2^128)).
Structural: adopt main's `crates/*` layout and `fiat_shamir` crate; the
K-variant PCS files (ligerito_k, ring_switch_k, stack_open_k, pack_k,
tensor_algebra_k, additive_ntt_f64) and the tower field modules move under
crates/{pcs,primitives}. Hint machinery (RHint, GPowMap, grow_gpow) lives in
lean_vm::cpu::hints. Bench binaries move into their owning crates.
Field/transcript: the shared Fiat-Shamir sponge is F64-word / F128T-scalar;
the GHASH-typed F128 consumers (flock zerocheck/lincheck, the F128 Ligerito
twin) ferry through raw lane lifts (pcs::as_e / as_ghash) at their transcript
touchpoints - no arithmetic crosses representations. flock's reduction and
the F128 Ligerito are generic over the opening type so lean_vm can drive them
with its stacked Ligerito-K proof on one transcript.
Protocol: keep main's RLC-batched three-tree bus GKR, dedup'd claim
decomposition, and single-seed statement binding, retyped to E; keep the
branch's K-committed stacked opening as the sole PCS path (the F128 stacked
opener stays for its own tests).
Deferred: the n->1 recursion harness (crates/rec_aggregation/src/recursion.rs
+ guests/recursion.py) is gated behind the `recursion` feature - it dissects
the proof stream word-for-word and needs porting to the 64-bit-word layout.
Tests: workspace builds warning-free; primitives/fiat_shamir/pcs/flock/
lean_vm/lean_compiler/xmss and the XMSS-aggregation + fibonacci harnesses all
pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Memory words become 128-bit elements of the tower E = GF(2^128) so a recursion guest can do native 128-bit field arithmetic, while addresses, the pc/fp, read counters, operands, opcodes, and domain separators stay in the 64-bit subfield K = GF(2^64) (g-powers of order 2^64-1). This keeps the branch's base-field-commit / extension-open PCS win: only data costs 128 bits, addressing stays 64. Representation & protocol: - Each 128-bit word is committed as its two K-lanes (_lo/_hi), two ordinary F64 columns in the one stacked K-witness; the PCS/stacking/Merkle layer is untouched. The E-value is reassembled (lo + hi*y) only in the constraint zerocheck and at the opening point, never in the commitment. - MUL_NATIVE = the E tower product, XOR = 128-bit xor, both written as degree-2 E-relations on the value lanes, so the zerocheck is unchanged. - On the bus a memory value rides two K-coordinates (widest tuple stays 9); the memory image is two lanes; the public-input binding is one claim per lane. - Words used as addresses (DEREF pointer, JUMP destination/frame) are forced into K by the single-lane address/selection constraints, and witness-gen errors if such a word has a nonzero high lane. - BLAKE3: a 128-bit chunk is one cell, so a row reads six cells (four inputs, two-cell digest); the 12 value lanes map 1:1 onto the same 12 flock q_pkd slots (flock packing unchanged). DSL: - 128-bit literals: an integer literal is the raw 128-bit bit pattern (low 64 = K-lane, high 64 = y-lane); 2**64 is the tower generator y, so a guest packs two lanes as `lo + hi * (2**64)`. KVal::Const, lit_field, parse_const, try_field_const, const_cell, Alias::Const, and fconsts migrate F64 -> F128T; g-power/address constants embed via .into(). - blake3 lowering: a 256-bit operand is two 128-bit cells (chunk = one cell, digest = two cells). Guests: all hashing guests rewritten to the 2-cell layout and revalidated (6 blake3 .py programs, the stack_buf suite, the hash_chain benchmark, and the xmss aggregation guest + driver — WORDS_PER_VALUE 2->1, WORDS_PER_BLOCK 4->2, the WOTS encoding reconstructed into one cell via the y literal). The `recursion` harness stays feature-gated off (awaits its own guest port). Docs: doc.tex, transition-to-64-bits.tex, and zkDSL.md updated to the hybrid design. Full workspace test suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings in the signers-cache footprint fix, main's "unify equality table construction" and "make recursive proof verification atomic" refactors. Conflict resolutions (11 hunks, 5 files): - signers_cache.rs: clean merge (the fix originated on both sides identically). - multilinear.rs, constraints.rs, gkr.rs, pcs/ligerito.rs: kept the field-exploration (dual-lane) versions. main's changes here were purely the eq_table -> build_eq rename from "unify equality table construction", which merged the base's two eq-table constructors into one. That unification is invalid under the dual-lane design: here eq_table (over F128T, the VM constraint lane) and build_eq (over F128, the PCS lane) are DISTINCT functions for different field types, not duplicates. Taking main's rename would also have dropped ligerito's `crate::as_ghash` conversions that map tower-field samples into F128 for the PCS, breaking the K PCS path. - rec_aggregation/lib.rs: took main's new atomic-verification exports (RecursiveProof, RecursiveVerifyError, run_recursion) but kept them under field-exploration's `#[cfg(feature = "recursion")]` gate, since the n->1 recursion harness still awaits the 64-bit guest port. recursion.rs itself auto-merged to main's version (compiled only with the feature). Verified: `cargo build --release` (default features) and `cargo test --workspace` both pass (pcs 98, primitives 50, lean_vm 16, flock 74, rec_aggregation 3, xmss 7; no failures). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F128 already had an x86_64 pclmulqdq path, but the tower field F128T and its F64 base had only an aarch64 PMULL kernel plus a naive software fallback, so on x86 every tower multiply ran software (~27 ns) while F128 used hardware clmul (~2.4 ns). Since the prover runs its whole polynomial arithmetic over F128T, that 11x per-mul gap made XMSS aggregation prove ~3x slower on x86 than on main, despite identical VM work (same cycles, instruction mix, and memory). Mirror the aarch64 kernels with x86_64 pclmulqdq: - gf2_64: one CLMUL product + a two-CLMUL fold by 0x1B (= x^64 mod P). - tower_f128: 3-CLMUL Karatsuba over the scalar coefficients, then the tower reduction c0 = reduce(p0) + x^61·reduce(p1), c1 = reduce(pm + p0); plus the deferred-reduction kernels (mul_unreduced/reduce, mul_base_unreduced/reduce) the sumcheck hot loop uses. Wired behind cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq")); aarch64 and the software fallback are unchanged. Verified against the software oracle (new x86_variants_match_software test, 10k random cases: mul, square, and both deferred paths) and the full workspace suite (304 tests). On this AMD box, xmss --n-signatures 820 goes 53.6 -> 136.3 XMSS/s (proving 15.3s -> 6.0s); the deferred inner product goes from 11.7x to 1.10x of F128. Credit: binius64 <https://github.com/binius-zk/binius64> for the GF(2^64) base-field CLMUL and the deferred-reduction structure (its Monbijou field is this same GF(2^64)); the degree-2 extension differs (ours is Artin-Schreier y^2+y+x^61, not their y^2+xy+1), so the extension reduction follows our field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tower Adds F128Txy, a GF((2^64)^2) tower K[y]/(y²+x·y+1) — binius64's tower choice — over the same base field K as tower_f128's Artin-Schreier K[y]/(y²+y+x^61). Same 3-CLMUL Karatsuba products; only the degree-2 fold differs (a shift-by-x vs a multiply by x^61), so benchmarking the two isolates the tower choice. Carries only the ops the comparison needs (eager mul, the deferred- reduction pair, square, inv), with an x86 pclmulqdq path plus a software fallback, and tests for the defining relation, field axioms, deferred- reduction linearity, and x86-vs-software agreement. field_bench grows a third column (C: F128Txy) and a C/B ratio that isolates the fold difference. On x86 the binius fold wins every mul-bound metric (C/B 0.58–0.85x); the deferred inner-product loop is the lone exception at ~1.13x, where per-term reduction is amortized away. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F128Txy shipped with only an x86 pclmulqdq path and a software fallback, so on aarch64 the field_bench head-to-head ran it in scalar software while the Artin-Schreier tower (F128T) used NEON PMULL — a ~60x apples-to-oranges gap that made the tower comparison meaningless on M-series. Add a NEON kernel that mirrors tower_f128's so only the degree-2 fold differs: 3-PMULL Karatsuba products, then each output limb reduced with the same two-PMULL tail. The binius fold y^2 = x*y + 1 folds one word fewer than the Artin-Schreier x^61 fold (a 128-bit p1<<1 vs a 192-bit fold), so mul_neon is 7 PMULL vs F128T's 8. Also route mul_unreduced and the deferred kreduce_u128 through NEON, and add a test that both NEON paths (fused mul_neon and mul_unreduced_neon + reduce) match the software reference over 10k inputs. With the fold now measured fairly on M4 Max (field_bench C/B): binius wins the throughput-bound muls ~0.87-0.92x and ties on latency/deferred — same direction as x86 (0.58-0.85x) but a smaller margin, since abundant PMULL throughput shrinks the one-word fold saving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix pcs.tex thm:mca-johnson against the primary source (BCHKS25 Thm 4.6, list correlated agreement): the theorem parameter is max(ceil(sqrt(rho)/eta), 3), not the ceil(sqrt(rho)/(2 eta)) of the non-list Thm 1.5 that Flock's Thm 8 quotes, and rho is the slightly reduced rate (k-1)/n. The implementation (johnson_m_param, reduced_rate) already had both right. In ligerito.rs, name misc/pcs.tex as the soundness source of truth and map thm:rbr's per-verifier-message error table onto the validate() checks (max semantics, per-transition 128 bits); replace dangling references to a former appendix. Remove the dead UDR regime (never constructed in production), making eta and expected_eps_ood_bits plain f64. Fix the OOD diagnostic doc (192, not 128) and the INITIAL_FOLDING_FATOR typo. Update doc.tex's PCS paragraph: per-level optimized eta (not 0.02) and 17-bit query grinding (queries close the remaining 111 bits). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The initial sumcheck is the dominant phase of the K-PCS open (~57% at log_n=24), and it allocs+faults two fresh ~100 MB F192 buffers (nf, nb) every fold round, then frees them — the allocator munmaps on drop, so every prove re-pays soft page faults on first touch. Route the fold outputs through the existing F192 scratch pool (primitives::scratch) via a new fold_out_buf helper, and give the consumed input buffers back in SumcheckProverK::fold, so repeated proves reuse resident pages instead of faulting fresh mappings. x86_64-gated to match flock (pooling here measured slower on aarch64, which keeps the fresh-alloc path). Correctness: F192 has no Drop and every fold slot is written before read, so recycled (stale) buffers are sound; all 55 pcs tests pass incl. the log_n 12/16/18 roundtrips. Measured (log_n=24, LIG_K_TRACE, steady state): initial sumcheck ~0.175 s -> ~0.063 s (~2.8x) total open ~0.31 s -> ~0.18 s (~1.7x) Ports the buffer-pooling idea from flock (github.com/succinctlabs/flock), commit "perf: add x86 AVX-512 prover acceleration" (the scratch-pool reuse for the initial-sumcheck fold). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TomWambsgans
force-pushed
the
small-proof
branch
from
July 18, 2026 12:51
70af811 to
d8dfe8b
Compare
Remove the Ligerito/Ligerito-K split and promote the F2^64-committed implementation to the canonical pcs::ligerito API. - move the shared security/configuration machinery into a private ligerito_config module and re-export it through pcs::ligerito - rename pack_k, ring_switch_k, stack_open_k, and tensor_algebra_k to their canonical module names - drop K suffixes from commitments, proofs, configs, ring-switch types, stack-opening types, helpers, and transcript aliases - migrate lean_vm, flock, recursive aggregation, throughput tests, guest comments, and protocol documentation to the canonical API - keep dense verifier and ring-switch reference paths only for tests, excluding unused helpers from production builds - remove stale compatibility terminology and the superseded extension-field implementation - preserve the existing NEON and AVX/AVX-512 optimized kernels unchanged Verified with: - cargo check --workspace --all-targets - cargo test --workspace --release - PCS_LOG_N=16 PCS_LOG_INV_RATE=1 PCS_SAMPLES=1 cargo test --release -p pcs --test pcs_throughput -- --ignored --nocapture - RAYON_NUM_THREADS=11 cargo run --release -- recursion --n 2 --log-inv-rate 1 - RAYON_NUM_THREADS=11 cargo run --release -- xmss --n-signatures 820 --log-inv-rate 1 - git diff --check
Introduce a VM instruction that reads two F_2^64 memory words and packs them canonically as one F_2^192 word with limbs (lo, hi, 0). Enforce source subfield membership and destination packing through literal-zero memory-bus coordinates, so the invariant is proof-enforced rather than only checked during witness generation. Wire the opcode through execution, traces, AIR, bytecode, layouts, transcript binding, table counts, statistics, and recursive-verifier shape handling. Preserve the existing BLAKE3 Transcript192 mode; no mixed packing mode is needed. Expose pack64x2(a, b) in zkDSL and use it in recursion.py to constrain every serialized Merkle-leaf limb before reassembly, hashing, and row-dot evaluation. Add end-to-end proving and invalid-extension-source tests, Python stubs, and VM/zkDSL documentation.
Remove the Transcript192 BLAKE3 mode and its alternate opcode, leaving one canonical 128+128-bit encoding throughout the VM. Shrink the BLAKE3 AIR table from 51 to 31 columns and reduce its local constraints to the five address bindings while the memory bus and Flock validity proof bind the canonical lanes. Add computed F192 limb advice and destination-target PACK64X2 support, including nested inline zkDSL helpers for transcript absorption, challenge reconstruction, and constrained conversion between canonical Merkle cells and Ligerito transcript words. Update recursion witness generation, documentation, and regression coverage accordingly.
Replace explicit challenge-power expansions across VM AIR constraints, recursive zkDSL verification, and quadratic sumchecks with Horner evaluation. Add shared native and zkDSL helpers for efficient F192 limb reconstruction, mirror every AIR formula on both sides, and update compiler documentation and examples. Include the repository-wide cargo fmt pass.
# Conflicts: # crates/flock/src/blake3.rs # crates/flock/tests/blake3_batch.rs # crates/lean_vm/src/blake3_flock.rs # crates/lean_vm/src/cpu/layout.rs # crates/lean_vm/src/cpu/mod.rs
Brings in main's standard-BLAKE3-chaining protocol (b94b315, b788b1c) ported onto this branch's 192-bit three-lane words, plus the SIMD-batched Merkle hashing, hierarchical tracing and pretty-print reconciliation (both originated here; main's ports superseded where they diverged), and the signers-cache encoding-predicate fingerprint.
The chaining port, adapted to canonical 128-bit chunks embedded in 192-bit cells: Op::Blake3 gains a chaining-value base offset (two consecutive cells, each read through the bus as (lo, hi, 0) so the canonical embedding is proof-enforced and the zero top limbs are never committed) and a 128-bit metadata immediate carried in the two low K-lanes of a 192-bit word whose lanes ride two public bytecode slots. The instruction encoding grows from six to nine public columns and the bus from nine to twelve coordinates; the stacked bytecode polynomial takes four selector bits. The BLAKE3 table adds the CV operand/address, two CV memory reads, and the sixth address binding; its eighteen value lanes are virtual, routed to the generalized flock witness (cv words 0..4, metadata words 18..20). The flock circuit takes cv/counter/blen/flags as free input rows with the re-baked family digest, and the branch's bilinear circuit walk is updated to match. vmhash::hash_slice, PCS Merkle leaves, xmss tweak hashes, and the program digest switch from the Merkle-Damgard length-in-IV construction to standard BLAKE3.
Guest updates: the recursion guest packs each queried leaf row into a contiguous cell run (level 0 packs two hinted F64 lanes per cell via PACK64X2; deeper levels pack the three serialized F192 limbs per word likewise, preserving the limb K-membership proof) and hashes it as chunk-chained standard BLAKE3 through per-level LIG_LEAF_BLOCKS (level 0: n/8 blocks of 8-byte lanes; deeper: 3n/8 blocks of 24-byte words); the BLAKE3 zerocheck branch grows to six Horner-batched address bindings. The xmss guest uses cv-chained sub-hashes (146 compressions per signature, down from 158). The compiler keeps pack64x2/pack64x2_into/hint_f192_limbs alongside the new blake3 keyword surface and lazy per-scope IV cells (SET from IV_CELLS, registered in the const-cell cache).
Kept from this branch where main's ports lagged: the log-inv-rate CLI and per-proof PCS rates, the F64 Ligerito implementation (main's tracing landed in deleted code), the prepared-reduction-witness plumbing, and the per-64-bit-word WOTS encoding.
Full workspace suite passes, including both recursion 2-to-1 tests (mixed rates included), xmss aggregation, and main's chaining regressions adapted to three-lane words. The ignored recursion_soundness_binds test fails identically on the pre-merge HEAD (a pre-existing breakage on this branch, not a merge regression).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge branch 'main' into small-proof
# Conflicts:
# crates/flock/src/blake3.rs
# crates/flock/tests/blake3_batch.rs
# crates/lean_vm/src/blake3_flock.rs
# crates/lean_vm/src/cpu/layout.rs
# crates/lean_vm/src/cpu/mod.rs
flock: reuse prepared reduction witness
Retain the fused A·z, B·z, and lincheck layouts created with q_pkd and consume them after commitment instead of regenerating the witness. Add env-gated reduction phase timing and exercise the prepared path in the batch harness.
Support StackBuf returns from normal functions
Use Horner evaluation for challenge polynomials
Replace explicit challenge-power expansions across VM AIR constraints, recursive zkDSL verification, and quadratic sumchecks with Horner evaluation. Add shared native and zkDSL helpers for efficient F192 limb reconstruction, mirror every AIR formula on both sides, and update compiler documentation and examples. Include the repository-wide cargo fmt pass.
Simplify recursion hashing to canonical BLAKE3 cells
Remove the Transcript192 BLAKE3 mode and its alternate opcode, leaving one canonical 128+128-bit encoding throughout the VM. Shrink the BLAKE3 AIR table from 51 to 31 columns and reduce its local constraints to the five address bindings while the memory bus and Flock validity proof bind the canonical lanes.
Add computed F192 limb advice and destination-target PACK64X2 support, including nested inline zkDSL helpers for transcript absorption, challenge reconstruction, and constrained conversion between canonical Merkle cells and Ligerito transcript words. Update recursion witness generation, documentation, and regression coverage accordingly.
Add PACK64X2 base-field packing instruction
Introduce a VM instruction that reads two F_2^64 memory words and packs them canonically as one F_2^192 word with limbs (lo, hi, 0). Enforce source subfield membership and destination packing through literal-zero memory-bus coordinates, so the invariant is proof-enforced rather than only checked during witness generation.
Wire the opcode through execution, traces, AIR, bytecode, layouts, transcript binding, table counts, statistics, and recursive-verifier shape handling. Preserve the existing BLAKE3 Transcript192 mode; no mixed packing mode is needed.
Expose pack64x2(a, b) in zkDSL and use it in recursion.py to constrain every serialized Merkle-leaf limb before reassembly, hashing, and row-dot evaluation. Add end-to-end proving and invalid-extension-source tests, Python stubs, and VM/zkDSL documentation.
Document MCA error in unique-decoding regime
Pretty-print displayed integers with commas
pcs: make F64 Ligerito the sole implementation
Remove the Ligerito/Ligerito-K split and promote the F2^64-committed implementation to the canonical pcs::ligerito API.
- move the shared security/configuration machinery into a private ligerito_config module and re-export it through pcs::ligerito
- rename pack_k, ring_switch_k, stack_open_k, and tensor_algebra_k to their canonical module names
- drop K suffixes from commitments, proofs, configs, ring-switch types, stack-opening types, helpers, and transcript aliases
- migrate lean_vm, flock, recursive aggregation, throughput tests, guest comments, and protocol documentation to the canonical API
- keep dense verifier and ring-switch reference paths only for tests, excluding unused helpers from production builds
- remove stale compatibility terminology and the superseded extension-field implementation
- preserve the existing NEON and AVX/AVX-512 optimized kernels unchanged
Verified with:
- cargo check --workspace --all-targets
- cargo test --workspace --release
- PCS_LOG_N=16 PCS_LOG_INV_RATE=1 PCS_SAMPLES=1 cargo test --release -p pcs --test pcs_throughput -- --ignored --nocapture
- RAYON_NUM_THREADS=11 cargo run --release -- recursion --n 2 --log-inv-rate 1
- RAYON_NUM_THREADS=11 cargo run --release -- xmss --n-signatures 820 --log-inv-rate 1
- git diff --check
pcs: vectorize F192 NTT with AVX-512
improve hierarchical PCS tracing
primitives: add AVX-512VBMI bit transpose
enable tracing
remove 128 bits fields
make tests pass
add pcs_throughput benchmark
pcs/ligerito_k: pool the initial-sumcheck fold buffers (x86_64)
The initial sumcheck is the dominant phase of the K-PCS open (~57% at
log_n=24), and it allocs+faults two fresh ~100 MB F192 buffers (nf, nb)
every fold round, then frees them — the allocator munmaps on drop, so
every prove re-pays soft page faults on first touch.
Route the fold outputs through the existing F192 scratch pool
(primitives::scratch) via a new fold_out_buf helper, and give the
consumed input buffers back in SumcheckProverK::fold, so repeated proves
reuse resident pages instead of faulting fresh mappings. x86_64-gated to
match flock (pooling here measured slower on aarch64, which keeps the
fresh-alloc path). Correctness: F192 has no Drop and every fold slot is
written before read, so recycled (stale) buffers are sound; all 55 pcs
tests pass incl. the log_n 12/16/18 roundtrips.
Measured (log_n=24, LIG_K_TRACE, steady state):
initial sumcheck ~0.175 s -> ~0.063 s (~2.8x)
total open ~0.31 s -> ~0.18 s (~1.7x)
Ports the buffer-pooling idea from flock (github.com/succinctlabs/flock),
commit "perf: add x86 AVX-512 prover acceleration" (the scratch-pool
reuse for the initial-sumcheck fold).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align Ligerito RBR soundness doc and implementation
Fix pcs.tex thm:mca-johnson against the primary source (BCHKS25 Thm 4.6,
list correlated agreement): the theorem parameter is max(ceil(sqrt(rho)/eta), 3),
not the ceil(sqrt(rho)/(2 eta)) of the non-list Thm 1.5 that Flock's Thm 8
quotes, and rho is the slightly reduced rate (k-1)/n. The implementation
(johnson_m_param, reduced_rate) already had both right.
In ligerito.rs, name misc/pcs.tex as the soundness source of truth and map
thm:rbr's per-verifier-message error table onto the validate() checks
(max semantics, per-transition 128 bits); replace dangling references to a
former appendix. Remove the dead UDR regime (never constructed in
production), making eta and expected_eps_ood_bits plain f64. Fix the OOD
diagnostic doc (192, not 128) and the INITIAL_FOLDING_FATOR typo.
Update doc.tex's PCS paragraph: per-level optimized eta (not 0.02) and
17-bit query grinding (queries close the remaining 111 bits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge branch 'main' into small-proof
pcs.tex (a note on WHIR/Ligerito over binary fields, interpreted as List binding PCS
Use query grinding to reduce proof size
Optimize Johnson eta per recursive level
Use aggressive recursive RS rate schedule
Audit and tighten Ligerito soundness parameters
Correct the BCHKS25 Johnson proximity-gap parameter to use ceil(sqrt(rho) / eta), and evaluate the theorem at the reduced RS rate (K - 1) / n instead of the nominal encoding rate.
Move the global Johnson slack from 0.02 to 1/160, retaining more than 128.8 bits of proximity-gap soundness without grinding while reducing query counts and proof size.
Validate exact, unrounded query, proximity, OOD, list-unioned algebraic, and degree-191 ring-switch errors. Clarify the round-by-round security claim and add production-profile regression tests for all supported rates and witness sizes.
Make Ligerito rate proof-local and recursion-generic
Pass log_inv_rate explicitly through proving instead of process-global state, announce and validate it through Fiat-Shamir alongside layout sizes, and derive commitment/opening parameters from the transcript-bound value.
Embed all supported rate/size configurations in the recursion guest so one program can verify mixed-rate inner proofs, while letting the CLI-selected rate independently configure both inner and outer proofs. Add CLI validation, the environment-driven query-count report, mixed-rate regression coverage, and update the README.
Move Ligerito to 128-bit Johnson list decoding
Raise the global round-by-round soundness target to 128 bits and derive the production Ligerito ladder in the Johnson list-decoding regime over GF192 with eta 0.02.
Use the post-commit L0 opening claim as its implicit binding evaluation, and add one explicit OOD evaluation for each recursive commitment. Port those OOD claims through the GF64-committed prover, dense and succinct native verifiers, proof encoding, recursion hint builder, and in-VM recursion verifier.
The GF192 margin makes the query, proximity-gap, and OOD-binding terms each clear 128 bits without query or fold grinding. Add parameter assertions and OOD tamper coverage, and update the security documentation.
Validated with all PCS, Flock, LeanVM, and recursion release tests, recursion --n 2, and xmss --n-signatures 820. The recursion proof drops from 579.1 KiB to 518.3 KiB and guest cycles from 2,842,984 to 2,591,048.
Optimize GF192 ring switching in recursion
Factor the GF192 trace-dual basis over its GF64 degree-three tower, replacing the dense 192-by-192 Frobenius table with smaller base and tower tables. Assert the factorization while constructing guest placeholders.
Evaluate the two transposed claims and their transparent ring-switch weights in lockstep so they share coefficients, powers, and verifier-weight loads.
This reduces the recursion --n 2 guest from 3,235,330 to 2,842,984 cycles (12.1%), the proof from 610.7 to 579.1 KiB, and measured proving time from 13.50s to 9.22s.
Merge field-exploration into small-proof
Bring over native BLAKE3 circuit-walk verification, compiled recursion-guest caching, and clarified division-by-zero semantics. Adapt the verifier paths and cache key to GF192. Deliberately omit the bus-grinding changes from 97d9c687cd01fbec9987172f56abd810fa2299d7.
doc
recursion: cache the compiled guest across verifies
RecursiveProof::verify recompiled the recursion guest on every call
(~235 ms), dwarfing the ~20 ms of actual proof checking. The guest is a
pure function of the inner bytecode and the batch size, so
recursion_guest_arc caches compiled guests process-wide, keyed by
(fs_seed(inner_program), nsub) and capped at 8 shapes, compiling
outside the lock (mirrors blake3_flock::setup_for). verify borrows the
pristine cached Arc; prover paths clone an owned copy for their witness
streams (cpu::Program now derives Clone).
Complete recursive verify: 268 ms -> 18.7 ms once a shape is warm, now
dominated by the Ligerito opening.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native verifier evaluates A0/B0 by circuit walking
The native verification of a recursive proof paid the ~21M substituted
nonzeros of the BLAKE3 matrices twice: check_reduced contracted two eq
tables through them to discharge the deferred v_a/v_b claims (~11 ms,
forcing the ~300 ms matrices build), and lincheck::verify materialized
the alpha-batched column marginal via the CSC fold (~1.2 ms + one-time
22 ms transpose).
bilinear_walk_pair (flock.pdf, Circuit walking) computes u^T A0 w and
u^T B0 w in O(circuit) field ops (~43 us) by walking the unsubstituted
compression cascade over F128T wire values, mirroring build_matrices
row for row; a randomized cross-check test ties it to the materialized
matrices. check_reduced now walks. lincheck::verify no longer folds
comb_vec through the rounds: the final consistency check collapses to
one bilinear form against the rank-1 column weight
w_col[c] = z_partial[c_skip]*eq(r_inner_rest, c_rest), answered by the
new LincheckCircuit::bilinear_form (WalkLincheckCircuit; default None
falls back to the marginal + inner product). Transcript unchanged.
Proof checking drops ~31 ms -> ~20 ms per recursive verify.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document the GF192 machine field
Update the VM and proving-system specification from the old quadratic GF128 representation to the implemented cubic tower E = GF64[y]/(y^3 + y + 1). Describe three-limb memory words, arithmetic, immediates, memory interactions, instruction tables, and public-input openings.
Clarify that BLAKE3 uses canonical 128-bit values embedded in full GF192 cells with a zero top limb, and record that the larger challenge field eliminates grand-product grinding under the instance caps. Also distinguish the zkDSL's 128-bit literal syntax from its 192-bit machine-word type.
Use univariate batching for GF192 ring switching
Replace the padded eight-challenge coordinate cube with powers of one GF192 Fiat-Shamir challenge. Share that challenge across stacked ring-switch claims and retain the existing GF64 transpose and GF192 transparent-weight evaluation paths.
In the recursion guest, compute the trace-dual linearized coefficients directly with Horner evaluation. This removes the equality tree and coordinate-weight buffer, reducing the recursive verifier by 1,510 instructions and 76,746 VM steps relative to the padded baseline while retaining better than 184 bits of batching soundness.
feat: migrate recursion and PCS to F192
Move the protocol challenge and machine-word field to the cubic F192 tower while retaining F64 physical columns and canonical 128-bit BLAKE3 cells.
- add portable, x86, and AArch64 F192 arithmetic with deferred-reduction kernels
- carry all three limbs through the compiler, ISA, trace tables, Flock reductions, transcript, GKR, and public-input binding
- generalize K-packed Ligerito, ring switching, tensor algebra, query sampling, and Merkle serialization to 192-bit challenges and folded words
- update the recursion guest AIR, claim descriptors, memory binding, packed queries, and Merkle verification, including the rho-point fix for virtual BLAKE claims
- remove grand-product bus grinding under the F192 soundness budget and account for the actual 2^192 Ligerito challenge field
- refresh DSL documentation and tests, and keep the large BLAKE3 batch benchmark as an explicit release-only benchmark
Clarify division by zero semantics
Fix bus grinding soundness bound
pcs: batch F64 NTT butterflies with NEON
docs: align field architecture documentation
pcs: batch F64 NTT butterflies with VPCLMUL
pcs: optimize tower-field stacked openings
Merge branch 'main' into field-exploration
pcs: avoid redundant basis initialization work
cpu: transmit the PI low lane so the guest skips Frobenius extraction
The public-input binding needs both K-lanes of MEM(r) = interp(pi, r) as opening
claims. The lane values are deterministic from the seeded pi, but a recursive
verifier can't cheaply slice F64 lanes out of the E-valued pi — the old guest
recovered them with two 64-squaring Frobenius loops. Instead, transmit the low
lane v_lo = MEM_LO(r) (one scalar) and deduce the high lane
v_hi = (MEM(r) + v_lo)*Y^-1 from the pi-derived E-value. The guest replays it as
one scalar read + one multiply — no Frobenius, no X_INV.
This nets a simplification: bind_pi_claim is a single helper returning
[ColumnClaim; 2] (reusing multilinear::interp), the transcript add_scalar/
next_scalar is inline at each call site, and the now-dead X_INV constant + its
Frobenius baking are deleted (guest and harness). Transcript and claim order are
unchanged from the two-lane binding, so it stays sound.
Validated in release: cpu 8/8; recursion_2to1, _mixed, _soundness_binds
(tamper-rejection), _generic_many all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
flock: restore standalone BLAKE3 batch benchmark
Merge branch 'tower-only-flock' into field-exploration
field: remove GHASH and use tower field throughout
unify Ligerito protocol configuration
sponge: carry the domain-separation tag in lane 2 (main convention)
The v2 sponge bound the DS tag in the 4th compress lane, which forced the
recursion guest to encode its tags as `k << 64` (the tag in the F128T high
lane). Move the tag to the 3rd lane (right after the up-to-two data words) so
the guest's tags are plain `1,2,3,4,5` again, matching main's convention. The
tag lane is a pure convention (all five values stay distinct and nonzero, so
domain separation is unchanged); byte-absorb blocks now carry two data words
(16-byte chunks) instead of three, since the tag takes the third lane.
Validated end-to-end in release: cpu 8, pcs 103, flock 77, recursion_2to1 +
mixed, and recursion_soundness_binds (tamper-rejection) all pass — prover,
verifier, and the in-VM guest replay agree on the new transcript.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
recursion: remove the `recursion` feature gate (harness is ported)
The gate + `unimplemented!` stub existed only because the recursion harness
was unported to the 64-bit machine; now that it works end-to-end, drop the
feature so the `recursion` module and the real `run_recursion` are always
compiled. `cargo run --release -- recursion --n 2` runs the aggregation with
no extra flags (verified: 2 inner proofs aggregate and the recursive proof
verifies). Also removes the now-dead `uk` base-field encoder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
recursion: port the N->1 aggregation to the 64-bit tower protocol
Make the in-VM recursion guest verify N->1 recursive proofs on the 64-bit
(K=F64, extension F128T) protocol. Each fix addressed a constant or convention
that coincided under the old GHASH / 128-bit packing (LOG_PACKING=7) but
diverges in the tower with 64-bit packing (LOG_PACKING_K=6):
- tower ring-switch trace-dual basis, derived by the same construction over
the tower's coordinate basis and verified against the duality property
- Fiat-Shamir domain-separation tag in compress lane 3 (the tower sponge)
- bit-decomposition reconstruction in the real F128T coordinate basis; the
field generator's powers span only F64, so g^i is not the coordinate basis
- two-lane 128-bit memory public-input claims (MEM_LO/MEM_HI) via Frobenius
lane extraction of the bound public-input words
- AIR constraint formulas re-ported for the LO/HI memory split
- K-opener transcript realigned to its F128 reference that the guest replays:
ordered d-bit-chunk query sampler + surfaced query squeezes, scalar Merkle
root binding, 64-wide s_hat_v, struct-based Ligerito observed-scalar replay,
and the SLOT_STRIDE_LOG = K_LOG - LOG_PACKING_K terminal identity
Validated: recursion_2to1 and recursion_2to1_mixed verify honest recursive
proofs; recursion_soundness_binds rejects every tampered hint (the in-VM
verifier genuinely binds); pcs 103, cpu 8. No check was weakened or stubbed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
flock: migrate the verifier to the binius tower (F128T)
Move flock's verifier (zerocheck + lincheck + the blake3 reduction) to the
binius tower field F128T, so a downstream in-VM verifier replays a single
field instead of the two-field GHASH/tower bridge.
The fast GHASH prover is unchanged (the shift_reduce univariate-skip
optimization stays). It now bridges its transcript messages and claims through
the real ghash<->tower isomorphism instead of the bit-copy reinterpretation,
so its outputs are genuine tower values the F128T verifier checks. The
blake3_flock boundary is native tower (no isomorphism on point/value/weights);
only the prover-only s_hat_v capture still crosses.
Adds the tower phi8 embedding (iso-image of the GHASH basis, exhaustive
homomorphism test, images land in F64) and a tower lagrange-weight helper.
Validated: 77 flock tests and 8 cpu prove/verify + tamper-rejection tests
pass; workspace builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
lean_vm: drop the DEREF pointer's always-zero HI lane (K-valued address)
The DEREF pointer is constrained to K by `a2 = p·obe` (a2 is a single-lane K
address), so its high lane is provably 0. Commit it as a single K lane
(deref::P) instead of P_LO/P_HI, with the memory read carrying a literal 0 high
lane via the new FlushBuilder::memory_k. The bus balances only if the stored
value's HI lane is also 0, which the K constraint guarantees — sound; a
debug_assert pins p.c1 == 0 at fill time.
Removes one committed column of zeros from the most frequent instruction's
table (DEREF, 571/XMSS): committed witness 2^26.180 -> 2^26.170 (~0.7%).
Branch-specific (only the 128-bit-memory-word design gives an address a HI
lane); no main equivalent. All prove/verify tests pass.
Not applied to the JUMP d/f destination words: they are only forced into K on
taken branches (b=1); a not-taken branch reads an unconstrained memory word, so
forcing HI=0 would reject valid traces.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge commit 'b956c4d' into field-exploration
# Conflicts:
# crates/flock/src/zerocheck.rs
Merge commit 'b7e1be2' into field-exploration
# Conflicts:
# crates/flock/tests/blake3_batch.rs
# crates/lean_vm/src/pcs.rs
# crates/pcs/src/ligerito.rs
Merge commit '9e957fc' into field-exploration
# Conflicts:
# crates/fiat_shamir/src/sponge.rs
# crates/pcs/src/merkle.rs
Merge commit '2905de0' into field-exploration
# Conflicts:
# crates/lean_vm/src/leaf.rs
Merge commit '7b74709' into field-exploration
# Conflicts:
# crates/primitives/src/multilinear.rs
Merge commit '086f622' into field-exploration
# Conflicts:
# crates/lean_vm/src/pcs.rs
Merge commit 'b336897' into field-exploration
# Conflicts:
# crates/lean_vm/src/cpu/layout.rs
Merge commit '11e799f' into field-exploration
# Conflicts:
# crates/lean_compiler/src/lower.rs
Merge commit 'a825bf6' into field-exploration
# Conflicts:
# crates/flock/src/zerocheck.rs
# crates/lean_vm/src/tables.rs
Merge commit 'bd8b31b' into field-exploration
Merge commit '53077e5' into field-exploration
field: switch the VM's extension field E to the binius64 tower
Make the binius64 tower K[y]/(y^2 + x*y + 1) the shipped degree-2 extension
field E, replacing the Artin-Schreier tower K[y]/(y^2 + y + x^61). Same base
field K = F64 and identical repr(C){c0,c1} layout, so the ~1,200 call sites are
untouched: F128T now names the binius tower (tower_f128.rs, formerly the
tower_f128_xy investigation variant), and the old Artin-Schreier tower is kept
as F128TArtin (tower_f128_artin.rs) for the field benchmarks only.
- Build binius up to full field parity before the swap: From<F64>, AddAssign,
the mixed K x E path (mul_base / mul_base_unreduced / a BaseUnreduced
accumulator, all tower-independent), Serialize/Deserialize/Hash, the Y const.
- Re-derive the GHASH <-> tower isomorphism (iso_f128.rs) for y^2 = x*y + 1.
The K-embedding is tower-independent (X unchanged), so only the Y columns
move; the j=0 block is byte-identical to the old matrices. gen_iso_xy.rs
regenerates and verifies them (100k random multiplicativity + round-trips),
replacing the lost scratchpad/iso.py.
Correctness verified on aarch64: the whole workspace builds and every test
passes -- pcs prover/verifier round-trips, lean_vm (VM, GKR, constraints, the
leaf mixed K x E path), and blake3_flock (which maps claims through the
re-derived iso at the flock boundary), plus all integration tests.
This is the correctness migration only; the batched fold-kernel speedup that
motivated it lands separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
remove superseded zerocheck kernel
field benches: wire the VPCLMULQDQ batched inner product
field_bench: new 'inner prod, VPCLMUL' row (x86 vpclmulqdq+avx512f only) running
the batched Karatsuba kernel (B=2) for F128T and F128Txy — ~5.5-6.9x the scalar
'deferred' row (0.37 vs 2.3-2.6 ns/term), and it collapses the tower gap from
C/B 1.12x (scalar) to 1.00x: wide CLMUL erases binius's deferred-inner-product
deficit without flipping it. Banner now reports the VPCLMULQDQ backend.
inner_bench: the noise-robust comparator is now generic over the field, with an
x86_64 backend ranking scalar-deferred vs vpclmul kara/school at bank counts
1/2/4 for both towers, plus a best-of-each head-to-head. Findings on Zen4:
Karatsuba beats schoolbook (0.367 vs 0.393 ns/term) — the reverse of M-series,
where PMULL is cheap enough that schoolbook's extra product buys dropping the
pre-XOR; on x86 CLMUL throughput is the bound, so the extra CLMUL loses. B=2
wins the bank sweep. Confirms the schoolbook win was Apple-specific.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
field: AVX-512 VPCLMULQDQ batched inner-product kernels for both towers
Adds field/vpclmul.rs, a shared AVX-512 batched Karatsuba accumulator: one
VPCLMULQDQ does four independent 64x64->128 carry-less products (one per zmm
lane), folding four field elements per CLMUL. Two variants — karatsuba_acc
(3 CLMUL + shuffle/XOR pre-mix) and schoolbook_acc (4 CLMUL, no pre-mix) —
both return the same (Sum p0, Sum p1, Sum pm) triple with a const-generic bank
count for ILP.
Both degree-2 towers gain inner_unreduced_vpclmul_{kara,school}<B> in their
x86_64 modules. F128T and F128Txy are repr(C){c0,c1} with identical layout, so
the accumulate is shared; each kernel only supplies its field-specific reduce,
reusing the already-tested Unreduced::reduce(). Only the batched accumulate is
new arithmetic.
Tests check the batched kernels against the scalar deferred reference across 18
lengths (including partial vector groups and the <4 scalar tail), both kernels,
bank counts 1/2/4, on both fields.
Gated on target_feature vpclmulqdq + avx512f (Zen4 / Sapphire-Rapids-class);
-C target-cpu=native enables both. The CLMUL stays 128-bit per lane — this
batches four, it does not widen the field multiply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
field: schoolbook batched inner-product NEON kernel + noise-robust comparator
inner_unreduced_neon (the deferred sum_i a_i*b_i hot-loop kernel) was
Karatsuba, 3 PMULL/term. On M4 Max PMULL is not the bottleneck, so dropping
the per-term am/bm Karatsuba pre-XOR (2 DUP + 2 EOR) in favour of one extra
PMULL wins: schoolbook products run ~1.25x faster. inner_unreduced_neon now
dispatches to inner_unreduced_school::<2>; inner_unreduced_kara is retained as
the comparison alternate. field_bench's "inner prod, NEON kernel" row: the
tower kernel drops 0.58 -> 0.47 ns/op.
The win was isolated with a noise-robust comparator (bin/inner_bench): it
interleaves every method round-robin (rotating order) and ranks by the MINIMUM
time across many rounds -- noise only ever adds time, so the min is the
cleanest estimate of true speed -- with a per-round win-count as corroboration.
That methodology overturned two single-pass "wins" as pure measurement noise:
accumulator banking (x1 == x2 == x4) and slice bounds-check removal both do
nothing here. Only the schoolbook-vs-Karatsuba product choice moves the number.
Also make the field_bench CLMUL-backend banner architecture-aware so it no
longer prints "numbers not meaningful" on x86 (which has a real pclmulqdq
path), and notes that path is 128-bit CLMUL, not VPCLMULQDQ.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
field: aarch64 NEON path for the binius64 tower (F128Txy)
F128Txy shipped with only an x86 pclmulqdq path and a software fallback, so
on aarch64 the field_bench head-to-head ran it in scalar software while the
Artin-Schreier tower (F128T) used NEON PMULL — a ~60x apples-to-oranges gap
that made the tower comparison meaningless on M-series.
Add a NEON kernel that mirrors tower_f128's so only the degree-2 fold
differs: 3-PMULL Karatsuba products, then each output limb reduced with the
same two-PMULL tail. The binius fold y^2 = x*y + 1 folds one word fewer than
the Artin-Schreier x^61 fold (a 128-bit p1<<1 vs a 192-bit fold), so mul_neon
is 7 PMULL vs F128T's 8. Also route mul_unreduced and the deferred
kreduce_u128 through NEON, and add a test that both NEON paths (fused mul_neon
and mul_unreduced_neon + reduce) match the software reference over 10k inputs.
With the fold now measured fairly on M4 Max (field_bench C/B): binius wins the
throughput-bound muls ~0.87-0.92x and ties on latency/deferred — same
direction as x86 (0.58-0.85x) but a smaller margin, since abundant PMULL
throughput shrinks the one-word fold saving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
centralize Merkle root encoding
centralize F128 byte encoding
field: add binius64 degree-2 tower and bench it vs the Artin-Schreier tower
Adds F128Txy, a GF((2^64)^2) tower K[y]/(y²+x·y+1) — binius64's tower
choice — over the same base field K as tower_f128's Artin-Schreier
K[y]/(y²+y+x^61). Same 3-CLMUL Karatsuba products; only the degree-2
fold differs (a shift-by-x vs a multiply by x^61), so benchmarking the
two isolates the tower choice.
Carries only the ops the comparison needs (eager mul, the deferred-
reduction pair, square, inv), with an x86 pclmulqdq path plus a software
fallback, and tests for the defining relation, field axioms, deferred-
reduction linearity, and x86-vs-software agreement.
field_bench grows a third column (C: F128Txy) and a C/B ratio that
isolates the fold difference. On x86 the binius fold wins every mul-bound
metric (C/B 0.58–0.85x); the deferred inner-product loop is the lone
exception at ~1.13x, where per-term reduction is amortized away.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
simplify bus claim decomposition
share multilinear field helpers
naming
derive column sizes from layout sources
wip
field: add x86_64 pclmulqdq path for GF(2^64) and the F128T tower
F128 already had an x86_64 pclmulqdq path, but the tower field F128T and its
F64 base had only an aarch64 PMULL kernel plus a naive software fallback, so on
x86 every tower multiply ran software (~27 ns) while F128 used hardware clmul
(~2.4 ns). Since the prover runs its whole polynomial arithmetic over F128T,
that 11x per-mul gap made XMSS aggregation prove ~3x slower on x86 than on
main, despite identical VM work (same cycles, instruction mix, and memory).
Mirror the aarch64 kernels with x86_64 pclmulqdq:
- gf2_64: one CLMUL product + a two-CLMUL fold by 0x1B (= x^64 mod P).
- tower_f128: 3-CLMUL Karatsuba over the scalar coefficients, then the tower
reduction c0 = reduce(p0) + x^61·reduce(p1), c1 = reduce(pm + p0); plus the
deferred-reduction kernels (mul_unreduced/reduce, mul_base_unreduced/reduce)
the sumcheck hot loop uses. Wired behind
cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq")); aarch64 and
the software fallback are unchanged.
Verified against the software oracle (new x86_variants_match_software test,
10k random cases: mul, square, and both deferred paths) and the full workspace
suite (304 tests). On this AMD box, xmss --n-signatures 820 goes 53.6 -> 136.3
XMSS/s (proving 15.3s -> 6.0s); the deferred inner product goes from 11.7x to
1.10x of F128.
Credit: binius64 <https://github.com/binius-zk/binius64> for the GF(2^64)
base-field CLMUL and the deferred-reduction structure (its Monbijou field is
this same GF(2^64)); the degree-2 extension differs (ours is Artin-Schreier
y^2+y+x^61, not their y^2+xy+1), so the extension reduction follows our field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reuse compiler constant pool
simplify challenge and table construction
deduplicate deterministic test rngs
generate phi8 table from basis images
Merge branch 'main' into field-exploration
Brings in the signers-cache footprint fix, main's "unify equality table
construction" and "make recursive proof verification atomic" refactors.
Conflict resolutions (11 hunks, 5 files):
- signers_cache.rs: clean merge (the fix originated on both sides identically).
- multilinear.rs, constraints.rs, gkr.rs, pcs/ligerito.rs: kept the
field-exploration (dual-lane) versions. main's changes here were purely the
eq_table -> build_eq rename from "unify equality table construction", which
merged the base's two eq-table constructors into one. That unification is
invalid under the dual-lane design: here eq_table (over F128T, the VM
constraint lane) and build_eq (over F128, the PCS lane) are DISTINCT
functions for different field types, not duplicates. Taking main's rename
would also have dropped ligerito's `crate::as_ghash` conversions that map
tower-field samples into F128 for the PCS, breaking the K PCS path.
- rec_aggregation/lib.rs: took main's new atomic-verification exports
(RecursiveProof, RecursiveVerifyError, run_recursion) but kept them under
field-exploration's `#[cfg(feature = "recursion")]` gate, since the n->1
recursion harness still awaits the 64-bit guest port. recursion.rs itself
auto-merged to main's version (compiled only with the feature).
Verified: `cargo build --release` (default features) and `cargo test
--workspace` both pass (pcs 98, primitives 50, lean_vm 16, flock 74,
rec_aggregation 3, xmss 7; no failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
signers cache: fold hash-construction fingerprint into the footprint
The signer cache filename hashed only the declared XMSS parameters (slot,
key range, message, V/W/CHAIN_LENGTH/...). Those are identical across
branches that change the hash *construction* underneath — e.g. the
Merkle-Damgard IV switching field width (GF(2^128) <-> GF(2^64)) rewrites
every digest while touching no constant. Both branches then computed the
same filename and the serialized sizes match, so a run on one branch loaded
the other's cryptographically-invalid signers and panicked in verification.
(cargo clean masked it by wiping the poisoned file.)
Fold a known-answer test vector of the real primitives (one tweak_hash, one
md_tweak_hash) into footprint(), so any change to digest behavior lands in a
distinct cache file. Per-branch caches now coexist instead of clobbering one
another. Two BLAKE3 compressions; negligible next to generating a signer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unify equality table construction
make recursive proof verification atomic
vm: 128-bit memory words (E) with 64-bit addressing (K)
Memory words become 128-bit elements of the tower E = GF(2^128) so a
recursion guest can do native 128-bit field arithmetic, while addresses,
the pc/fp, read counters, operands, opcodes, and domain separators stay in
the 64-bit subfield K = GF(2^64) (g-powers of order 2^64-1). This keeps the
branch's base-field-commit / extension-open PCS win: only data costs 128
bits, addressing stays 64.
Representation & protocol:
- Each 128-bit word is committed as its two K-lanes (_lo/_hi), two ordinary
F64 columns in the one stacked K-witness; the PCS/stacking/Merkle layer is
untouched. The E-value is reassembled (lo + hi*y) only in the constraint
zerocheck and at the opening point, never in the commitment.
- MUL_NATIVE = the E tower product, XOR = 128-bit xor, both written as
degree-2 E-relations on the value lanes, so the zerocheck is unchanged.
- On the bus a memory value rides two K-coordinates (widest tuple stays 9);
the memory image is two lanes; the public-input binding is one claim per
lane.
- Words used as addresses (DEREF pointer, JUMP destination/frame) are forced
into K by the single-lane address/selection constraints, and witness-gen
errors if such a word has a nonzero high lane.
- BLAKE3: a 128-bit chunk is one cell, so a row reads six cells (four inputs,
two-cell digest); the 12 value lanes map 1:1 onto the same 12 flock q_pkd
slots (flock packing unchanged).
DSL:
- 128-bit literals: an integer literal is the raw 128-bit bit pattern
(low 64 = K-lane, high 64 = y-lane); 2**64 is the tower generator y, so a
guest packs two lanes as `lo + hi * (2**64)`. KVal::Const, lit_field,
parse_const, try_field_const, const_cell, Alias::Const, and fconsts migrate
F64 -> F128T; g-power/address constants embed via .into().
- blake3 lowering: a 256-bit operand is two 128-bit cells (chunk = one cell,
digest = two cells).
Guests: all hashing guests rewritten to the 2-cell layout and revalidated
(6 blake3 .py programs, the stack_buf suite, the hash_chain benchmark, and
the xmss aggregation guest + driver — WORDS_PER_VALUE 2->1, WORDS_PER_BLOCK
4->2, the WOTS encoding reconstructed into one cell via the y literal). The
`recursion` harness stays feature-gated off (awaits its own guest port).
Docs: doc.tex, transition-to-64-bits.tex, and zkDSL.md updated to the hybrid
design.
Full workspace test suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge branch 'main' into field-exploration
Reconcile main's 8-crate workspace restructure + one-shared-transcript
protocol with this branch's 64-bit field transition (K = GF(2^64) machine
words opened over the tower E = GF(2^128)).
Structural: adopt main's `crates/*` layout and `fiat_shamir` crate; the
K-variant PCS files (ligerito_k, ring_switch_k, stack_open_k, pack_k,
tensor_algebra_k, additive_ntt_f64) and the tower field modules move under
crates/{pcs,primitives}. Hint machinery (RHint, GPowMap, grow_gpow) lives in
lean_vm::cpu::hints. Bench binaries move into their owning crates.
Field/transcript: the shared Fiat-Shamir sponge is F64-word / F128T-scalar;
the GHASH-typed F128 consumers (flock zerocheck/lincheck, the F128 Ligerito
twin) ferry through raw lane lifts (pcs::as_e / as_ghash) at their transcript
touchpoints - no arithmetic crosses representations. flock's reduction and
the F128 Ligerito are generic over the opening type so lean_vm can drive them
with its stacked Ligerito-K proof on one transcript.
Protocol: keep main's RLC-batched three-tree bus GKR, dedup'd claim
decomposition, and single-seed statement binding, retyped to E; keep the
branch's K-committed stacked opening as the sole PCS path (the F128 stacked
opener stays for its own tests).
Deferred: the n->1 recursion harness (crates/rec_aggregation/src/recursion.rs
+ guests/recursion.py) is gated behind the `recursion` feature - it dissects
the proof stream word-for-word and needs porting to the 64-bit-word layout.
Tests: workspace builds warning-free; primitives/fiat_shamir/pcs/flock/
lean_vm/lean_compiler/xmss and the XMSS-aggregation + fibonacci harnesses all
pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recursion guest: runtime-sized buffers replace the linear capacity caps
HeapBuf sizes carried in the exponent (HeapBufDyn: the alloc hint does an
O(1) reverse g-power lookup at witness generation, erroring past 2^20
cells) already existed - the guest now uses them wherever the exact size
is a certified runtime g-power at declaration time:
- zerocheck_r / flock_point chains: exactly m = K_LOG + tau_5 slots
(mr1cs_g); zerocheck_rhos / flock_round chains: exactly m - 6
(mr1cs_g * INV_GEN^6). R1CS_M_CAP and R1CS_ROUNDS_CAP die.
- zeta and the nine GKR per-layer chains: exactly mu (+2) slots
(g_bus_mu), zeta's declaration moving below the certified bus depth.
What stays capacity-sized, and why: STREAM_CAP / YR_SLOT_STRIDE size
hint pools (witness-hint lengths are compile-time); QPKD_VARS_CAP and
MU_CAP are row STRIDES of two-row / triangle layouts (a runtime stride
would unfold every access); GKR_ROUNDS_CAP / GKR_POINTS_CAP are
quadratic in mu (a runtime g-power for them needs exponent-domain
multiplication, all cost, no cycles saved).
recursion_2to1 cycles 2,516,990 -> 2,517,066 (+76: one MUL per
materialized runtime size) against exact allocations. Suite 244 + 3
ignored green, heavy recursion suites pass, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge pull request #2 from leanEthereum/recursion
Recursion
ligerito: the fold constants are one knob each - INITIAL_K=5 works
Changing INITIAL_K / LEVEL_K / RESIDUAL_MAX_LOG in crates/pcs now
propagates everywhere:
- lean_vm's LOG_BATCH and LOG_INV_RATE stop re-declaring their own 6 / 1
behind compile-time asserts: they ARE ::pcs::ligerito::INITIAL_K /
LOG_INV_RATE_0 (the Merkle leaf width and the L0 fold count are one
knob by construction - the L0 commit is reused). The blake3_batch test
reads the constants too.
- recursion_soundness_binds stops hardcoding the residual shape: the
rs_yslot_bits tamper index (the first PADDING slot coordinate, k =
yr_log_n) is derived from the fold ladder, and the tamper is skipped
when this candidate's yr_log_n equals YR_LOG_CAP (the guest reads only
YR_LOG_CAP coords, so no pad coordinate exists and the over-read path
it guards is absent - exactly what happens at INITIAL_K=5).
Verified by actually flipping the knobs: the FULL workspace suite, both
heavy recursion suites, and the xmss CLI all pass at INITIAL_K=5 (the
guest re-derives its baked tables from the config, so recursion just
works); pcs + rec_aggregation pass at LEVEL_K=4. Shipped values are
unchanged (6 / 3 / 5), so derived configs, proofs and cycles are
byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ligerito: LigeritoProfile dies; the shipped shape becomes named constants
Only one configuration ever ships (the old Secure profile), so the
selector enum and its Fast/Slim derivation arms go. What was implicit
now has a name in crates/pcs/src/ligerito.rs:
- SECURITY_BITS = 120 round-by-round soundness target
- LOG_INV_RATE_0 = 1 L0 code rate 2^-1
- QUERY_GRINDING_BITS = 18 query-phase PoW (queries cover 120 - 18)
- INITIAL_K = 6 the INITIAL folding factor (L0 folds 2^6
interleaved rows; = PcsParams::log_batch_size)
- LEVEL_K = 3 the folding factor of every subsequent level
- RESIDUAL_MAX_LOG = 5 folding stops at <= 5 remaining variables:
the residual yr (<= 32 coeffs) is sent in clear
derive_profile(m, profile) -> derive_config(m); PcsParams loses its
profile field; lean_vm pins its LOG_BATCH / LOG_INV_RATE / SECURITY_BITS
to the pcs constants with compile-time asserts. The SoundnessRegime
enum and the Johnson/OOD analysis + prover/verifier machinery stay for
hand-built configs (the OOD roundtrip test constructs them directly);
only the profile-driven derivation of them is gone.
Derived configs are value-identical to the old Secure profile, so
proofs and the recursion guest are untouched (cycles unchanged). Suite
244 + 3 ignored green, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recursion verifier: derive positions-per-word from depth
The Ligerito query decoder took per_word as a separate Const, but it is
just FIELD_BITS / depth. Derive it inside decode_query_bits and drop the
LIG_POSITIONS_PER_WORD placeholder (guest table + Rust export + caller
arg); the write-offset now uses FIELD_BITS // LIG_TREE_DEPTH directly.
Pure refactor: identical 128/depth packing, identical emitted code, no
cycle change. Removes a redundant parameter and a placeholder threaded
through the guest; the decode specialization collapses from
__L{depth}_L{per_word} to __L{depth}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
zkDSL: print(...) - a prover-side debug print
`print(expr)` / `print("label", expr)` displays a value at witness
generation: the expression evaluates into a cell and a Print hint hangs
on a one-instruction no-op anchor, so it fires exactly at that program
point, only on the executed path, and adds no constraints (nothing
enters the transcript; proving/verification are untouched). The label
defaults to the argument's source text. Output goes to stderr as
`[print] label = ...` with the friendliest reading: decimal for small
integers, g^k for small g-powers, both when they overlap (8 = x^3 shows
as `8 (g^3)`), hi:lo hex otherwise, `<unwritten>` for a cell nobody
wrote. Allowed inside @inline bodies and loop bodies (free vars flow
through). Documented in zkDSL.md; test: tests/print_debug.rs.
Suite 244 + 3 ignored green, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
compiler: @inline calls in expression position; eq_weight dedup covers all 4 sites
Two compiler-level changes, found while deduplicating the recursion
guest's eq-tensor weight loops:
1. @inline calls now work embedded in expressions. An inlined tail
return of ANY plain var recorded a RetBind::Gaddr alias (gaddr_of
treats every runtime cell as an exp-0 g-address), which only let /
tuple bindings consumed; a call inside an expression (7 * f(x), a
store's RHS, a single-target match arm) read its never-written dst
cell and the stale bind mis-bound the NEXT let. Expression positions
now consume the bind (take_inline_ret_cell), materializing aliased
returns (free for a var; one MUL for a shifted pointer); Stmt::Let
intercepts bare user-fn calls first, keeping the zero-copy alias
hand-over of the obs / fs_next idiom byte-identical; multi-target
match_range arms materialize into their join cells. Regression test:
tests/inline_expr.rs (proves + verifies all three shapes).
2. A compile-time heap index that folds to a non-g-power field constant
(buf[0], buf[2], an integer loop var leaking in from a StackBuf
conversion) is now a compile-time error instead of a wild-pointer
DEREF at proving time - heap cell k is addressed as buf[GEN ** k].
Exactly this bit during the dedup: converting bytecode_sel to a
HeapBuf left one plain-int read in the defer_out fill, and the k=0
read lowered to address*0, failing 78k instructions later with zero
compile-time signal. Regression test: tests/heap_index.rs.
The guest's four eq-tensor weight loops then collapse into one @inline
eq_weight(ch, count, idx, msb_span) helper used straight in expression
position: row fold weights, query alpha weights, the bytecode selector
reduction (bytecode_sel now a HeapBuf), and the lincheck const-pin term
(msb_span = K_LOG selects its MSB-first bit walk).
recursion_2to1 guest cycles 2,516,950 -> 2,516,990 (+40: the selector's
three cells moved from frame to heap). Suite 243 + 3 ignored green,
heavy recursion suites pass, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wip
protocol: every Ligerito scalar rides the stream, the opening keeps only hints
LigeritoProof was the last pre-stream record struct: sumcheck messages,
OOD values, the final message yr, the level roots and both PoW nonce
families were transported in the struct and manually re-observed by the
verifier. All of them now ride the shared transcript stream at their
protocol points - add_scalar/next_scalar for the scalars (bound as
written/read), the commit-root two-scalar convention for level roots,
and grind/grind_check for the fold + query nonces (raw transport, bound
by the grind itself; query grinds push a nonce even at 0 bits, as
before). The opening object keeps only the hash-bearing hint data:
opened rows + Merkle multi-proofs per level. initial_root leaves the
proof entirely - the verifier uses the expected root it already has.
Ripples:
- SumcheckProver drops its internal transcript accumulation; tests
collect the returned messages instead, and the roundtrip tests drive
the verifier from the prover's bundle (no more detached/empty-stream
verification). Tamper tests corrupt stream words at their layout
positions, and lean_vm's validity tamper test corrupts an opened row.
- The recursion harness stops extracting lig_sumcheck_msgs / final_msg /
level_roots / fold+query nonces into hint pools, and its lincheck
tail-offset slice now skips the opening's stream words (counted from
the config shapes).
- The guest's open_stacked takes the main stream cursor: msg_cursor IS
the stream, level roots are two fs_next words (replacing the 3-absorb
byte form), yr fills from the stream, and grind nonces are raw cursor
reads bound by their DS_POW absorb. Six hint pools die.
recursion_2to1 guest cycles 2,537,008 -> 2,516,950. Suite 241 + 3
ignored green (both heavy recursion suites pass), clippy clean, xmss
CLI at 157.8 XMSS/s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
flock: re-add the standalone batch BLAKE3 proving test
The purge (2956d38) deleted tests/blake3_bench.rs along with the
standalone flock prover API it exercised (prove_fast_timed, flare
transcript, statement_digest). This brings the workload back on today's
API: witness-gen -> commit -> Blake3Setup::prove_validity_stacked
(zerocheck + lincheck reduction, one stacked Ligerito open) ->
verify_validity_stacked, over FLOCK_N compressions (default 8192, Secure
profile) with per-phase timings and a verify + finish gate - the fast
loop for iterating on the flock reduction / PCS without the xmss harness.
RAYON_NUM_THREADS=11 FLOCK_N=131072 cargo test --release -p flock \
--test blake3_batch -- --nocapture
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lean_vm: the Ligerito proof moves out of RingSwitchVerify
RingSwitchVerify becomes a plain claims bundle (no lifetime): the opening
proof is not ring-switch data, it is the ONE stacked proof covering the
slot claims too, so pcs::verify now takes it as its own parameter. The
caller (cpu::verify) reads it off the openings hint channel at its
protocol point and passes the borrow straight through - dropping the
.clone() that copied the whole Merkle-bearing proof (next_opening returns
&'a tied to the deserialized Proof, not to the VerifierState borrow), and
the stale "BLAKE3 attachment" field doc with it.
No protocol change. Suite 240 + 3 ignored green, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recursion guest: baked prefix-mask table replaces the hinted overlap mask
claim_overlap_mask's content is fully determined by the already-pinned
claim_nover (the prefix of exactly nover ones), so instead of hinting +
certifying it, the guest bakes ALL cap+1 possible prefix masks into one
constant table (row t = [k < t]) and each claim selects row nover by
pointer arithmetic (nover_g ** YR_LOG_CAP, a 3-MUL square-and-multiply).
Mask reads stay one DEREF; the hint stream and the booleanity,
monotone-prefix, and popcount pins all vanish (the prefix property holds
by construction). The hi-span zero asserts stay and double as the
nover <= yr_log_n pin: a larger nover selects a row whose prefix reaches
into the asserted span. nover's range check tightens from SIZE_BITS to
YR_LOG_CAP + 1, which also pins the row select to the table.
(A first attempt wrote the mask with two runtime mul_range loops per
claim; loop-call overhead cost +16.5k cycles, so it was dropped for the
table select, which is straight-line.)
The host stream and its tamper case in recursion_soundness_binds go
away; the mask is no longer prover-chosen and the claim_nover tamper
covers the overlap path.
Parser: an integer log bound folds like any parse-time size, so
`assert log x < YR_LOG_CAP + 1` works.
recursion_2to1: 2534682 -> 2516928 cycles (-0.7%), bytecode 314764 ->
305887 instructions. recursion_2to1(+mixed) and recursion_soundness_binds
pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zkDSL: list literals — `x = [a, b]` is an initialized StackBuf
New sugar for the alloc-then-store idiom: `x = [a, b, …]` allocates a
StackBuf of the element count and writes each element in place, through
the same stack-store path (copies/constants still defer as aliases), so
the emitted code is identical to the manual form. Elements lower before
the name rebinds, so `s = [s[1], s[0]]` swaps through the old binding.
The recursion guest drops ~25 three-line StackBuf init blocks for it;
bytecode is unchanged (314764 instructions, same committed witness).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recursion guest: walk the flock message regions in order too
The flock zerocheck multilinear-round messages (zc_msgs) and lincheck-round
messages (lincheck_msgs) were the last "reserve region, offset-read later"
sites: `zc_msgs = cursor` / `lincheck_msgs = cursor` plus a bulk skip. Walk them
in stream order with fs_next instead. The unroll rounds walk the cursor
directly; the flock multilinear mul_range threads it through a new
flock_round_cursor heap chain, exactly like the GKR/AIR rounds thread
gkr_round_cursor. The `cursor *= nmlv_g * nmlv_g` and
`cursor *= GEN ** (2 * LINCHECK_ROUNDS)` skips are gone; the cursor now walks
every stream word in order, and the only remaining cursor moves are the
heap-chain seeds and the raw grinding nonce.
Transcript unchanged: recursion_2to1 / _mixed verify with the committed witness
identical (2^26.079).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
recursion guest: fs_next fetches, observes, and advances in one step
Rename read_obs -> fs_next and have it return the advanced cursor too, so a
stream walk is `fs, x, cursor = fs_next(fs, cursor)` with no manual
`cursor *= GEN`. Every per-word read now walks: sequential regions thread a
running cursor; fixed-base per-round loops (flock zerocheck/lincheck, the two
aggregation sumchecks) thread within each read-pair, which also drops the
`* GEN ** (2i+1)` offset multiplies. The three "reserve region, observe it as a
block later" sites (zerocheck round-1, lincheck z_partial, ring-switch s_hat_v)
now fetch-and-observe as they walk, into small HeapBufs. The only manual cursor
moves left are genuine region-skips and the raw grinding nonce (absorbed later
as DS_POW).
Transcript unchanged: recursion_2to1 / _mixed prove and verify with the
committed witness identical (2^26.079).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
compiler: inline multi-return can alias-return a g-address
Generalize the inline tail-return binding so each returned value binds into the
caller exactly as a `let name = <expr>` would: a `StackBuf` hands over its cell
run, a folded g-address hands over its symbolic pointer, both aliased at zero
copies; a scalar is copied into its dst cell. Previously only a StackBuf could
be alias-returned (and only as the sole return value); a g-address among several
returns would materialize into a cell, losing the free compile-time fold and the
compile-time bounds check.
Replaces the StackBuf-only `Option<Vec<Option<(Off,u32)>>>` record with a
`RetBind` enum threaded through lower_return / Stmt::Let / Stmt::LetTuple. New
stack_buf test `inline_returns_advanced_cursor` covers returning
(StackBuf, scalar, advanced-cursor) together and reading through the cursor.
This is what lets the recursion guest's stream walker return the advanced cursor
for free: `fs, x, cursor = fs_next(fs, cursor)`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
transcript: drop the flock/pcs sub-protocol domain-separation labels
The zerocheck, lincheck, pcs-open-batch, packed-direct, ring-switch, and
ligerito-basis sub-protocols each absorbed a `b"flock-*-v0"` label into the
Fiat-Shamir transcript. These were redundant: the transcript is a strict hash
chain with a fixed, non-adaptive verifier structure, so a segment's position
already identifies which sub-protocol it belongs to, and the top-level seed
(the flock-family + program digest) separates this proof system from any other.
Remove all six labels symmetrically from prover and verifier (crates/flock and
crates/pcs) and from the recursion guest's replay, plus the now-dead guest
label constants, harness placeholders, and word16 helper.
flock (74), pcs (62), lean_vm (15), and rec_aggregation (recursion_2to1 +
_mixed) all still prove and verify. The recursion guest shrinks 315445 ->
314920 instructions (1008 fewer BLAKE3 absorbs); a pass requires the native
proof and the guest replay to agree, confirming the removal is consistent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: give recursion DSL placeholders descriptive names
Pure mechanical rename of cryptic placeholders in recursion.rs and the
xmss_aggregate/recursion guest; no behavior change.
- N_INNER_ROUNDS -> N_FIXED_CHALLENGE_ROUNDS, INNER7 -> FIXED_CHALLENGES,
I7INV -> ONE_PLUS_CHALLENGE_INV
- {ZC,LC,OB,RS,PD,LIG}LBL A/B -> {ZEROCHECK,LINCHECK,OPEN_BATCH,
RING_SWITCH,PACKED_DIRECT,LIGERITO_BASIS}_LABEL_0/1
- GG dropped in favor of the builtin GEN; GINV -> INV_GEN
- ILD0/1/2 -> LAGRANGE_INV_0/1/2; ILAM/ICMB/ISDOM ->
LAGRANGE_INV_{LAMBDA,COMBINED,S}
- PHI -> PHI8_NODES; DELTA -> TRACE_DUAL_BASIS; SEEDB0/1 -> TRANSCRIPT_SEED_0/1
- merge duplicate NCL/NCLAIMS into a single N_CLAIMS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fiat-shamir: fuse stream read + sponge absorb into one read_obs
The recursion guest repeated `x = cursor[..]; fs = obs(fs, x); cursor *= GEN`
at every proof-stream read, maintaining the "observe before it influences a
challenge" invariant by hand at each site. Fuse read+observe into one
`read_obs(state, cursor) -> (state, x)` so a stream word cannot enter the
computation unbound.
That needs an @inline to return the sponge state (a StackBuf) alongside the
consumed scalar, which the lowering forbade: it aliased a returned StackBuf
only when it was the SOLE return value, and a multi-value return routed every
element through expr() (panicking on a StackBuf). Generalize `inline_stack_ret`
from one `Option<(Off,u32)>` to per-slot `Option<Vec<Option<(Off,u32)>>>`, so
each returned value is independently a StackBuf alias or a scalar cell;
lower_return / Stmt::Let / Stmt::LetTuple bind each accordingly. `fs` stays a
StackBuf and obs/squeeze/absorb are unchanged.
Guest transcript is unchanged: the recursion guest compiles byte-identically
(315445 instructions, same MUL/DEREF counts), and recursion_2to1 /
recursion_2to1_mixed still prove and verify. New stack_buf test covers the
inline StackBuf+scalar tuple return in isolation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
naming
feat: cache XMSS benchmark signers to disk
Signer generation (keygen over slots 0..=15 + grinding sign) is the
untimed setup cost of the aggregation benchmark and dwarfs proving for
large batches. Generate each signer once, store the batch to disk under
target/signers-cache/, and reload on later runs.
The cache is a growing pool keyed on a footprint of the generation
parameters (not on n), so bumping LEANVM_XMSS_N only generates the new
tail and never re-does a smaller run's work. Also memoized in-process.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
benchmarks
perf: fs_seed no longer rebuilds + rehashes the circuit family per process
82f4f74 made every transcript seed depend on the flock circuit-family
digest, whose OnceLock init called build_block_r1cs(3) and hashed the
matrices entry by entry: ~200 ms of symbolic matrix building (a second
copy, next to the setup's own) plus ~250 ms of per-entry blake3::update
calls over 21M nonzeros, landing inside the first prove. On XMSS x820
that cost ~6% on a 16-core x86 box and ~25% on an M4 Max (1.6 s prove).
Three fixes:
- flock::blake3::FAMILY_DIGEST: the digest is a constant of the circuit
family, so bake it; family_digest_matches_baked recomputes and compares,
failing on any circuit change until the constant is bumped. Same value
as before, so fs_seed and the recursion guests are untouched.
- build_block_r1cs clones the cached matrices() instead of re-running the
symbolic builder: setups and const-pin lookups share one build per
process (200 ms -> 72 ms per extra call).
- absorb_matrix flattens to one buffer before hashing (244 ms -> ~10 ms),
keeping the byte stream (and digest) identical.
XMSS x820, 11 threads, x86: 5.55 s before 82f4f74, 5.87-5.94 s after,
5.46 s now. Suite 235 + 3 ignored green, recursion suites pass, clippy
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
polish: apply ~104 review findings across all crates
Comment/naming/dead-code pass over the new workspace:
- fix stale docs referencing purged code (BaseFold targets, jagged,
default_config, verify_succinct, prove_batched, record structs)
- re-attach doc blocks that had drifted onto the wrong item
- rename ceil_log2 -> log2_ceil (naming convention), LigeritoLevelConfig
k_levels -> k (scalar, not a collection), VerifyError::PcsAb -> Pcs
- delete leftovers: empty impl blocks, unused locals, apply_triple (+test),
dead rounds vectors in lincheck, fold_packed_witness_at_z
- gate test-only items (#[cfg(test)]): stacked prove/verify wrappers,
witness::stack, udr_queries; keep the general NTT entry points pub
- fix stale crate paths in misc/doc.tex
cargo test --workspace: 234 passed + 3 ignored; ignored recursion suites
pass; clippy warning-clean across all targets.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
restructure: vendor/ dies, the repo becomes 8 clean workspace crates
crates/
primitives GF(2^128)/GF(2^8) field kernels (+ phi8, g-power helpers),
bit transposes, multilinear/eq/Lagrange helpers, the scratch
pool, alloc/log2/perf utilities
fiat_shamir the VM-native sponge + the ProverState/VerifierState
transcript wrappers, now GENERIC over the opening type (the
one upward reference the old layout had)
pcs additive NTT, Merkle, commit, packing (+ PaddingSpec, moved
from zerocheck), ring-switch, Ligerito, and the stacked
open/verify facade as lib.rs; exports the concrete
Proof/ProverState/VerifierState aliases
flock r1cs, zerocheck (+ univariate skip), lincheck, ZClaim,
VerifyError, and the BLAKE3 circuit + witness generation
(the old flock-prover, as blake3.rs / blake3_witness.rs)
lean_vm the VM: cpu (incl. the new cpu::hints with RHint/GPowMap —
moved from the compiler, breaking the cpu<->compiler cycle),
tables, constraints, witness, gkr, leaf, the stacked-PCS
glue, blake3_flock, vmhash
lean_compiler the zkDSL front end (ast/ir/lower/parser) + zkDSL.md, with
the compiler feature tests + tests/programs/
rec_aggregation the two flagship harnesses as a LIBRARY (run_recursion,
run_xmss_aggregation, run_fibonacci) + guests/*.py + the
recursion/xmss/fibonacci suites as unit tests
xmss unchanged, relocated
Root: leanvm-b is now a clap CLI (xmss / recursion / fibonacci) over
rec_aggregat…
The test hardcoded the inner proof's stacked log-size as m=22 and derived its residual-pad tamper coordinate from that: yr_pad_idx = yr_log_n(22) = 4. The committed stack for its config has since grown to m=23 (whose ladder has yr_log_n = 5), so coordinate 4 of rs_yslot_bits is a real y-slot index bit, not padding; the honest value there is 1 and the assert_ne tamper-setup check failed before any soundness assertion ran.
Derive the stacked size from the batch instead of hardcoding it: build_batch now records each inner proof's layout m (stack_mus), exactly as gen_verify reconstructs it, and the test reads batch.stack_mus[0]. The dispatch-candidate bounds move into shared MU_MIN/MU_MAX consts used by both the placeholder map and the test's YR_LOG_CAP mirror, closing that drift channel too.
The config moves to (8, 2^13) (m=24, yr_log_n = 3 < cap) so the residual-slot over-read tamper is exercised rather than skipped, and the silent skip branch becomes a hard assert: a future shape drift that parks yr_log_n at the cap fails the test loudly instead of quietly weakening it. Refresh recursion_generic_many's stale per-config m annotations (all shifted by one: m in {23..26}).
All five tamper cases now run and are rejected; the honest proof verifies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge branch 'main' into small-proof
Brings in main's standard-BLAKE3-chaining protocol (b94b315, b788b1c) ported onto this branch's 192-bit three-lane words, plus the SIMD-batched Merkle hashing, hierarchical tracing and pretty-print reconciliation (both originated here; main's ports superseded where they diverged), and the signers-cache encoding-predicate fingerprint.
The chaining port, adapted to canonical 128-bit chunks embedded in 192-bit cells: Op::Blake3 gains a chaining-value base offset (two consecutive cells, each read through the bus as (lo, hi, 0) so the canonical embedding is proof-enforced and the zero top limbs are never committed) and a 128-bit metadata immediate carried in the two low K-lanes of a 192-bit word whose lanes ride two public bytecode slots. The instruction encoding grows from six to nine public columns and the bus from nine to twelve coordinates; the stacked bytecode polynomial takes four selector bits. The BLAKE3 table adds the CV operand/address, two CV memory reads, and the sixth address binding; its eighteen value lanes are virtual, routed to the generalized flock witness (cv words 0..4, metadata words 18..20). The flock circuit takes cv/counter/blen/flags as free input rows with the re-baked family digest, and the branch's bilinear circuit walk is updated to match. vmhash::hash_slice, PCS Merkle leaves, xmss tweak hashes, and the program digest switch from the Merkle-Damgard length-in-IV construction to standard BLAKE3.
Guest updates: the recursion guest packs each queried leaf row into a contiguous cell run (level 0 packs two hinted F64 lanes per cell via PACK64X2; deeper levels pack the three serialized F192 limbs per word likewise, preserving the limb K-membership proof) and hashes it as chunk-chained standard BLAKE3 through per-level LIG_LEAF_BLOCKS (level 0: n/8 blocks of 8-byte lanes; deeper: 3n/8 blocks of 24-byte words); the BLAKE3 zerocheck branch grows to six Horner-batched address bindings. The xmss guest uses cv-chained sub-hashes (146 compressions per signature, down from 158). The compiler keeps pack64x2/pack64x2_into/hint_f192_limbs alongside the new blake3 keyword surface and lazy per-scope IV cells (SET from IV_CELLS, registered in the const-cell cache).
Kept from this branch where main's ports lagged: the log-inv-rate CLI and per-proof PCS rates, the F64 Ligerito implementation (main's tracing landed in deleted code), the prepared-reduction-witness plumbing, and the per-64-bit-word WOTS encoding.
Full workspace suite passes, including both recursion 2-to-1 tests (mixed rates included), xmss aggregation, and main's chaining regressions adapted to three-lane words. The ignored recursion_soundness_binds test fails identically on the pre-merge HEAD (a pre-existing breakage on this branch, not a merge regression).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge branch 'main' into small-proof
# Conflicts:
# crates/flock/src/blake3.rs
# crates/flock/tests/blake3_batch.rs
# crates/lean_vm/src/blake3_flock.rs
# crates/lean_vm/src/cpu/layout.rs
# crates/lean_vm/src/cpu/mod.rs
flock: reuse prepared reduction witness
Retain the fused A·z, B·z, and lincheck layouts created with q_pkd and consume them after commitment instead of regenerating the witness. Add env-gated reduction phase timing and exercise the prepared path in the batch harness.
Support StackBuf returns from normal functions
Use Horner evaluation for challenge polynomials
Replace explicit challenge-power expansions across VM AIR constraints, recursive zkDSL verification, and quadratic sumchecks with Horner evaluation. Add shared native and zkDSL helpers for efficient F192 limb reconstruction, mirror every AIR formula on both sides, and update compiler documentation and examples. Include the repository-wide cargo fmt pass.
Simplify recursion hashing to canonical BLAKE3 cells
Remove the Transcript192 BLAKE3 mode and its alternate opcode, leaving one canonical 128+128-bit encoding throughout the VM. Shrink the BLAKE3 AIR table from 51 to 31 columns and reduce its local constraints to the five address bindings while the memory bus and Flock validity proof bind the canonical lanes.
Add computed F192 limb advice and destination-target PACK64X2 support, including nested inline zkDSL helpers for transcript absorption, challenge reconstruction, and constrained conversion between canonical Merkle cells and Ligerito transcript words. Update recursion witness generation, documentation, and regression coverage accordingly.
Add PACK64X2 base-field packing instruction
Introduce a VM instruction that reads two F_2^64 memory words and packs them canonically as one F_2^192 word with limbs (lo, hi, 0). Enforce source subfield membership and destination packing through literal-zero memory-bus coordinates, so the invariant is proof-enforced rather than only checked during witness generation.
Wire the opcode through execution, traces, AIR, bytecode, layouts, transcript binding, table counts, statistics, and recursive-verifier shape handling. Preserve the existing BLAKE3 Transcript192 mode; no mixed packing mode is needed.
Expose pack64x2(a, b) in zkDSL and use it in recursion.py to constrain every serialized Merkle-leaf limb before reassembly, hashing, and row-dot evaluation. Add end-to-end proving and invalid-extension-source tests, Python stubs, and VM/zkDSL documentation.
Document MCA error in unique-decoding regime
Pretty-print displayed integers with commas
pcs: make F64 Ligerito the sole implementation
Remove the Ligerito/Ligerito-K split and promote the F2^64-committed implementation to the canonical pcs::ligerito API.
- move the shared security/configuration machinery into a private ligerito_config module and re-export it through pcs::ligerito
- rename pack_k, ring_switch_k, stack_open_k, and tensor_algebra_k to their canonical module names
- drop K suffixes from commitments, proofs, configs, ring-switch types, stack-opening types, helpers, and transcript aliases
- migrate lean_vm, flock, recursive aggregation, throughput tests, guest comments, and protocol documentation to the canonical API
- keep dense verifier and ring-switch reference paths only for tests, excluding unused helpers from production builds
- remove stale compatibility terminology and the superseded extension-field implementation
- preserve the existing NEON and AVX/AVX-512 optimized kernels unchanged
Verified with:
- cargo check --workspace --all-targets
- cargo test --workspace --release
- PCS_LOG_N=16 PCS_LOG_INV_RATE=1 PCS_SAMPLES=1 cargo test --release -p pcs --test pcs_throughput -- --ignored --nocapture
- RAYON_NUM_THREADS=11 cargo run --release -- recursion --n 2 --log-inv-rate 1
- RAYON_NUM_THREADS=11 cargo run --release -- xmss --n-signatures 820 --log-inv-rate 1
- git diff --check
pcs: vectorize F192 NTT with AVX-512
improve hierarchical PCS tracing
primitives: add AVX-512VBMI bit transpose
enable tracing
remove 128 bits fields
make tests pass
add pcs_throughput benchmark
pcs/ligerito_k: pool the initial-sumcheck fold buffers (x86_64)
The initial sumcheck is the dominant phase of the K-PCS open (~57% at
log_n=24), and it allocs+faults two fresh ~100 MB F192 buffers (nf, nb)
every fold round, then frees them — the allocator munmaps on drop, so
every prove re-pays soft page faults on first touch.
Route the fold outputs through the existing F192 scratch pool
(primitives::scratch) via a new fold_out_buf helper, and give the
consumed input buffers back in SumcheckProverK::fold, so repeated proves
reuse resident pages instead of faulting fresh mappings. x86_64-gated to
match flock (pooling here measured slower on aarch64, which keeps the
fresh-alloc path). Correctness: F192 has no Drop and every fold slot is
written before read, so recycled (stale) buffers are sound; all 55 pcs
tests pass incl. the log_n 12/16/18 roundtrips.
Measured (log_n=24, LIG_K_TRACE, steady state):
initial sumcheck ~0.175 s -> ~0.063 s (~2.8x)
total open ~0.31 s -> ~0.18 s (~1.7x)
Ports the buffer-pooling idea from flock (github.com/succinctlabs/flock),
commit "perf: add x86 AVX-512 prover acceleration" (the scratch-pool
reuse for the initial-sumcheck fold).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align Ligerito RBR soundness doc and implementation
Fix pcs.tex thm:mca-johnson against the primary source (BCHKS25 Thm 4.6,
list correlated agreement): the theorem parameter is max(ceil(sqrt(rho)/eta), 3),
not the ceil(sqrt(rho)/(2 eta)) of the non-list Thm 1.5 that Flock's Thm 8
quotes, and rho is the slightly reduced rate (k-1)/n. The implementation
(johnson_m_param, reduced_rate) already had both right.
In ligerito.rs, name misc/pcs.tex as the soundness source of truth and map
thm:rbr's per-verifier-message error table onto the validate() checks
(max semantics, per-transition 128 bits); replace dangling references to a
former appendix. Remove the dead UDR regime (never constructed in
production), making eta and expected_eps_ood_bits plain f64. Fix the OOD
diagnostic doc (192, not 128) and the INITIAL_FOLDING_FATOR typo.
Update doc.tex's PCS paragraph: per-level optimized eta (not 0.02) and
17-bit query grinding (queries close the remaining 111 bits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge branch 'main' into small-proof
pcs.tex (a note on WHIR/Ligerito over binary fields, interpreted as List binding PCS
Use query grinding to reduce proof size
Optimize Johnson eta per recursive level
Use aggressive recursive RS rate schedule
Audit and tighten Ligerito soundness parameters
Correct the BCHKS25 Johnson proximity-gap parameter to use ceil(sqrt(rho) / eta), and evaluate the theorem at the reduced RS rate (K - 1) / n instead of the nominal encoding rate.
Move the global Johnson slack from 0.02 to 1/160, retaining more than 128.8 bits of proximity-gap soundness without grinding while reducing query counts and proof size.
Validate exact, unrounded query, proximity, OOD, list-unioned algebraic, and degree-191 ring-switch errors. Clarify the round-by-round security claim and add production-profile regression tests for all supported rates and witness sizes.
Make Ligerito rate proof-local and recursion-generic
Pass log_inv_rate explicitly through proving instead of process-global state, announce and validate it through Fiat-Shamir alongside layout sizes, and derive commitment/opening parameters from the transcript-bound value.
Embed all supported rate/size configurations in the recursion guest so one program can verify mixed-rate inner proofs, while letting the CLI-selected rate independently configure both inner and outer proofs. Add CLI validation, the environment-driven query-count report, mixed-rate regression coverage, and update the README.
Move Ligerito to 128-bit Johnson list decoding
Raise the global round-by-round soundness target to 128 bits and derive the production Ligerito ladder in the Johnson list-decoding regime over GF192 with eta 0.02.
Use the post-commit L0 opening claim as its implicit binding evaluation, and add one explicit OOD evaluation for each recursive commitment. Port those OOD claims through the GF64-committed prover, dense and succinct native verifiers, proof encoding, recursion hint builder, and in-VM recursion verifier.
The GF192 margin makes the query, proximity-gap, and OOD-binding terms each clear 128 bits without query or fold grinding. Add parameter assertions and OOD tamper coverage, and update the security documentation.
Validated with all PCS, Flock, LeanVM, and recursion release tests, recursion --n 2, and xmss --n-signatures 820. The recursion proof drops from 579.1 KiB to 518.3 KiB and guest cycles from 2,842,984 to 2,591,048.
Optimize GF192 ring switching in recursion
Factor the GF192 trace-dual basis over its GF64 degree-three tower, replacing the dense 192-by-192 Frobenius table with smaller base and tower tables. Assert the factorization while constructing guest placeholders.
Evaluate the two transposed claims and their transparent ring-switch weights in lockstep so they share coefficients, powers, and verifier-weight loads.
This reduces the recursion --n 2 guest from 3,235,330 to 2,842,984 cycles (12.1%), the proof from 610.7 to 579.1 KiB, and measured proving time from 13.50s to 9.22s.
Merge field-exploration into small-proof
Bring over native BLAKE3 circuit-walk verification, compiled recursion-guest caching, and clarified division-by-zero semantics. Adapt the verifier paths and cache key to GF192. Deliberately omit the bus-grinding changes from 97d9c687cd01fbec9987172f56abd810fa2299d7.
doc
recursion: cache the compiled guest across verifies
RecursiveProof::verify recompiled the recursion guest on every call
(~235 ms), dwarfing the ~20 ms of actual proof checking. The guest is a
pure function of the inner bytecode and the batch size, so
recursion_guest_arc caches compiled guests process-wide, keyed by
(fs_seed(inner_program), nsub) and capped at 8 shapes, compiling
outside the lock (mirrors blake3_flock::setup_for). verify borrows the
pristine cached Arc; prover paths clone an owned copy for their witness
streams (cpu::Program now derives Clone).
Complete recursive verify: 268 ms -> 18.7 ms once a shape is warm, now
dominated by the Ligerito opening.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native verifier evaluates A0/B0 by circuit walking
The native verification of a recursive proof paid the ~21M substituted
nonzeros of the BLAKE3 matrices twice: check_reduced contracted two eq
tables through them to discharge the deferred v_a/v_b claims (~11 ms,
forcing the ~300 ms matrices build), and lincheck::verify materialized
the alpha-batched column marginal via the CSC fold (~1.2 ms + one-time
22 ms transpose).
bilinear_walk_pair (flock.pdf, Circuit walking) computes u^T A0 w and
u^T B0 w in O(circuit) field ops (~43 us) by walking the unsubstituted
compression cascade over F128T wire values, mirroring build_matrices
row for row; a randomized cross-check test ties it to the materialized
matrices. check_reduced now walks. lincheck::verify no longer folds
comb_vec through the rounds: the final consistency check collapses to
one bilinear form against the rank-1 column weight
w_col[c] = z_partial[c_skip]*eq(r_inner_rest, c_rest), answered by the
new LincheckCircuit::bilinear_form (WalkLincheckCircuit; default None
falls back to the marginal + inner product). Transcript unchanged.
Proof checking drops ~31 ms -> ~20 ms per recursive verify.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document the GF192 machine field
Update the VM and proving-system specification from the old quadratic GF128 representation to the implemented cubic tower E = GF64[y]/(y^3 + y + 1). Describe three-limb memory words, arithmetic, immediates, memory interactions, instruction tables, and public-input openings.
Clarify that BLAKE3 uses canonical 128-bit values embedded in full GF192 cells with a zero top limb, and record that the larger challenge field eliminates grand-product grinding under the instance caps. Also distinguish the zkDSL's 128-bit literal syntax from its 192-bit machine-word type.
Use univariate batching for GF192 ring switching
Replace the padded eight-challenge coordinate cube with powers of one GF192 Fiat-Shamir challenge. Share that challenge across stacked ring-switch claims and retain the existing GF64 transpose and GF192 transparent-weight evaluation paths.
In the recursion guest, compute the trace-dual linearized coefficients directly with Horner evaluation. This removes the equality tree and coordinate-weight buffer, reducing the recursive verifier by 1,510 instructions and 76,746 VM steps relative to the padded baseline while retaining better than 184 bits of batching soundness.
feat: migrate recursion and PCS to F192
Move the protocol challenge and machine-word field to the cubic F192 tower while retaining F64 physical columns and canonical 128-bit BLAKE3 cells.
- add portable, x86, and AArch64 F192 arithmetic with deferred-reduction kernels
- carry all three limbs through the compiler, ISA, trace tables, Flock reductions, transcript, GKR, and public-input binding
- generalize K-packed Ligerito, ring switching, tensor algebra, query sampling, and Merkle serialization to 192-bit challenges and folded words
- update the recursion guest AIR, claim descriptors, memory binding, packed queries, and Merkle verification, including the rho-point fix for virtual BLAKE claims
- remove grand-product bus grinding under the F192 soundness budget and account for the actual 2^192 Ligerito challenge field
- refresh DSL documentation and tests, and keep the large BLAKE3 batch benchmark as an explicit release-only benchmark
Clarify division by zero semantics
Fix bus grinding soundness bound
pcs: batch F64 NTT butterflies with NEON
docs: align field architecture documentation
pcs: batch F64 NTT butterflies with VPCLMUL
pcs: optimize tower-field stacked openings
Merge branch 'main' into field-exploration
pcs: avoid redundant basis initialization work
cpu: transmit the PI low lane so the guest skips Frobenius extraction
The public-input binding needs both K-lanes of MEM(r) = interp(pi, r) as opening
claims. The lane values are deterministic from the seeded pi, but a recursive
verifier can't cheaply slice F64 lanes out of the E-valued pi — the old guest
recovered them with two 64-squaring Frobenius loops. Instead, transmit the low
lane v_lo = MEM_LO(r) (one scalar) and deduce the high lane
v_hi = (MEM(r) + v_lo)*Y^-1 from the pi-derived E-value. The guest replays it as
one scalar read + one multiply — no Frobenius, no X_INV.
This nets a simplification: bind_pi_claim is a single helper returning
[ColumnClaim; 2] (reusing multilinear::interp), the transcript add_scalar/
next_scalar is inline at each call site, and the now-dead X_INV constant + its
Frobenius baking are deleted (guest and harness). Transcript and claim order are
unchanged from the two-lane binding, so it stays sound.
Validated in release: cpu 8/8; recursion_2to1, _mixed, _soundness_binds
(tamper-rejection), _generic_many all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
flock: restore standalone BLAKE3 batch benchmark
Merge branch 'tower-only-flock' into field-exploration
field: remove GHASH and use tower field throughout
unify Ligerito protocol configuration
sponge: carry the domain-separation tag in lane 2 (main convention)
The v2 sponge bound the DS tag in the 4th compress lane, which forced the
recursion guest to encode its tags as `k << 64` (the tag in the F128T high
lane). Move the tag to the 3rd lane (right after the up-to-two data words) so
the guest's tags are plain `1,2,3,4,5` again, matching main's convention. The
tag lane is a pure convention (all five values stay distinct and nonzero, so
domain separation is unchanged); byte-absorb blocks now carry two data words
(16-byte chunks) instead of three, since the tag takes the third lane.
Validated end-to-end in release: cpu 8, pcs 103, flock 77, recursion_2to1 +
mixed, and recursion_soundness_binds (tamper-rejection) all pass — prover,
verifier, and the in-VM guest replay agree on the new transcript.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
recursion: remove the `recursion` feature gate (harness is ported)
The gate + `unimplemented!` stub existed only because the recursion harness
was unported to the 64-bit machine; now that it works end-to-end, drop the
feature so the `recursion` module and the real `run_recursion` are always
compiled. `cargo run --release -- recursion --n 2` runs the aggregation with
no extra flags (verified: 2 inner proofs aggregate and the recursive proof
verifies). Also removes the now-dead `uk` base-field encoder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
recursion: port the N->1 aggregation to the 64-bit tower protocol
Make the in-VM recursion guest verify N->1 recursive proofs on the 64-bit
(K=F64, extension F128T) protocol. Each fix addressed a constant or convention
that coincided under the old GHASH / 128-bit packing (LOG_PACKING=7) but
diverges in the tower with 64-bit packing (LOG_PACKING_K=6):
- tower ring-switch trace-dual basis, derived by the same construction over
the tower's coordinate basis and verified against the duality property
- Fiat-Shamir domain-separation tag in compress lane 3 (the tower sponge)
- bit-decomposition reconstruction in the real F128T coordinate basis; the
field generator's powers span only F64, so g^i is not the coordinate basis
- two-lane 128-bit memory public-input claims (MEM_LO/MEM_HI) via Frobenius
lane extraction of the bound public-input words
- AIR constraint formulas re-ported for the LO/HI memory split
- K-opener transcript realigned to its F128 reference that the guest replays:
ordered d-bit-chunk query sampler + surfaced query squeezes, scalar Merkle
root binding, 64-wide s_hat_v, struct-based Ligerito observed-scalar replay,
and the SLOT_STRIDE_LOG = K_LOG - LOG_PACKING_K terminal identity
Validated: recursion_2to1 and recursion_2to1_mixed verify honest recursive
proofs; recursion_soundness_binds rejects every tampered hint (the in-VM
verifier genuinely binds); pcs 103, cpu 8. No check was weakened or stubbed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
flock: migrate the verifier to the binius tower (F128T)
Move flock's verifier (zerocheck + lincheck + the blake3 reduction) to the
binius tower field F128T, so a downstream in-VM verifier replays a single
field instead of the two-field GHASH/tower bridge.
The fast GHASH prover is unchanged (the shift_reduce univariate-skip
optimization stays). It now bridges its transcript messages and claims through
the real ghash<->tower isomorphism instead of the bit-copy reinterpretation,
so its outputs are genuine tower values the F128T verifier checks. The
blake3_flock boundary is native tower (no isomorphism on point/value/weights);
only the prover-only s_hat_v capture still crosses.
Adds the tower phi8 embedding (iso-image of the GHASH basis, exhaustive
homomorphism test, images land in F64) and a tower lagrange-weight helper.
Validated: 77 flock tests and 8 cpu prove/verify + tamper-rejection tests
pass; workspace builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXRdyJAixoTtQ2Uh8WtHBv
lean_vm: drop the DEREF pointer's always-zero HI lane (K-valued address)
The DEREF pointer is constrained to K by `a2 = p·obe` (a2 is a single-lane K
address), so its high lane is provably 0. Commit it as a single K lane
(deref::P) instead of P_LO/P_HI, with the memory read carrying a literal 0 high
lane via the new FlushBuilder::memory_k. The bus balances only if the stored
value's HI lane is also 0, which the K constraint guarantees — sound; a
debug_assert pins p.c1 == 0 at fill time.
Removes one committed column of zeros from the most frequent instruction's
table (DEREF, 571/XMSS): committed witness 2^26.180 -> 2^26.170 (~0.7%).
Branch-specific (only the 128-bit-memory-word design gives an address a HI
lane); no main equivalent. All prove/verify tests pass.
Not applied to the JUMP d/f destination words: they are only forced into K on
taken branches (b=1); a not-taken branch reads an unconstrained memory word, so
forcing HI=0 would reject valid traces.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge commit 'b956c4d' into field-exploration
# Conflicts:
# crates/flock/src/zerocheck.rs
Merge commit 'b7e1be2' into field-exploration
# Conflicts:
# crates/flock/tests/blake3_batch.rs
# crates/lean_vm/src/pcs.rs
# crates/pcs/src/ligerito.rs
Merge commit '9e957fc' into field-exploration
# Conflicts:
# crates/fiat_shamir/src/sponge.rs
# crates/pcs/src/merkle.rs
Merge commit '2905de0' into field-exploration
# Conflicts:
# crates/lean_vm/src/leaf.rs
Merge commit '7b74709' into field-exploration
# Conflicts:
# crates/primitives/src/multilinear.rs
Merge commit '086f622' into field-exploration
# Conflicts:
# crates/lean_vm/src/pcs.rs
Merge commit 'b336897' into field-exploration
# Conflicts:
# crates/lean_vm/src/cpu/layout.rs
Merge commit '11e799f' into field-exploration
# Conflicts:
# crates/lean_compiler/src/lower.rs
Merge commit 'a825bf6' into field-exploration
# Conflicts:
# crates/flock/src/zerocheck.rs
# crates/lean_vm/src/tables.rs
Merge commit 'bd8b31b' into field-exploration
Merge commit '53077e5' into field-exploration
field: switch the VM's extension field E to the binius64 tower
Make the binius64 tower K[y]/(y^2 + x*y + 1) the shipped degree-2 extension
field E, replacing the Artin-Schreier tower K[y]/(y^2 + y + x^61). Same base
field K = F64 and identical repr(C){c0,c1} layout, so the ~1,200 call sites are
untouched: F128T now names the binius tower (tower_f128.rs, formerly the
tower_f128_xy investigation variant), and the old Artin-Schreier tower is kept
as F128TArtin (tower_f128_artin.rs) for the field benchmarks only.
- Build binius up to full field parity before the swap: From<F64>, AddAssign,
the mixed K x E path (mul_base / mul_base_unreduced / a BaseUnreduced
accumulator, all tower-independent), Serialize/Deserialize/Hash, the Y const.
- Re-derive the GHASH <-> tower isomorphism (iso_f128.rs) for y^2 = x*y + 1.
The K-embedding is tower-independent (X unchanged), so only the Y columns
move; the j=0 block is byte-identical to the old matrices. gen_iso_xy.rs
regenerates and verifies them (100k random multiplicativity + round-trips),
replacing the lost scratchpad/iso.py.
Correctness verified on aarch64: the whole workspace builds and every test
passes -- pcs prover/verifier round-trips, lean_vm (VM, GKR, constraints, the
leaf mixed K x E path), and blake3_flock (which maps claims through the
re-derived iso at the flock boundary), plus all integration tests.
This is the correctness migration only; the batched fold-kernel speedup that
motivated it lands separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
remove superseded zerocheck kernel
field benches: wire the VPCLMULQDQ batched inner product
field_bench: new 'inner prod, VPCLMUL' row (x86 vpclmulqdq+avx512f only) running
the batched Karatsuba kernel (B=2) for F128T and F128Txy — ~5.5-6.9x the scalar
'deferred' row (0.37 vs 2.3-2.6 ns/term), and it collapses the tower gap from
C/B 1.12x (scalar) to 1.00x: wide CLMUL erases binius's deferred-inner-product
deficit without flipping it. Banner now reports the VPCLMULQDQ backend.
inner_bench: the noise-robust comparator is now generic over the field, with an
x86_64 backend ranking scalar-deferred vs vpclmul kara/school at bank counts
1/2/4 for both towers, plus a best-of-each head-to-head. Findings on Zen4:
Karatsuba beats schoolbook (0.367 vs 0.393 ns/term) — the reverse of M-series,
where PMULL is cheap enough that schoolbook's extra product buys dropping the
pre-XOR; on x86 CLMUL throughput is the bound, so the extra CLMUL loses. B=2
wins the bank sweep. Confirms the schoolbook win was Apple-specific.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
field: AVX-512 VPCLMULQDQ batched inner-product kernels for both towers
Adds field/vpclmul.rs, a shared AVX-512 batched Karatsuba accumulator: one
VPCLMULQDQ does four independent 64x64->128 carry-less products (one per zmm
lane), folding four field elements per CLMUL. Two variants — karatsuba_acc
(3 CLMUL + shuffle/XOR pre-mix) and schoolbook_acc (4 CLMUL, no pre-mix) —
both return the same (Sum p0, Sum p1, Sum pm) triple with a const-generic bank
count for ILP.
Both degree-2 towers gain inner_unreduced_vpclmul_{kara,school}<B> in their
x86_64 modules. F128T and F128Txy are repr(C){c0,c1} with identical layout, so
the accumulate is shared; each kernel only supplies its field-specific reduce,
reusing the already-tested Unreduced::reduce(). Only the batched accumulate is
new arithmetic.
Tests check the batched kernels against the scalar deferred reference across 18
lengths (including partial vector groups and the <4 scalar tail), both kernels,
bank counts 1/2/4, on both fields.
Gated on target_feature vpclmulqdq + avx512f (Zen4 / Sapphire-Rapids-class);
-C target-cpu=native enables both. The CLMUL stays 128-bit per lane — this
batches four, it does not widen the field multiply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
field: schoolbook batched inner-product NEON kernel + noise-robust comparator
inner_unreduced_neon (the deferred sum_i a_i*b_i hot-loop kernel) was
Karatsuba, 3 PMULL/term. On M4 Max PMULL is not the bottleneck, so dropping
the per-term am/bm Karatsuba pre-XOR (2 DUP + 2 EOR) in favour of one extra
PMULL wins: schoolbook products run ~1.25x faster. inner_unreduced_neon now
dispatches to inner_unreduced_school::<2>; inner_unreduced_kara is retained as
the comparison alternate. field_bench's "inner prod, NEON kernel" row: the
tower kernel drops 0.58 -> 0.47 ns/op.
The win was isolated with a noise-robust comparator (bin/inner_bench): it
interleaves every method round-robin (rotating order) and ranks by the MINIMUM
time across many rounds -- noise only ever adds time, so the min is the
cleanest estimate of true speed -- with a per-round win-count as corroboration.
That methodology overturned two single-pass "wins" as pure measurement noise:
accumulator banking (x1 == x2 == x4) and slice bounds-check removal both do
nothing here. Only the schoolbook-vs-Karatsuba product choice moves the number.
Also make the field_bench CLMUL-backend banner architecture-aware so it no
longer prints "numbers not meaningful" on x86 (which has a real pclmulqdq
path), and notes that path is 128-bit CLMUL, not VPCLMULQDQ.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
field: aarch64 NEON path for the binius64 tower (F128Txy)
F128Txy shipped with only an x86 pclmulqdq path and a software fallback, so
on aarch64 the field_bench head-to-head ran it in scalar software while the
Artin-Schreier tower (F128T) used NEON PMULL — a ~60x apples-to-oranges gap
that made the tower comparison meaningless on M-series.
Add a NEON kernel that mirrors tower_f128's so only the degree-2 fold
differs: 3-PMULL Karatsuba products, then each output limb reduced with the
same two-PMULL tail. The binius fold y^2 = x*y + 1 folds one word fewer than
the Artin-Schreier x^61 fold (a 128-bit p1<<1 vs a 192-bit fold), so mul_neon
is 7 PMULL vs F128T's 8. Also route mul_unreduced and the deferred
kreduce_u128 through NEON, and add a test that both NEON paths (fused mul_neon
and mul_unreduced_neon + reduce) match the software reference over 10k inputs.
With the fold now measured fairly on M4 Max (field_bench C/B): binius wins the
throughput-bound muls ~0.87-0.92x and ties on latency/deferred — same
direction as x86 (0.58-0.85x) but a smaller margin, since abundant PMULL
throughput shrinks the one-word fold saving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
centralize Merkle root encoding
centralize F128 byte encoding
field: add binius64 degree-2 tower and bench it vs the Artin-Schreier tower
Adds F128Txy, a GF((2^64)^2) tower K[y]/(y²+x·y+1) — binius64's tower
choice — over the same base field K as tower_f128's Artin-Schreier
K[y]/(y²+y+x^61). Same 3-CLMUL Karatsuba products; only the degree-2
fold differs (a shift-by-x vs a multiply by x^61), so benchmarking the
two isolates the tower choice.
Carries only the ops the comparison needs (eager mul, the deferred-
reduction pair, square, inv), with an x86 pclmulqdq path plus a software
fallback, and tests for the defining relation, field axioms, deferred-
reduction linearity, and x86-vs-software agreement.
field_bench grows a third column (C: F128Txy) and a C/B ratio that
isolates the fold difference. On x86 the binius fold wins every mul-bound
metric (C/B 0.58–0.85x); the deferred inner-product loop is the lone
exception at ~1.13x, where per-term reduction is amortized away.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
simplify bus claim decomposition
share multilinear field helpers
naming
derive column sizes from layout sources
wip
field: add x86_64 pclmulqdq path for GF(2^64) and the F128T tower
F128 already had an x86_64 pclmulqdq path, but the tower field F128T and its
F64 base had only an aarch64 PMULL kernel plus a naive software fallback, so on
x86 every tower multiply ran software (~27 ns) while F128 used hardware clmul
(~2.4 ns). Since the prover runs its whole polynomial arithmetic over F128T,
that 11x per-mul gap made XMSS aggregation prove ~3x slower on x86 than on
main, despite identical VM work (same cycles, instruction mix, and memory).
Mirror the aarch64 kernels with x86_64 pclmulqdq:
- gf2_64: one CLMUL product + a two-CLMUL fold by 0x1B (= x^64 mod P).
- tower_f128: 3-CLMUL Karatsuba over the scalar coefficients, then the tower
reduction c0 = reduce(p0) + x^61·reduce(p1), c1 = reduce(pm + p0); plus the
deferred-reduction kernels (mul_unreduced/reduce, mul_base_unreduced/reduce)
the sumcheck hot loop uses. Wired behind
cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq")); aarch64 and
the software fallback are unchanged.
Verified against the software oracle (new x86_variants_match_software test,
10k random cases: mul, square, and both deferred paths) and the full workspace
suite (304 tests). On this AMD box, xmss --n-signatures 820 goes 53.6 -> 136.3
XMSS/s (proving 15.3s -> 6.0s); the deferred inner product goes from 11.7x to
1.10x of F128.
Credit: binius64 <https://github.com/binius-zk/binius64> for the GF(2^64)
base-field CLMUL and the deferred-reduction structure (its Monbijou field is
this same GF(2^64)); the degree-2 extension differs (ours is Artin-Schreier
y^2+y+x^61, not their y^2+xy+1), so the extension reduction follows our field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reuse compiler constant pool
simplify challenge and table construction
deduplicate deterministic test rngs
generate phi8 table from basis images
Merge branch 'main' into field-exploration
Brings in the signers-cache footprint fix, main's "unify equality table
construction" and "make recursive proof verification atomic" refactors.
Conflict resolutions (11 hunks, 5 files):
- signers_cache.rs: clean merge (the fix originated on both sides identically).
- multilinear.rs, constraints.rs, gkr.rs, pcs/ligerito.rs: kept the
field-exploration (dual-lane) versions. main's changes here were purely the
eq_table -> build_eq rename from "unify equality table construction", which
merged the base's two eq-table constructors into one. That unification is
invalid under the dual-lane design: here eq_table (over F128T, the VM
constraint lane) and build_eq (over F128, the PCS lane) are DISTINCT
functions for different field types, not duplicates. Taking main's rename
would also have dropped ligerito's `crate::as_ghash` conversions that map
tower-field samples into F128 for the PCS, breaking the K PCS path.
- rec_aggregation/lib.rs: took main's new atomic-verification exports
(RecursiveProof, RecursiveVerifyError, run_recursion) but kept them under
field-exploration's `#[cfg(feature = "recursion")]` gate, since the n->1
recursion harness still awaits the 64-bit guest port. recursion.rs itself
auto-merged to main's version (compiled only with the feature).
Verified: `cargo build --release` (default features) and `cargo test
--workspace` both pass (pcs 98, primitives 50, lean_vm 16, flock 74,
rec_aggregation 3, xmss 7; no failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
signers cache: fold hash-construction fingerprint into the footprint
The signer cache filename hashed only the declared XMSS parameters (slot,
key range, message, V/W/CHAIN_LENGTH/...). Those are identical across
branches that change the hash *construction* underneath — e.g. the
Merkle-Damgard IV switching field width (GF(2^128) <-> GF(2^64)) rewrites
every digest while touching no constant. Both branches then computed the
same filename and the serialized sizes match, so a run on one branch loaded
the other's cryptographically-invalid signers and panicked in verification.
(cargo clean masked it by wiping the poisoned file.)
Fold a known-answer test vector of the real primitives (one tweak_hash, one
md_tweak_hash) into footprint(), so any change to digest behavior lands in a
distinct cache file. Per-branch caches now coexist instead of clobbering one
another. Two BLAKE3 compressions; negligible next to generating a signer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unify equality table construction
make recursive proof verification atomic
vm: 128-bit memory words (E) with 64-bit addressing (K)
Memory words become 128-bit elements of the tower E = GF(2^128) so a
recursion guest can do native 128-bit field arithmetic, while addresses,
the pc/fp, read counters, operands, opcodes, and domain separators stay in
the 64-bit subfield K = GF(2^64) (g-powers of order 2^64-1). This keeps the
branch's base-field-commit / extension-open PCS win: only data costs 128
bits, addressing stays 64.
Representation & protocol:
- Each 128-bit word is committed as its two K-lanes (_lo/_hi), two ordinary
F64 columns in the one stacked K-witness; the PCS/stacking/Merkle layer is
untouched. The E-value is reassembled (lo + hi*y) only in the constraint
zerocheck and at the opening point, never in the commitment.
- MUL_NATIVE = the E tower product, XOR = 128-bit xor, both written as
degree-2 E-relations on the value lanes, so the zerocheck is unchanged.
- On the bus a memory value rides two K-coordinates (widest tuple stays 9);
the memory image is two lanes; the public-input binding is one claim per
lane.
- Words used as addresses (DEREF pointer, JUMP destination/frame) are forced
into K by the single-lane address/selection constraints, and witness-gen
errors if such a word has a nonzero high lane.
- BLAKE3: a 128-bit chunk is one cell, so a row reads six cells (four inputs,
two-cell digest); the 12 value lanes map 1:1 onto the same 12 flock q_pkd
slots (flock packing unchanged).
DSL:
- 128-bit literals: an integer literal is the raw 128-bit bit pattern
(low 64 = K-lane, high 64 = y-lane); 2**64 is the tower generator y, so a
guest packs two lanes as `lo + hi * (2**64)`. KVal::Const, lit_field,
parse_const, try_field_const, const_cell, Alias::Const, and fconsts migrate
F64 -> F128T; g-power/address constants embed via .into().
- blake3 lowering: a 256-bit operand is two 128-bit cells (chunk = one cell,
digest = two cells).
Guests: all hashing guests rewritten to the 2-cell layout and revalidated
(6 blake3 .py programs, the stack_buf suite, the hash_chain benchmark, and
the xmss aggregation guest + driver — WORDS_PER_VALUE 2->1, WORDS_PER_BLOCK
4->2, the WOTS encoding reconstructed into one cell via the y literal). The
`recursion` harness stays feature-gated off (awaits its own guest port).
Docs: doc.tex, transition-to-64-bits.tex, and zkDSL.md updated to the hybrid
design.
Full workspace test suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge branch 'main' into field-exploration
Reconcile main's 8-crate workspace restructure + one-shared-transcript
protocol with this branch's 64-bit field transition (K = GF(2^64) machine
words opened over the tower E = GF(2^128)).
Structural: adopt main's `crates/*` layout and `fiat_shamir` crate; the
K-variant PCS files (ligerito_k, ring_switch_k, stack_open_k, pack_k,
tensor_algebra_k, additive_ntt_f64) and the tower field modules move under
crates/{pcs,primitives}. Hint machinery (RHint, GPowMap, grow_gpow) lives in
lean_vm::cpu::hints. Bench binaries move into their owning crates.
Field/transcript: the shared Fiat-Shamir sponge is F64-word / F128T-scalar;
the GHASH-typed F128 consumers (flock zerocheck/lincheck, the F128 Ligerito
twin) ferry through raw lane lifts (pcs::as_e / as_ghash) at their transcript
touchpoints - no arithmetic crosses representations. flock's reduction and
the F128 Ligerito are generic over the opening type so lean_vm can drive them
with its stacked Ligerito-K proof on one transcript.
Protocol: keep main's RLC-batched three-tree bus GKR, dedup'd claim
decomposition, and single-seed statement binding, retyped to E; keep the
branch's K-committed stacked opening as the sole PCS path (the F128 stacked
opener stays for its own tests).
Deferred: the n->1 recursion harness (crates/rec_aggregation/src/recursion.rs
+ guests/recursion.py) is gated behind the `recursion` feature - it dissects
the proof stream word-for-word and needs porting to the 64-bit-word layout.
Tests: workspace builds warning-free; primitives/fiat_shamir/pcs/flock/
lean_vm/lean_compiler/xmss and the XMSS-aggregation + fibonacci harnesses all
pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recursion guest: runtime-sized buffers replace the linear capacity caps
HeapBuf sizes carried in the exponent (HeapBufDyn: the alloc hint does an
O(1) reverse g-power lookup at witness generation, erroring past 2^20
cells) already existed - the guest now uses them wherever the exact size
is a certified runtime g-power at declaration time:
- zerocheck_r / flock_point chains: exactly m = K_LOG + tau_5 slots
(mr1cs_g); zerocheck_rhos / flock_round chains: exactly m - 6
(mr1cs_g * INV_GEN^6). R1CS_M_CAP and R1CS_ROUNDS_CAP die.
- zeta and the nine GKR per-layer chains: exactly mu (+2) slots
(g_bus_mu), zeta's declaration moving below the certified bus depth.
What stays capacity-sized, and why: STREAM_CAP / YR_SLOT_STRIDE size
hint pools (witness-hint lengths are compile-time); QPKD_VARS_CAP and
MU_CAP are row STRIDES of two-row / triangle layouts (a runtime stride
would unfold every access); GKR_ROUNDS_CAP / GKR_POINTS_CAP are
quadratic in mu (a runtime g-power for them needs exponent-domain
multiplication, all cost, no cycles saved).
recursion_2to1 cycles 2,516,990 -> 2,517,066 (+76: one MUL per
materialized runtime size) against exact allocations. Suite 244 + 3
ignored green, heavy recursion suites pass, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge pull request #2 from leanEthereum/recursion
Recursion
ligerito: the fold constants are one knob each - INITIAL_K=5 works
Changing INITIAL_K / LEVEL_K / RESIDUAL_MAX_LOG in crates/pcs now
propagates everywhere:
- lean_vm's LOG_BATCH and LOG_INV_RATE stop re-declaring their own 6 / 1
behind compile-time asserts: they ARE ::pcs::ligerito::INITIAL_K /
LOG_INV_RATE_0 (the Merkle leaf width and the L0 fold count are one
knob by construction - the L0 commit is reused). The blake3_batch test
reads the constants too.
- recursion_soundness_binds stops hardcoding the residual shape: the
rs_yslot_bits tamper index (the first PADDING slot coordinate, k =
yr_log_n) is derived from the fold ladder, and the tamper is skipped
when this candidate's yr_log_n equals YR_LOG_CAP (the guest reads only
YR_LOG_CAP coords, so no pad coordinate exists and the over-read path
it guards is absent - exactly what happens at INITIAL_K=5).
Verified by actually flipping the knobs: the FULL workspace suite, both
heavy recursion suites, and the xmss CLI all pass at INITIAL_K=5 (the
guest re-derives its baked tables from the config, so recursion just
works); pcs + rec_aggregation pass at LEVEL_K=4. Shipped values are
unchanged (6 / 3 / 5), so derived configs, proofs and cycles are
byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ligerito: LigeritoProfile dies; the shipped shape becomes named constants
Only one configuration ever ships (the old Secure profile), so the
selector enum and its Fast/Slim derivation arms go. What was implicit
now has a name in crates/pcs/src/ligerito.rs:
- SECURITY_BITS = 120 round-by-round soundness target
- LOG_INV_RATE_0 = 1 L0 code rate 2^-1
- QUERY_GRINDING_BITS = 18 query-phase PoW (queries cover 120 - 18)
- INITIAL_K = 6 the INITIAL folding factor (L0 folds 2^6
interleaved rows; = PcsParams::log_batch_size)
- LEVEL_K = 3 the folding factor of every subsequent level
- RESIDUAL_MAX_LOG = 5 folding stops at <= 5 remaining variables:
the residual yr (<= 32 coeffs) is sent in clear
derive_profile(m, profile) -> derive_config(m); PcsParams loses its
profile field; lean_vm pins its LOG_BATCH / LOG_INV_RATE / SECURITY_BITS
to the pcs constants with compile-time asserts. The SoundnessRegime
enum and the Johnson/OOD analysis + prover/verifier machinery stay for
hand-built configs (the OOD roundtrip test constructs them directly);
only the profile-driven derivation of them is gone.
Derived configs are value-identical to the old Secure profile, so
proofs and the recursion guest are untouched (cycles unchanged). Suite
244 + 3 ignored green, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recursion verifier: derive positions-per-word from depth
The Ligerito query decoder took per_word as a separate Const, but it is
just FIELD_BITS / depth. Derive it inside decode_query_bits and drop the
LIG_POSITIONS_PER_WORD placeholder (guest table + Rust export + caller
arg); the write-offset now uses FIELD_BITS // LIG_TREE_DEPTH directly.
Pure refactor: identical 128/depth packing, identical emitted code, no
cycle change. Removes a redundant parameter and a placeholder threaded
through the guest; the decode specialization collapses from
__L{depth}_L{per_word} to __L{depth}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
zkDSL: print(...) - a prover-side debug print
`print(expr)` / `print("label", expr)` displays a value at witness
generation: the expression evaluates into a cell and a Print hint hangs
on a one-instruction no-op anchor, so it fires exactly at that program
point, only on the executed path, and adds no constraints (nothing
enters the transcript; proving/verification are untouched). The label
defaults to the argument's source text. Output goes to stderr as
`[print] label = ...` with the friendliest reading: decimal for small
integers, g^k for small g-powers, both when they overlap (8 = x^3 shows
as `8 (g^3)`), hi:lo hex otherwise, `<unwritten>` for a cell nobody
wrote. Allowed inside @inline bodies and loop bodies (free vars flow
through). Documented in zkDSL.md; test: tests/print_debug.rs.
Suite 244 + 3 ignored green, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
compiler: @inline calls in expression position; eq_weight dedup covers all 4 sites
Two compiler-level changes, found while deduplicating the recursion
guest's eq-tensor weight loops:
1. @inline calls now work embedded in expressions. An inlined tail
return of ANY plain var recorded a RetBind::Gaddr alias (gaddr_of
treats every runtime cell as an exp-0 g-address), which only let /
tuple bindings consumed; a call inside an expression (7 * f(x), a
store's RHS, a single-target match arm) read its never-written dst
cell and the stale bind mis-bound the NEXT let. Expression positions
now consume the bind (take_inline_ret_cell), materializing aliased
returns (free for a var; one MUL for a shifted pointer); Stmt::Let
intercepts bare user-fn calls first, keeping the zero-copy alias
hand-over of the obs / fs_next idiom byte-identical; multi-target
match_range arms materialize into their join cells. Regression test:
tests/inline_expr.rs (proves + verifies all three shapes).
2. A compile-time heap index that folds to a non-g-power field constant
(buf[0], buf[2], an integer loop var leaking in from a StackBuf
conversion) is now a compile-time error instead of a wild-pointer
DEREF at proving time - heap cell k is addressed as buf[GEN ** k].
Exactly this bit during the dedup: converting bytecode_sel to a
HeapBuf left one plain-int read in the defer_out fill, and the k=0
read lowered to address*0, failing 78k instructions later with zero
compile-time signal. Regression test: tests/heap_index.rs.
The guest's four eq-tensor weight loops then collapse into one @inline
eq_weight(ch, count, idx, msb_span) helper used straight in expression
position: row fold weights, query alpha weights, the bytecode selector
reduction (bytecode_sel now a HeapBuf), and the lincheck const-pin term
(msb_span = K_LOG selects its MSB-first bit walk).
recursion_2to1 guest cycles 2,516,950 -> 2,516,990 (+40: the selector's
three cells moved from frame to heap). Suite 243 + 3 ignored green,
heavy recursion suites pass, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wip
protocol: every Ligerito scalar rides the stream, the opening keeps only hints
LigeritoProof was the last pre-stream record struct: sumcheck messages,
OOD values, the final message yr, the level roots and both PoW nonce
families were transported in the struct and manually re-observed by the
verifier. All of them now ride the shared transcript stream at their
protocol points - add_scalar/next_scalar for the scalars (bound as
written/read), the commit-root two-scalar convention for level roots,
and grind/grind_check for the fold + query nonces (raw transport, bound
by the grind itself; query grinds push a nonce even at 0 bits, as
before). The opening object keeps only the hash-bearing hint data:
opened rows + Merkle multi-proofs per level. initial_root leaves the
proof entirely - the verifier uses the expected root it already has.
Ripples:
- SumcheckProver drops its internal transcript accumulation; tests
collect the returned messages instead, and the roundtrip tests drive
the verifier from the prover's bundle (no more detached/empty-stream
verification). Tamper tests corrupt stream words at their layout
positions, and lean_vm's validity tamper test corrupts an opened row.
- The recursion harness stops extracting lig_sumcheck_msgs / final_msg /
level_roots / fold+query nonces into hint pools, and its lincheck
tail-offset slice now skips the opening's stream words (counted from
the config shapes).
- The guest's open_stacked takes the main stream cursor: msg_cursor IS
the stream, level roots are two fs_next words (replacing the 3-absorb
byte form), yr fills from the stream, and grind nonces are raw cursor
reads bound by their DS_POW absorb. Six hint pools die.
recursion_2to1 guest cycles 2,537,008 -> 2,516,950. Suite 241 + 3
ignored green (both heavy recursion suites pass), clippy clean, xmss
CLI at 157.8 XMSS/s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
flock: re-add the standalone batch BLAKE3 proving test
The purge (2956d38) deleted tests/blake3_bench.rs along with the
standalone flock prover API it exercised (prove_fast_timed, flare
transcript, statement_digest). This brings the workload back on today's
API: witness-gen -> commit -> Blake3Setup::prove_validity_stacked
(zerocheck + lincheck reduction, one stacked Ligerito open) ->
verify_validity_stacked, over FLOCK_N compressions (default 8192, Secure
profile) with per-phase timings and a verify + finish gate - the fast
loop for iterating on the flock reduction / PCS without the xmss harness.
RAYON_NUM_THREADS=11 FLOCK_N=131072 cargo test --release -p flock \
--test blake3_batch -- --nocapture
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lean_vm: the Ligerito proof moves out of RingSwitchVerify
RingSwitchVerify becomes a plain claims bundle (no lifetime): the opening
proof is not ring-switch data, it is the ONE stacked proof covering the
slot claims too, so pcs::verify now takes it as its own parameter. The
caller (cpu::verify) reads it off the openings hint channel at its
protocol point and passes the borrow straight through - dropping the
.clone() that copied the whole Merkle-bearing proof (next_opening returns
&'a tied to the deserialized Proof, not to the VerifierState borrow), and
the stale "BLAKE3 attachment" field doc with it.
No protocol change. Suite 240 + 3 ignored green, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recursion guest: baked prefix-mask table replaces the hinted overlap mask
claim_overlap_mask's content is fully determined by the already-pinned
claim_nover (the prefix of exactly nover ones), so instead of hinting +
certifying it, the guest bakes ALL cap+1 possible prefix masks into one
constant table (row t = [k < t]) and each claim selects row nover by
pointer arithmetic (nover_g ** YR_LOG_CAP, a 3-MUL square-and-multiply).
Mask reads stay one DEREF; the hint stream and the booleanity,
monotone-prefix, and popcount pins all vanish (the prefix property holds
by construction). The hi-span zero asserts stay and double as the
nover <= yr_log_n pin: a larger nover selects a row whose prefix reaches
into the asserted span. nover's range check tightens from SIZE_BITS to
YR_LOG_CAP + 1, which also pins the row select to the table.
(A first attempt wrote the mask with two runtime mul_range loops per
claim; loop-call overhead cost +16.5k cycles, so it was dropped for the
table select, which is straight-line.)
The host stream and its tamper case in recursion_soundness_binds go
away; the mask is no longer prover-chosen and the claim_nover tamper
covers the overlap path.
Parser: an integer log bound folds like any parse-time size, so
`assert log x < YR_LOG_CAP + 1` works.
recursion_2to1: 2534682 -> 2516928 cycles (-0.7%), bytecode 314764 ->
305887 instructions. recursion_2to1(+mixed) and recursion_soundness_binds
pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zkDSL: list literals — `x = [a, b]` is an initialized StackBuf
New sugar for the alloc-then-store idiom: `x = [a, b, …]` allocates a
StackBuf of the element count and writes each element in place, through
the same stack-store path (copies/constants still defer as aliases), so
the emitted code is identical to the manual form. Elements lower before
the name rebinds, so `s = [s[1], s[0]]` swaps through the old binding.
The recursion guest drops ~25 three-line StackBuf init blocks for it;
bytecode is unchanged (314764 instructions, same committed witness).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recursion guest: walk the flock message regions in order too
The flock zerocheck multilinear-round messages (zc_msgs) and lincheck-round
messages (lincheck_msgs) were the last "reserve region, offset-read later"
sites: `zc_msgs = cursor` / `lincheck_msgs = cursor` plus a bulk skip. Walk them
in stream order with fs_next instead. The unroll rounds walk the cursor
directly; the flock multilinear mul_range threads it through a new
flock_round_cursor heap chain, exactly like the GKR/AIR rounds thread
gkr_round_cursor. The `cursor *= nmlv_g * nmlv_g` and
`cursor *= GEN ** (2 * LINCHECK_ROUNDS)` skips are gone; the cursor now walks
every stream word in order, and the only remaining cursor moves are the
heap-chain seeds and the raw grinding nonce.
Transcript unchanged: recursion_2to1 / _mixed verify with the committed witness
identical (2^26.079).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
recursion guest: fs_next fetches, observes, and advances in one step
Rename read_obs -> fs_next and have it return the advanced cursor too, so a
stream walk is `fs, x, cursor = fs_next(fs, cursor)` with no manual
`cursor *= GEN`. Every per-word read now walks: sequential regions thread a
running cursor; fixed-base per-round loops (flock zerocheck/lincheck, the two
aggregation sumchecks) thread within each read-pair, which also drops the
`* GEN ** (2i+1)` offset multiplies. The three "reserve region, observe it as a
block later" sites (zerocheck round-1, lincheck z_partial, ring-switch s_hat_v)
now fetch-and-observe as they walk, into small HeapBufs. The only manual cursor
moves left are genuine region-skips and the raw grinding nonce (absorbed later
as DS_POW).
Transcript unchanged: recursion_2to1 / _mixed prove and verify with the
committed witness identical (2^26.079).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
compiler: inline multi-return can alias-return a g-address
Generalize the inline tail-return binding so each returned value binds into the
caller exactly as a `let name = <expr>` would: a `StackBuf` hands over its cell
run, a folded g-address hands over its symbolic pointer, both aliased at zero
copies; a scalar is copied into its dst cell. Previously only a StackBuf could
be alias-returned (and only as the sole return value); a g-address among several
returns would materialize into a cell, losing the free compile-time fold and the
compile-time bounds check.
Replaces the StackBuf-only `Option<Vec<Option<(Off,u32)>>>` record with a
`RetBind` enum threaded through lower_return / Stmt::Let / Stmt::LetTuple. New
stack_buf test `inline_returns_advanced_cursor` covers returning
(StackBuf, scalar, advanced-cursor) together and reading through the cursor.
This is what lets the recursion guest's stream walker return the advanced cursor
for free: `fs, x, cursor = fs_next(fs, cursor)`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
transcript: drop the flock/pcs sub-protocol domain-separation labels
The zerocheck, lincheck, pcs-open-batch, packed-direct, ring-switch, and
ligerito-basis sub-protocols each absorbed a `b"flock-*-v0"` label into the
Fiat-Shamir transcript. These were redundant: the transcript is a strict hash
chain with a fixed, non-adaptive verifier structure, so a segment's position
already identifies which sub-protocol it belongs to, and the top-level seed
(the flock-family + program digest) separates this proof system from any other.
Remove all six labels symmetrically from prover and verifier (crates/flock and
crates/pcs) and from the recursion guest's replay, plus the now-dead guest
label constants, harness placeholders, and word16 helper.
flock (74), pcs (62), lean_vm (15), and rec_aggregation (recursion_2to1 +
_mixed) all still prove and verify. The recursion guest shrinks 315445 ->
314920 instructions (1008 fewer BLAKE3 absorbs); a pass requires the native
proof and the guest replay to agree, confirming the removal is consistent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: give recursion DSL placeholders descriptive names
Pure mechanical rename of cryptic placeholders in recursion.rs and the
xmss_aggregate/recursion guest; no behavior change.
- N_INNER_ROUNDS -> N_FIXED_CHALLENGE_ROUNDS, INNER7 -> FIXED_CHALLENGES,
I7INV -> ONE_PLUS_CHALLENGE_INV
- {ZC,LC,OB,RS,PD,LIG}LBL A/B -> {ZEROCHECK,LINCHECK,OPEN_BATCH,
RING_SWITCH,PACKED_DIRECT,LIGERITO_BASIS}_LABEL_0/1
- GG dropped in favor of the builtin GEN; GINV -> INV_GEN
- ILD0/1/2 -> LAGRANGE_INV_0/1/2; ILAM/ICMB/ISDOM ->
LAGRANGE_INV_{LAMBDA,COMBINED,S}
- PHI -> PHI8_NODES; DELTA -> TRACE_DUAL_BASIS; SEEDB0/1 -> TRANSCRIPT_SEED_0/1
- merge duplicate NCL/NCLAIMS into a single N_CLAIMS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fiat-shamir: fuse stream read + sponge absorb into one read_obs
The recursion guest repeated `x = cursor[..]; fs = obs(fs, x); cursor *= GEN`
at every proof-stream read, maintaining the "observe before it influences a
challenge" invariant by hand at each site. Fuse read+observe into one
`read_obs(state, cursor) -> (state, x)` so a stream word cannot enter the
computation unbound.
That needs an @inline to return the sponge state (a StackBuf) alongside the
consumed scalar, which the lowering forbade: it aliased a returned StackBuf
only when it was the SOLE return value, and a multi-value return routed every
element through expr() (panicking on a StackBuf). Generalize `inline_stack_ret`
from one `Option<(Off,u32)>` to per-slot `Option<Vec<Option<(Off,u32)>>>`, so
each returned value is independently a StackBuf alias or a scalar cell;
lower_return / Stmt::Let / Stmt::LetTuple bind each accordingly. `fs` stays a
StackBuf and obs/squeeze/absorb are unchanged.
Guest transcript is unchanged: the recursion guest compiles byte-identically
(315445 instructions, same MUL/DEREF counts), and recursion_2to1 /
recursion_2to1_mixed still prove and verify. New stack_buf test covers the
inline StackBuf+scalar tuple return in isolation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
naming
feat: cache XMSS benchmark signers to disk
Signer generation (keygen over slots 0..=15 + grinding sign) is the
untimed setup cost of the aggregation benchmark and dwarfs proving for
large batches. Generate each signer once, store the batch to disk under
target/signers-cache/, and reload on later runs.
The cache is a growing pool keyed on a footprint of the generation
parameters (not on n), so bumping LEANVM_XMSS_N only generates the new
tail and never re-does a smaller run's work. Also memoized in-process.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
benchmarks
perf: fs_seed no longer rebuilds + rehashes the circuit family per process
82f4f74 made every transcript seed depend on the flock circuit-family
digest, whose OnceLock init called build_block_r1cs(3) and hashed the
matrices entry by entry: ~200 ms of symbolic matrix building (a second
copy, next to the setup's own) plus ~250 ms of per-entry blake3::update
calls over 21M nonzeros, landing inside the first prove. On XMSS x820
that cost ~6% on a 16-core x86 box and ~25% on an M4 Max (1.6 s prove).
Three fixes:
- flock::blake3::FAMILY_DIGEST: the digest is a constant of the circuit
family, so bake it; family_digest_matches_baked recomputes and compares,
failing on any circuit change until the constant is bumped. Same value
as before, so fs_seed and the recursion guests are untouched.
- build_block_r1cs clones the cached matrices() instead of re-running the
symbolic builder: setups and const-pin lookups share one build per
process (200 ms -> 72 ms per extra call).
- absorb_matrix flattens to one buffer before hashing (244 ms -> ~10 ms),
keeping the byte stream (and digest) identical.
XMSS x820, 11 threads, x86: 5.55 s before 82f4f74, 5.87-5.94 s after,
5.46 s now. Suite 235 + 3 ignored green, recursion suites pass, clippy
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
polish: apply ~104 review findings across all crates
Comment/naming/dead-code pass over the new workspace:
- fix stale docs referencing purged code (BaseFold targets, jagged,
default_config, verify_succinct, prove_batched, record structs)
- re-attach doc blocks that had drifted onto the wrong item
- rename ceil_log2 -> log2_ceil (naming convention), LigeritoLevelConfig
k_levels -> k (scalar, not a collection), VerifyError::PcsAb -> Pcs
- delete leftovers: empty impl blocks, unused locals, apply_triple (+test),
dead rounds vectors in lincheck, fold_packed_witness_at_z
- gate test-only items (#[cfg(test)]): stacked prove/verify wrappers,
witness::stack, udr_queries; keep the general NTT entry points pub
- fix stale crate paths in misc/doc.tex
cargo test --workspace: 234 passed + 3 ignored; ignored recursion suites
pass; clippy warning-clean across all targets.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
restructure: vendor/ dies, the repo becomes 8 clean workspace crates
crates/
primitives GF(2^128)/GF(2^8) field kernels (+ phi8, g-power helpers),
bit transposes, multilinear/eq/Lagrange helpers, the scratch
pool, alloc/log2/perf utilities
fiat_shamir the VM-native sponge + the ProverState/VerifierState
…
TomWambsgans
force-pushed
the
small-proof
branch
from
July 22, 2026 23:32
3a4f981 to
c834012
Compare
main's `faster Ring Switching verifier for GHASH` is specific to the field
this branch removed, so its math does not carry over. Resolution per file:
* `pcs/ring_switch.rs`, `pcs/lib.rs`: ours. The new closed form
(`linearized_eq_coeffs_eq` and its 15 seed constants) needs two things
this branch no longer has. It needs a *power* basis of the opening field,
built from the sparse GHASH modulus x^128+x^7+x^2+x+1, whose trace-dual
is reversed monomials plus a 7-term correction; our E is the degree-3
tower over GF(2^64), so its F_2 basis is the product basis
{base[i] * tower[j]}, not alpha^i. And it needs *eq* coefficients, so
that the 128-term sum splits into a 7-factor product; we batch the
coordinates univariately with (1, rho, ..., rho^191) instead. Our
counterpart optimization is already in: the tower factorization
delta[64j+i] = base[i] * tower[j] splits c_k into two Horner
evaluations. Main's `RingSwitchVerifierOutput` removal also does not
apply, since ours carries `coordinate_weights` rather than an
`eq_r_dprime` tensor, and the stacked verifier now lives in
`pcs/stack_open.rs`.
* `guests/recursion.py`: ours. Same reason, plus git silently took
main's `c_table = StackBuf(FIELD_BITS)` outside any conflict hunk;
our c_table is written and read at runtime exponents from
`mul_range`, so it must stay a HeapBuf.
* `rec_aggregation/src/recursion.rs`: ours, taking main's switch to
`parse_with_replacements(include_str!(..))`. That part is
field-independent: it drops the runtime read of the guest source from
CARGO_MANIFEST_DIR. Kept our `TRACE_DUAL_BASE`/`TRACE_DUAL_TOWER`
placeholders over main's `RS_COEFF_ORBITS`, and kept the guest cache.
The generalized ring-switching note and its CI job come in unchanged; its
new closing section documents the GHASH closed form as a specialization of
the general construction this branch instantiates at f = 64, e = 192.
Full workspace test suite passes, including the two `#[ignore]`d heavy
recursion tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main's GHASH closed form (93a4f56) leaned on two properties this branch does not have: a power basis of the opening field, and eq weights whose 128-term sum factors into a 7-fold product. Both have analogues here, so the technique carries over after all; only Step 3 of the note is really weight-specific. Step 2 is generic in the modulus. For q = X^64+X^4+X^3+X+1 we have d = q'(x) = x^2+1, and the Laurent expansion of 1/q at infinity gives Tr(x^m/d) = [m = 63] for every m < 120, so beta_i is again a reversed monomial plus a sparse correction, this time on i < 4 rather than i < 7: beta_i = d^-1 (x^(63-i) + eps_i), eps = [x^3+x^2+1, x^2+x, x+1, 1, 0, ..., 0]. Step 3 changes shape. We batch the 192 coordinates univariately with rho^w, not with an eq tensor, so the weights do not factor over the bits of i and there is no product form. What replaces it is a geometric series: sum_{i<64} u^i = (u^64+1)/(u+1) = (1+u)^63, since 64 is a power of two, an identity that holds for every u including u = 1 where both sides vanish. With u = rho x^(-2^k), A_k = Q_k (1 + rho P_k)^63 + sum_{i<4} rho^i R_k[i]. (1+u)^63 by the 2^t-1 addition chain is 5 squarings and 3 products, so a level costs 13 multiplications: the same O(log f) the eq product reaches, via a different identity. The tower half is unchanged in principle but now also baked. gamma_0 = 1 (Tr_{E/K}(1) = 3 = 1 while Tr(y) and Tr(y^2) are symmetric functions of the roots of y^3+y+1 and vanish), which is also why beta lies in K and A_k has Frobenius period 64. Both orbits are fixed, so the guest bakes all 64 base levels and 192 tower levels instead of squaring a runtime orbit table: the 64x64 and 3x192 Frobenius tables in main() are gone along with their 4.6k products, and verify_sub no longer takes them. Also ported the field-independent half of 93a4f56: eval_rs_eq walks the runtime coordinates outside and the fixed 192 Frobenius powers inside, so z evolves by squaring in registers against one contiguous product row instead of through a stored z-power table. That removes roughly half the memory traffic of the hot 192 x ~38 nest, plus 384 per-level buffer allocations. Same product formula, same result. RAYON_NUM_THREADS=11, recursion --n 2, guest cycles: 2,196,207 before 2,142,542 closed-form coefficients -2.4% 1,977,096 + eval_rs_eq restructuring -9.97% total MUL 710,692 -> 641,783, DEREF 693,814 -> 591,205, data memory 2^21.322 -> 2^21.166. Program grows 609,056 -> 614,490 instructions, still inside the 2^20 pad. Short of main's >15% because the starting point was already better: the tower factorization and the period-64 base had cut coefficient construction to 9.2k products, where main's dense 128x128 Moore transform cost 16.4k. The remaining ring-switch cost is the transposed claim, which is 64 x 192 genuine products either way. New unit test checks the closed form against the dense 192-term Frobenius sum at every level. Full workspace suite passes, including the two ignored heavy recursion tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seventeen warnings across five crates, all pre-existing. None were behavioural; recursion --n 2 still reports 1,977,096 guest cycles and a 376.539 KiB proof. * `pcs/ntt/additive_ntt_f64.rs`: annotate the ten NEON transmutes (missing_transmute_annotations). Only the inner ones in argument position were flagged; the outer ones are pinned by their let binding. uint64x2_t -> poly64x2_t is what vmull_high_p64 wants. * `flock/blake3.rs`: drop three needless borrows. The bindings are already `&[F192]`, so `&a_packed_words` was `&&[F192]`. * `flock/zerocheck/univariate_skip_optimized.rs`: allow identity_op on `xor_apply_byte_into_8_regs`. `0 ^ BH` is the i = 0 case of the `i ^ BH` row-select pattern, and writing it out keeps the four loads visibly parallel; a bare `BH` would hide that. * `pcs/merkle.rs`: `N % 64 == 0` -> `N.is_multiple_of(64)`. * `rec_aggregation/recursion.rs`: `stack_mus` is read only by `recursion_soundness_binds`, which needs the selected dispatch candidate to place its residual-coordinate tamper. Marked `cfg_attr(not(test), allow(dead_code))` rather than deleted. * `xmss`: drop the `primitives` dependency, which nothing in the crate referenced. Also carries an unrelated README edit made alongside: the XMSS aggregation example now runs 890 signatures. `cargo clippy --workspace --all-targets` is clean in both dev and release, as is plain `cargo build --all-targets`. All 263 tests pass, plus every ignored test except `print_ligerito_query_counts`, which is a manual parameter report that panics unless LIGERITO_LOG_INV_RATE and LIGERITO_NUM_VARS are set; it fails identically before this change and passes when given them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The default inner workload committed 2^25.06 words, so it padded to a 2^26 PCS with the top level almost empty. Any layout change that shaves a few percent of real data therefore drops the inner proof a whole PCS level, and a recursion measurement taken across that boundary compares a 2^26 opening against a 2^25 one rather than the verifier itself. 64000 iterations commits 2^25.655 here, filling most of the 2^26 bucket, so the figure stays comparable across layout work. The inner program's DEREF range check gives out just above 66000, so this is close to the ceiling this shape allows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in main's `Tail-call the mul_range loop helper` (1efa725). One conflict, in `lower.rs`: main added a `tail: bool` parameter to `lower_call`, this branch added return-shape assertions in the same `call_into` body. Kept both. Two further call sites needed the new argument -- one auto-merged, and `lower_call(.., physical.len(), ..)`, which only exists here because of the StackBuf-return work. `recursion_soundness_binds` then failed its own precondition: test config's yr_log_n (5) reached YR_LOG_CAP (5) Not a soundness break -- that assert exists so shape drift fails loudly instead of silently skipping the tamper. The tail call removes two instructions per loop iteration, which shrinks the inner witness enough to drop the test's committed log-size from 24 to 23, and 23 is one of the two candidates whose residual log sits exactly at the cap, leaving no pad coordinate to corrupt. Moved the config from (8, 2^13) to (8, 2^14), which restores stack_mu = 24 and yr_log_n = 3, so the over-read path is exercised again. recursion --n 2 on this branch: inner cycles 1,856,262 -> 1,728,250 -6.9% guest cycles 1,975,790 -> 1,961,840 -0.7% JUMP 35,935 -> 21,925 The inner figure matches 2*(64000-1) + 2*(8-1) = 128,012 exactly, as on main. The guest saves 2 cycles for each of ~7,000 tail-called iterations. Full workspace green: 263 tests plus both ignored heavy recursion tests. Clippy clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`{:.2}` applied to the String returned by pretty_f64 is a max-WIDTH spec,
not a decimal precision, so the ratio printed as "0." instead of "0.532".
Restore pretty_f64 across the whole report block (the pow closure and the
[inner] re-print had also drifted to raw {:.2} floats, making the re-print
disagree with the one prove_inner emits), widen the pow column back to 9
for the extra digit, and drop a duplicated committed-witness-size line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
using an extension field of 192 bits (a tower of degree 3 over F_2^64)