From 20b489b802e9fc4dca7e4b8043ad14d34cc15687 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 1 Jul 2026 21:56:09 -0300 Subject: [PATCH 01/52] stark: add field-generic constraint IR with builder and CPU interpreter New module crypto/stark/src/constraint_ir/ providing a flat, topologically ordered IR for transition constraints, generic over a field tower , E> (defaulting to Goldilocks and its degree-3 extension): - ir.rs: Op/Dim/ConstraintProgram. Constants live in base_consts/ ext_consts side tables referenced by index (Op::ConstBase(u32)/ Op::ConstExt(u32)), keeping Op a plain Copy+Eq+Hash payload with zero bounds on the fields. - builder.rs: IrBuilder with (Op, Dim) hash-consing, by-value constant dedup via linear scan (FieldElement's canonicalizing PartialEq), and the id-0 = base-zero convention. - interp.rs: forward-pass interpreter; eval_program / eval_program_verifier match the AIR compute_transition_prover / compute_transition contracts, eval_program_base is the minimal single-root entry point. Const ops read the side tables directly. Not wired into the prover or verifier; no behavior change. --- crypto/stark/src/constraint_ir/builder.rs | 282 ++++++++++++++++++++++ crypto/stark/src/constraint_ir/interp.rs | 267 ++++++++++++++++++++ crypto/stark/src/constraint_ir/ir.rs | 122 ++++++++++ crypto/stark/src/constraint_ir/mod.rs | 30 +++ crypto/stark/src/lib.rs | 1 + 5 files changed, 702 insertions(+) create mode 100644 crypto/stark/src/constraint_ir/builder.rs create mode 100644 crypto/stark/src/constraint_ir/interp.rs create mode 100644 crypto/stark/src/constraint_ir/ir.rs create mode 100644 crypto/stark/src/constraint_ir/mod.rs diff --git a/crypto/stark/src/constraint_ir/builder.rs b/crypto/stark/src/constraint_ir/builder.rs new file mode 100644 index 000000000..3b9d2d6eb --- /dev/null +++ b/crypto/stark/src/constraint_ir/builder.rs @@ -0,0 +1,282 @@ +//! Explicit-builder capture front-end. +//! +//! Every transition constraint is captured into a flat [`ConstraintProgram`] +//! through an explicit [`IrBuilder`]: each constraint translates its algebra +//! into builder calls (`main`, `add`, `sub`, `mul`, ...). No fake field, no +//! thread-local arena. +//! +//! The builder hash-conses every node on `(Op, Dim)` and only emits leaves for +//! columns the constraint actually reads, so captured programs are minimal. +//! Field constants live in the [`ConstraintProgram`]'s `base_consts` / +//! `ext_consts` side tables; the builder deduplicates them by value via a linear +//! scan (`FieldElement`'s canonicalizing `PartialEq`) — the tables are tiny and +//! capture runs once at setup, so no hash map is needed there (and none would be +//! sound: `FieldElement`'s derived `Hash` and manual `Eq` disagree on +//! non-canonical representations). + +use std::collections::HashMap; + +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as GoldilocksExtension; +use math::field::goldilocks::GoldilocksField; +use math::field::traits::IsField; + +use super::ir::{ConstraintProgram, Dim, Op}; + +/// A handle to a node in an [`IrBuilder`]: its arena id and result dimension. +/// +/// `Copy` so constraint bodies read like ordinary field arithmetic. +#[derive(Clone, Copy, Debug)] +pub struct Expr { + id: u32, + dim: Dim, +} + +impl Expr { + /// The node's result dimension. + pub fn dim(self) -> Dim { + self.dim + } +} + +/// Builds a [`ConstraintProgram`] from explicit node-construction calls. +/// +/// Nodes are appended in topological order (id `i` references only `< i`) and +/// hash-consed on `(Op, Dim)`, so structurally identical subexpressions share a +/// single id. Field constants are deduplicated by value in the `base_consts` / +/// `ext_consts` tables (linear scan). Node id `0` is reserved for the base-field +/// zero (`Op::ConstBase(0)`, `base_consts[0] = 0`), matching the interpreter's +/// convention. +pub struct IrBuilder { + nodes: Vec, + dims: Vec, + cse: HashMap<(Op, Dim), u32>, + base_consts: Vec>, + ext_consts: Vec>, + roots: Vec, + /// Set by [`Self::mark_unsupported`] when a constraint couldn't be + /// captured. Propagated to [`ConstraintProgram::complete`] so callers know + /// not to interpret an incomplete program. + complete: bool, +} + +impl Default for IrBuilder { + fn default() -> Self { + Self::new() + } +} + +impl IrBuilder { + /// Create a builder with the reserved base-field zero node at id 0. + pub fn new() -> Self { + let mut b = IrBuilder { + nodes: Vec::new(), + dims: Vec::new(), + cse: HashMap::new(), + base_consts: Vec::new(), + ext_consts: Vec::new(), + roots: Vec::new(), + complete: true, + }; + // Reserve id 0 = ConstBase(0) = base-field zero. `const_base(0)` will + // dedup to this. + let zero = b.const_base(0); + debug_assert_eq!(zero.id, 0); + b + } + + /// Record that the constraint currently being captured has no capture + /// implementation. Does not panic and does not emit a root for it — the + /// resulting program is marked incomplete (see + /// [`ConstraintProgram::complete`]) so callers know not to interpret it. + pub fn mark_unsupported(&mut self) { + self.complete = false; + } + + /// Append (or reuse) a node with the given op and result dimension. + fn push(&mut self, op: Op, dim: Dim) -> Expr { + if let Some(&id) = self.cse.get(&(op, dim)) { + return Expr { id, dim }; + } + let id = self.nodes.len() as u32; + self.nodes.push(op); + self.dims.push(dim); + self.cse.insert((op, dim), id); + Expr { id, dim } + } + + // --------------------------------------------------------------------- + // Leaves + // --------------------------------------------------------------------- + + /// A main-trace column read at the given frame `offset`, row 0. + pub fn main(&mut self, offset: u8, col: usize) -> Expr { + self.push( + Op::Var { + main: true, + offset, + row: 0, + col: col as u16, + }, + Dim::Base, + ) + } + + /// An aux-trace column read at the given frame `offset`, row 0 + /// ([`Dim::Ext`]). + pub fn aux(&mut self, offset: u8, col: usize) -> Expr { + self.push( + Op::Var { + main: false, + offset, + row: 0, + col: col as u16, + }, + Dim::Ext, + ) + } + + /// A periodic column read at the current row ([`Dim::Base`]). + pub fn periodic(&mut self, idx: usize) -> Expr { + self.push(Op::Periodic { idx: idx as u16 }, Dim::Base) + } + + /// A LogUp RAP challenge, uniform per proof ([`Dim::Ext`]). + pub fn challenge(&mut self, idx: usize) -> Expr { + self.push(Op::RapChallenge { idx: idx as u16 }, Dim::Ext) + } + + /// A precomputed LogUp alpha power, uniform per proof ([`Dim::Ext`]). + pub fn alpha_power(&mut self, idx: usize) -> Expr { + self.push(Op::AlphaPow { idx: idx as u16 }, Dim::Ext) + } + + /// The LogUp table offset `L/N`, uniform per proof ([`Dim::Ext`]). + pub fn table_offset(&mut self) -> Expr { + self.push(Op::TableOffset, Dim::Ext) + } + + // --------------------------------------------------------------------- + // Constants + // --------------------------------------------------------------------- + + /// Intern a base-field constant into `base_consts`, deduplicating by value. + fn intern_base(&mut self, fe: FieldElement) -> Expr { + let idx = match self.base_consts.iter().position(|c| c == &fe) { + Some(idx) => idx, + None => { + let idx = self.base_consts.len(); + self.base_consts.push(fe); + idx + } + }; + self.push(Op::ConstBase(idx as u32), Dim::Base) + } + + /// Intern an extension-field constant into `ext_consts`, deduplicating by + /// value. + fn intern_ext(&mut self, fe: FieldElement) -> Expr { + let idx = match self.ext_consts.iter().position(|c| c == &fe) { + Some(idx) => idx, + None => { + let idx = self.ext_consts.len(); + self.ext_consts.push(fe); + idx + } + }; + self.push(Op::ConstExt(idx as u32), Dim::Ext) + } + + /// A base-field constant from a `u64`, reduced and deduplicated by value. + pub fn const_base(&mut self, v: u64) -> Expr { + self.intern_base(FieldElement::::from(v)) + } + + /// A base-field constant from an `i64`; negatives map to `p - |v|`. + pub fn const_signed(&mut self, v: i64) -> Expr { + self.intern_base(FieldElement::::from(v)) + } + + /// An extension-field constant, deduplicated by value. + pub fn const_ext(&mut self, v: FieldElement) -> Expr { + self.intern_ext(v) + } + + /// The base-field constant `1`. + pub fn one(&mut self) -> Expr { + self.const_base(1) + } + + // --------------------------------------------------------------------- + // Arithmetic + // --------------------------------------------------------------------- + + /// `a + b`. Result is [`Dim::Base`] only if both operands are base. + pub fn add(&mut self, a: Expr, b: Expr) -> Expr { + let dim = Self::join(a.dim, b.dim); + self.push(Op::Add(a.id, b.id), dim) + } + + /// `a - b`. Result is [`Dim::Base`] only if both operands are base. + pub fn sub(&mut self, a: Expr, b: Expr) -> Expr { + let dim = Self::join(a.dim, b.dim); + self.push(Op::Sub(a.id, b.id), dim) + } + + /// `a * b`. Result is [`Dim::Base`] only if both operands are base. + pub fn mul(&mut self, a: Expr, b: Expr) -> Expr { + let dim = Self::join(a.dim, b.dim); + self.push(Op::Mul(a.id, b.id), dim) + } + + /// `-a`. Preserves the operand's dimension. + pub fn neg(&mut self, a: Expr) -> Expr { + self.push(Op::Neg(a.id), a.dim) + } + + /// Explicitly embed a base value into the extension ([`Dim::Ext`]). + pub fn embed(&mut self, a: Expr) -> Expr { + self.push(Op::Embed(a.id), Dim::Ext) + } + + /// Typing join: `(Base, Base) -> Base`; any `Ext` operand -> `Ext`. + fn join(a: Dim, b: Dim) -> Dim { + match (a, b) { + (Dim::Base, Dim::Base) => Dim::Base, + _ => Dim::Ext, + } + } + + // --------------------------------------------------------------------- + // Emit / finish + // --------------------------------------------------------------------- + + /// Record `e` as the root for constraint `constraint_idx`. + /// + /// `roots` is indexed by `constraint_idx` (grown/filled with sentinel `0` + /// as needed), so constraints can be captured in any order and a full + /// per-table program ends up with `roots[c]` = constraint `c`'s value. + pub fn emit(&mut self, constraint_idx: usize, e: Expr) { + if self.roots.len() <= constraint_idx { + self.roots.resize(constraint_idx + 1, 0); + } + self.roots[constraint_idx] = e.id; + } + + /// Consume the builder and produce the captured program. + /// + /// `num_base` is the number of leading (by `constraint_idx`) constraints + /// that are base-field ([`Dim::Base`]) rooted, matching + /// `AIR::num_base_transition_constraints()`. + pub fn finish(self, num_base: usize) -> ConstraintProgram { + ConstraintProgram { + nodes: self.nodes, + dims: self.dims, + base_consts: self.base_consts, + ext_consts: self.ext_consts, + roots: self.roots, + num_base, + complete: self.complete, + } + } +} diff --git a/crypto/stark/src/constraint_ir/interp.rs b/crypto/stark/src/constraint_ir/interp.rs new file mode 100644 index 000000000..ee8cb3ba5 --- /dev/null +++ b/crypto/stark/src/constraint_ir/interp.rs @@ -0,0 +1,267 @@ +//! CPU interpreter for a captured [`ConstraintProgram`]. +//! +//! A single forward pass over the topologically ordered nodes evaluates each +//! node into a [`Value`] (base [`Dim::Base`] or extension [`Dim::Ext`]), reusing +//! the real `FieldElement` arithmetic so per-op results are bit-identical to the +//! compiled constraint path. Mixed-dimension ops auto-embed the base operand +//! into the extension, mirroring the field tower's `F: IsSubFieldOf` +//! arithmetic. +//! +//! [`eval_program`] / [`eval_program_verifier`] are the full entry points, +//! matching `AIR::compute_transition_prover` / `AIR::compute_transition` +//! respectively. [`eval_program_base`] is the minimal entry point (single root, +//! main-only, base-field result) kept for the per-constraint diff test. +//! +//! Every entry point is generic over the field tower `, E>`; +//! for the Goldilocks tower these monomorphize to the same arithmetic the +//! compiled folder emits. + +use math::field::element::FieldElement; +use math::field::traits::{IsField, IsSubFieldOf}; + +use super::ir::{ConstraintProgram, Dim, Op}; +use crate::table::TableView; +use crate::traits::TransitionEvaluationContext; + +/// A node's computed value: base field ([`Dim::Base`]) or extension +/// ([`Dim::Ext`]). +/// +/// `Clone`, not `Copy` — `Copy` is not provable for a generic `FieldElement`. +/// For the Goldilocks tower these clones compile to register copies. +#[derive(Clone, Debug)] +enum Value { + Base(FieldElement), + Ext(FieldElement), +} + +impl, E: IsField> Value { + /// Promote to the extension field, embedding a base value if needed. + fn to_ext(&self) -> FieldElement { + match self { + Value::Base(x) => x.clone().to_extension::(), + Value::Ext(x) => x.clone(), + } + } + + fn as_base(&self) -> FieldElement { + match self { + Value::Base(x) => x.clone(), + Value::Ext(_) => { + panic!("expected a base value but found an extension value") + } + } + } +} + +/// Shared forward pass: evaluate every node, then return the value array. +/// `resolve_var` resolves `Op::Var` leaves; `resolve_periodic` resolves +/// `Op::Periodic`; the remaining uniforms are read from field-agnostic closures +/// so prover/verifier share this one walk. +#[allow(clippy::too_many_arguments)] +fn run( + prog: &ConstraintProgram, + resolve_var: FVar, + resolve_periodic: FPeriodic, + resolve_challenge: FChallenge, + resolve_alpha: FAlpha, + resolve_offset: FOffset, +) -> Vec> +where + F: IsSubFieldOf, + E: IsField, + FVar: Fn(bool, u8, u8, u16) -> Value, + FPeriodic: Fn(u16) -> Value, + FChallenge: Fn(u16) -> FieldElement, + FAlpha: Fn(u16) -> FieldElement, + FOffset: Fn() -> FieldElement, +{ + let mut values: Vec> = Vec::with_capacity(prog.nodes.len()); + + for (i, op) in prog.nodes.iter().enumerate() { + let v = match *op { + Op::ConstBase(idx) => Value::Base(prog.base_consts[idx as usize].clone()), + Op::ConstExt(idx) => Value::Ext(prog.ext_consts[idx as usize].clone()), + Op::Var { + main, + offset, + row, + col, + } => resolve_var(main, offset, row, col), + Op::Periodic { idx } => resolve_periodic(idx), + Op::RapChallenge { idx } => Value::Ext(resolve_challenge(idx)), + Op::AlphaPow { idx } => Value::Ext(resolve_alpha(idx)), + Op::TableOffset => Value::Ext(resolve_offset()), + Op::Add(a, b) => binop(&values, a, b, prog.dims[i], |x, y| x + y, |x, y| x + y), + Op::Sub(a, b) => binop(&values, a, b, prog.dims[i], |x, y| x - y, |x, y| x - y), + Op::Mul(a, b) => binop(&values, a, b, prog.dims[i], |x, y| x * y, |x, y| x * y), + Op::Neg(a) => match (&values[a as usize], prog.dims[i]) { + (Value::Base(x), Dim::Base) => Value::Base(-x), + (val, Dim::Ext) => Value::Ext(-val.to_ext()), + // A base value tagged extension (or vice versa) is a dim + // mismatch; keep it in the extension to stay well-typed. + (Value::Ext(x), Dim::Base) => Value::Ext(-x.clone()), + }, + Op::Embed(a) => Value::Ext(values[a as usize].to_ext()), + }; + values.push(v); + } + + values +} + +/// Apply a binary op, auto-embedding to the extension field when the result +/// dimension is [`Dim::Ext`] (or either operand is already extension). +#[inline] +fn binop( + values: &[Value], + a: u32, + b: u32, + result_dim: Dim, + base_op: impl Fn(FieldElement, FieldElement) -> FieldElement, + ext_op: impl Fn(FieldElement, FieldElement) -> FieldElement, +) -> Value +where + F: IsSubFieldOf, + E: IsField, +{ + let va = &values[a as usize]; + let vb = &values[b as usize]; + match (va, vb, result_dim) { + (Value::Base(x), Value::Base(y), Dim::Base) => Value::Base(base_op(x.clone(), y.clone())), + _ => Value::Ext(ext_op(va.to_ext(), vb.to_ext())), + } +} + +/// Evaluate one constraint's root over a base-field main row. +/// +/// `main_row[col]` resolves `Var { main: true, col, .. }` leaves. The minimal +/// algebraic constraint set only reads main columns at offset 0, row 0 and +/// returns a base-field value. `constraint_idx` selects which root to read. +/// +/// Kept for the per-constraint diff test; [`eval_program`] is the full prover +/// entry point. +pub fn eval_program_base( + prog: &ConstraintProgram, + constraint_idx: usize, + main_row: &[FieldElement], +) -> FieldElement +where + F: IsSubFieldOf, + E: IsField, +{ + let values = run( + prog, + |main, _offset, row, col| { + assert!(main, "aux leaves are not part of the minimal algebraic set"); + assert_eq!(row, 0, "minimal set reads row 0 only"); + Value::Base(main_row[col as usize].clone()) + }, + |_idx| panic!("periodic leaves are not part of the minimal algebraic set"), + |_idx| panic!("challenge leaves are not part of the minimal algebraic set"), + |_idx| panic!("alpha_power leaves are not part of the minimal algebraic set"), + || panic!("table_offset leaves are not part of the minimal algebraic set"), + ); + let root = prog.roots[constraint_idx]; + values[root as usize].as_base() +} + +/// Full prover entry point: evaluate every constraint in `prog` against `ctx` +/// (must be [`TransitionEvaluationContext::Prover`]), writing base-field +/// ([`Dim::Base`]-rooted) constraints into `base_evals` and extension-field +/// ([`Dim::Ext`]-rooted) constraints into `ext_evals[prog.num_base..]` — the +/// same contract as `AIR::compute_transition_prover`. +pub fn eval_program( + prog: &ConstraintProgram, + ctx: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], +) where + F: IsSubFieldOf, + E: IsField, +{ + let TransitionEvaluationContext::Prover { + frame, + periodic_values, + rap_challenges, + logup_alpha_powers, + logup_table_offset, + .. + } = ctx + else { + unreachable!("eval_program called with a Verifier context"); + }; + + let values = run( + prog, + |main, offset, row, col| { + let step: &TableView = frame.get_evaluation_step(offset as usize); + debug_assert_eq!(row, 0, "tables read row 0 of each frame step"); + if main { + Value::Base(step.get_main_evaluation_element(0, col as usize).clone()) + } else { + Value::Ext(step.get_aux_evaluation_element(0, col as usize).clone()) + } + }, + |idx| Value::Base(periodic_values[idx as usize].clone()), + |idx| rap_challenges[idx as usize].clone(), + |idx| logup_alpha_powers[idx as usize].clone(), + || (*logup_table_offset).clone(), + ); + + for (c, &root) in prog.roots.iter().enumerate() { + let v = &values[root as usize]; + if c < prog.num_base { + base_evals[c] = v.as_base(); + } else { + ext_evals[c] = v.to_ext(); + } + } +} + +/// Full verifier entry point: evaluate every constraint in `prog` against `ctx` +/// (must be [`TransitionEvaluationContext::Verifier`]) at the out-of-domain +/// point, writing every constraint (base or LogUp) into `ext_evals` — the same +/// contract as `AIR::compute_transition`. The verifier frame holds only +/// extension-field elements, so base-rooted constraints are embedded into the +/// extension on write. +pub fn eval_program_verifier( + prog: &ConstraintProgram, + ctx: &TransitionEvaluationContext, + ext_evals: &mut [FieldElement], +) where + F: IsSubFieldOf, + E: IsField, +{ + let TransitionEvaluationContext::Verifier { + frame, + periodic_values, + rap_challenges, + logup_alpha_powers, + logup_table_offset, + .. + } = ctx + else { + unreachable!("eval_program_verifier called with a Prover context"); + }; + + let values = run( + prog, + |main, offset, row, col| { + let step: &TableView = frame.get_evaluation_step(offset as usize); + debug_assert_eq!(row, 0, "tables read row 0 of each frame step"); + if main { + Value::Ext(step.get_main_evaluation_element(0, col as usize).clone()) + } else { + Value::Ext(step.get_aux_evaluation_element(0, col as usize).clone()) + } + }, + |idx| Value::Ext(periodic_values[idx as usize].clone()), + |idx| rap_challenges[idx as usize].clone(), + |idx| logup_alpha_powers[idx as usize].clone(), + || (*logup_table_offset).clone(), + ); + + for (c, &root) in prog.roots.iter().enumerate() { + ext_evals[c] = values[root as usize].to_ext(); + } +} diff --git a/crypto/stark/src/constraint_ir/ir.rs b/crypto/stark/src/constraint_ir/ir.rs new file mode 100644 index 000000000..c4159589a --- /dev/null +++ b/crypto/stark/src/constraint_ir/ir.rs @@ -0,0 +1,122 @@ +//! Flat intermediate representation (IR) for captured transition constraints. +//! +//! A [`ConstraintProgram`] is a topologically ordered list of [`Op`] nodes plus +//! a per-constraint root id. It is produced by the builder capture front-end +//! (see [`crate::constraint_ir::builder`]) and consumed by the CPU interpreter +//! (see [`crate::constraint_ir::interp`]). +//! +//! The IR is generic over a field tower `` (default: the Goldilocks base +//! field and its degree-3 extension). Each node carries a [`Dim`] tag +//! distinguishing base-field values ([`Dim::Base`]) from extension-field values +//! ([`Dim::Ext`]). Field constants live in side tables (`base_consts` / +//! `ext_consts`) referenced by index, so [`Op`] stays a plain `Copy + Eq + Hash` +//! payload of `u32`s with no bounds on `F`/`E` — this keeps the builder's +//! `(Op, Dim)` common-subexpression map cheap and correct regardless of the +//! field (`FieldElement` values would otherwise poison that key type, since +//! non-canonical representations compare equal under `PartialEq` but hash +//! differently). + +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as GoldilocksExtension; +use math::field::goldilocks::GoldilocksField; +use math::field::traits::IsField; + +/// Field-arithmetic dimension of a node's value: base field ([`Dim::Base`]) or +/// its extension ([`Dim::Ext`]). +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] +pub enum Dim { + /// Base field. + #[default] + Base, + /// Extension field. + Ext, +} + +/// One IR instruction. Operand fields are `u32` ids into the program's `nodes` +/// arena; a node with id `i` only references nodes with id `< i`. Constant ops +/// carry a `u32` index into the program's `base_consts` / `ext_consts` tables +/// rather than the field value itself, so `Op` is `Copy + Eq + Hash` for any +/// field tower. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum Op { + /// A base-field literal: `base_consts[idx]`. + ConstBase(u32), + /// An extension-field literal: `ext_consts[idx]`. + ConstExt(u32), + /// A leaf read of a trace cell. `main` selects the main trace (base field) + /// vs the aux trace (extension field); `offset`/`row` select the frame + /// step/row, `col` the column. + Var { + /// `true` for a main-trace column read, `false` for an aux read. + main: bool, + /// Frame step index (0-based). + offset: u8, + /// Row within the step. + row: u8, + /// Column index. + col: u16, + }, + /// A periodic column read: `periodic_values[idx]` at the current row + /// ([`Dim::Base`]). + Periodic { idx: u16 }, + /// A LogUp RAP challenge: `rap_challenges[idx]` ([`Dim::Ext`], uniform per + /// proof). + RapChallenge { idx: u16 }, + /// A precomputed LogUp alpha power: `logup_alpha_powers[idx]` ([`Dim::Ext`], + /// uniform per proof). + AlphaPow { idx: u16 }, + /// The LogUp table offset `L/N` ([`Dim::Ext`], uniform per proof). + TableOffset, + /// `nodes[a] + nodes[b]`. + Add(u32, u32), + /// `nodes[a] - nodes[b]`. + Sub(u32, u32), + /// `nodes[a] * nodes[b]`. + Mul(u32, u32), + /// `-nodes[a]`. + Neg(u32), + /// Embed a base value into the extension (`>::embed`). + Embed(u32), +} + +/// A captured program for one transition constraint (or a set of them). +/// +/// `nodes` is topologically ordered (id `i` references only `< i`). `dims[i]` +/// is the result dimension of `nodes[i]`. `roots[c]` is the node id of +/// constraint `c`'s value. `base_consts` / `ext_consts` hold the field literals +/// referenced by `Op::ConstBase` / `Op::ConstExt`. +#[derive(Clone, Debug)] +pub struct ConstraintProgram { + /// Topologically ordered instruction list. + pub nodes: Vec, + /// Per-node result dimension, parallel to `nodes`. + pub dims: Vec, + /// Base-field constant table, indexed by `Op::ConstBase`. + pub base_consts: Vec>, + /// Extension-field constant table, indexed by `Op::ConstExt`. + pub ext_consts: Vec>, + /// Per-constraint root node ids, indexed by `constraint_idx`. + pub roots: Vec, + /// Number of constraints (a prefix of `roots`) that are base-field + /// ([`Dim::Base`]) rooted, matching `AIR::num_base_transition_constraints()`. + /// The prover interpreter writes these into `base_evals`; the rest (LogUp, + /// always [`Dim::Ext`]) go into `ext_evals[num_base..]`. + pub num_base: usize, + /// `false` if any constraint in this program was captured via the + /// default capture body (i.e. it has no real capture impl — see + /// [`crate::constraint_ir::builder::IrBuilder::mark_unsupported`]). + /// Callers must not interpret an incomplete program. + pub complete: bool, +} + +impl ConstraintProgram { + /// Number of nodes in the program (an effectiveness measure for hash-consing). + pub fn len(&self) -> usize { + self.nodes.len() + } + + /// Whether the program has no nodes. + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } +} diff --git a/crypto/stark/src/constraint_ir/mod.rs b/crypto/stark/src/constraint_ir/mod.rs new file mode 100644 index 000000000..361dc303b --- /dev/null +++ b/crypto/stark/src/constraint_ir/mod.rs @@ -0,0 +1,30 @@ +//! Field-generic flat IR for transition constraints. +//! +//! A transition constraint's algebra is captured, at AIR-construction time, +//! into a flat intermediate representation ([`ConstraintProgram`]) via an +//! explicit [`IrBuilder`]. Interpreting that IR on the CPU +//! ([`eval_program`] / [`eval_program_verifier`]) reproduces the constraint's +//! real evaluation bit-for-bit, and the same IR is the input to the future GPU +//! constraint-evaluation kernel. +//! +//! The whole module is generic over a field tower `, E>` +//! (defaulting to the Goldilocks base field and its degree-3 extension), so a +//! capture front-end can target it for any field. Constants live in side tables +//! keyed by index, which keeps [`Op`] a plain `Copy + Eq + Hash` payload and the +//! builder's common-subexpression cache sound for every field. +//! +//! - [`ir`]: the IR data structures ([`ConstraintProgram`], [`Op`], [`Dim`]). +//! - [`builder`]: the [`IrBuilder`] and [`Expr`] capture API. +//! - [`interp`]: a CPU forward-pass interpreter over the IR. +//! +//! [`ConstraintProgram`]: ir::ConstraintProgram +//! [`Op`]: ir::Op +//! [`Dim`]: ir::Dim + +pub mod builder; +pub mod interp; +pub mod ir; + +pub use builder::{Expr, IrBuilder}; +pub use interp::{eval_program, eval_program_base, eval_program_verifier}; +pub use ir::{ConstraintProgram, Dim, Op}; diff --git a/crypto/stark/src/lib.rs b/crypto/stark/src/lib.rs index 87236c5f9..e2280f65b 100644 --- a/crypto/stark/src/lib.rs +++ b/crypto/stark/src/lib.rs @@ -6,6 +6,7 @@ compile_error!("the `disk-spill` feature requires memmap2, which does not compil #[cfg(feature = "debug-checks")] pub mod bus_debug; pub mod commitment; +pub mod constraint_ir; pub mod constraints; pub mod context; pub mod debug; From ad15768335d032e160c61693eb9c0b258c3f5d57 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 1 Jul 2026 21:56:32 -0300 Subject: [PATCH 02/52] stark: unit tests for the constraint IR, incl. a non-Goldilocks tower Hand-built IrBuilder programs checked against direct FieldElement arithmetic: every Op variant and leaf kind, mixed base/ext arithmetic with auto-embed, constant dedup (base, signed, ext), the id-0 zero convention, CSE sharing, out-of-order emit() root indexing, the complete-flag plumbing, and 1000-row randomized differential checks. The prover and verifier entry points are exercised against hand-constructed TransitionEvaluationContext values (both variants), including the base-root promotion on the verifier side and next-row (offset 1) frame reads. A reflexive-tower test (E = F over the u32 test field, which has a different modulus and BaseType than Goldilocks) proves the module is genuinely field-generic; math's test-utils feature is enabled as a dev-dependency for it. --- crypto/stark/Cargo.toml | 1 + crypto/stark/src/constraint_ir/mod.rs | 3 + crypto/stark/src/constraint_ir/tests.rs | 560 ++++++++++++++++++++++++ 3 files changed, 564 insertions(+) create mode 100644 crypto/stark/src/constraint_ir/tests.rs diff --git a/crypto/stark/Cargo.toml b/crypto/stark/Cargo.toml index d0f6a51ef..8060b80f3 100644 --- a/crypto/stark/Cargo.toml +++ b/crypto/stark/Cargo.toml @@ -37,6 +37,7 @@ web-sys = { version = "0.3.64", features = ['console'], optional = true } serde_cbor = { version = "0.11.1" } [dev-dependencies] +math = { path = "../math", features = ["test-utils"] } criterion = { version = "0.4", default-features = false } env_logger = "*" test-log = { version = "0.2.11", features = ["log"] } diff --git a/crypto/stark/src/constraint_ir/mod.rs b/crypto/stark/src/constraint_ir/mod.rs index 361dc303b..258aa23b7 100644 --- a/crypto/stark/src/constraint_ir/mod.rs +++ b/crypto/stark/src/constraint_ir/mod.rs @@ -25,6 +25,9 @@ pub mod builder; pub mod interp; pub mod ir; +#[cfg(test)] +mod tests; + pub use builder::{Expr, IrBuilder}; pub use interp::{eval_program, eval_program_base, eval_program_verifier}; pub use ir::{ConstraintProgram, Dim, Op}; diff --git a/crypto/stark/src/constraint_ir/tests.rs b/crypto/stark/src/constraint_ir/tests.rs new file mode 100644 index 000000000..61e3d8dbb --- /dev/null +++ b/crypto/stark/src/constraint_ir/tests.rs @@ -0,0 +1,560 @@ +//! Unit tests for the field-generic constraint IR: hand-built programs checked +//! against direct `FieldElement` arithmetic, the prover/verifier entry points +//! against hand-constructed contexts, and a non-Goldilocks tower (`E = F`) that +//! exercises the reflexive `IsSubFieldOf` path — the point of the genericity. + +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext; +use math::field::goldilocks::GoldilocksField as Fp; +use math::field::test_fields::u32_test_field::U32TestField; + +use super::builder::IrBuilder; +use super::interp::{eval_program, eval_program_base, eval_program_verifier}; +use super::ir::{ConstraintProgram, Dim, Op}; +use crate::frame::Frame; +use crate::lookup::PackingShifts; +use crate::table::TableView; +use crate::traits::TransitionEvaluationContext; + +type FpE = FieldElement; +type ExtE = FieldElement; + +fn fp(v: u64) -> FpE { + FpE::from(v) +} + +/// Build a degree-3 Goldilocks extension element from three `u64` components. +fn ext3(a: u64, b: u64, c: u64) -> ExtE { + ExtE::from_raw([fp(a), fp(b), fp(c)]) +} + +// ------------------------------------------------------------------------ +// id-0 convention + const dedup +// ------------------------------------------------------------------------ + +#[test] +fn id_zero_is_base_const_zero() { + let b = IrBuilder::::new(); + let prog = b.finish(0); + // Node 0 is ConstBase(0); base_consts[0] is the base-field zero. + assert_eq!(prog.nodes[0], Op::ConstBase(0)); + assert_eq!(prog.dims[0], Dim::Base); + assert_eq!(prog.base_consts[0], FpE::zero()); + assert_eq!(prog.len(), 1); + assert!(!prog.is_empty()); +} + +#[test] +fn const_base_zero_dedups_to_id_zero() { + let mut b = IrBuilder::::new(); + let z = b.const_base(0); + assert_eq!(z.dim(), Dim::Base); + let prog = b.finish(0); + // No new node or const slot: reuses the reserved id-0 zero. + assert_eq!(prog.nodes.len(), 1); + assert_eq!(prog.base_consts.len(), 1); +} + +#[test] +fn const_dedup_same_value_interned_once() { + let mut b = IrBuilder::::new(); + b.const_base(7); + b.const_base(7); + let prog = b.finish(0); + // base_consts: [0, 7] only; nodes: ConstBase(0), ConstBase(1) only. + assert_eq!(prog.base_consts, vec![fp(0), fp(7)]); + assert_eq!(prog.nodes.len(), 2); +} + +#[test] +fn const_signed_negative_reduces_and_dedups() { + let mut b = IrBuilder::::new(); + let neg = b.const_signed(-1); + assert_eq!(neg.dim(), Dim::Base); + let prog = b.finish(0); + // -1 in the field is p - 1; matches FieldElement::from(-1i64). + assert_eq!(prog.base_consts[1], FpE::from(-1i64)); + + // Interning the same negative twice uses one slot and one node. + let mut b2 = IrBuilder::::new(); + b2.const_signed(-5); + b2.const_signed(-5); + let prog2 = b2.finish(0); + assert_eq!(prog2.base_consts, vec![fp(0), FpE::from(-5i64)]); + assert_eq!(prog2.nodes.len(), 2); + + // A positive i64 dedups against the same value interned via const_base. + let mut b3 = IrBuilder::::new(); + b3.const_base(9); + b3.const_signed(9); + let prog3 = b3.finish(0); + assert_eq!(prog3.base_consts, vec![fp(0), fp(9)]); + assert_eq!(prog3.nodes.len(), 2); +} + +#[test] +fn const_ext_dedups_by_value() { + let mut b = IrBuilder::::new(); + let e1 = b.const_ext(ext3(1, 2, 3)); + b.const_ext(ext3(1, 2, 3)); + b.const_ext(ext3(4, 5, 6)); + assert_eq!(e1.dim(), Dim::Ext); + let prog = b.finish(0); + // ext_consts: two distinct values. + assert_eq!(prog.ext_consts, vec![ext3(1, 2, 3), ext3(4, 5, 6)]); + // nodes: ConstBase(0) [id-0] + ConstExt(0) + ConstExt(1). + assert_eq!(prog.nodes.len(), 3); + assert_eq!(prog.nodes[1], Op::ConstExt(0)); + assert_eq!(prog.nodes[2], Op::ConstExt(1)); +} + +// ------------------------------------------------------------------------ +// CSE on (Op, Dim) still works with side-table constants. +// ------------------------------------------------------------------------ + +#[test] +fn cse_shares_structurally_identical_subexpressions() { + let mut b = IrBuilder::::new(); + let x = b.main(0, 0); + let y = b.main(0, 1); + let s1 = b.add(x, y); + let s2 = b.add(x, y); // structurally identical: no new node + let nodes_so_far = 4; // zero, x, y, add + let m = b.mul(s1, s2); // Mul(add, add): one new node + b.emit(0, m); + let prog = b.finish(1); + assert_eq!(prog.nodes.len(), nodes_so_far + 1); + + let row = vec![fp(3), fp(4)]; + let got = eval_program_base(&prog, 0, &row); + let s = fp(3) + fp(4); + assert_eq!(got, s * s); +} + +// ------------------------------------------------------------------------ +// Every arithmetic Op over base-field leaves, checked against direct math. +// ------------------------------------------------------------------------ + +#[test] +fn base_arithmetic_add_sub_mul_neg() { + // Roots: idx 0 = (x + y) - (x * y); idx 1 = its negation. + let mut b = IrBuilder::::new(); + let x = b.main(0, 0); + let y = b.main(0, 1); + let sum = b.add(x, y); + let prod = b.mul(x, y); + let diff = b.sub(sum, prod); + let negd = b.neg(diff); + assert_eq!(sum.dim(), Dim::Base); + assert_eq!(prod.dim(), Dim::Base); + assert_eq!(diff.dim(), Dim::Base); + assert_eq!(negd.dim(), Dim::Base); + b.emit(0, diff); + b.emit(1, negd); + let prog = b.finish(2); + + for (px, py) in [(3u64, 5u64), (0, 9), (100, 7), (1, 1)] { + let row = vec![fp(px), fp(py)]; + let expected = (fp(px) + fp(py)) - (fp(px) * fp(py)); + assert_eq!(eval_program_base(&prog, 0, &row), expected); + assert_eq!(eval_program_base(&prog, 1, &row), -expected); + } +} + +#[test] +fn base_const_arithmetic() { + // 2 * x - 1 + let mut b = IrBuilder::::new(); + let x = b.main(0, 0); + let two = b.const_base(2); + let one = b.one(); + let twox = b.mul(two, x); + let res = b.sub(twox, one); + b.emit(0, res); + let prog = b.finish(1); + + for xv in [0u64, 1, 2, 42, 1_000_000] { + let got = eval_program_base(&prog, 0, &[fp(xv)]); + assert_eq!(got, fp(2) * fp(xv) - fp(1)); + } +} + +// ------------------------------------------------------------------------ +// Frame offsets: reading the next row (offset 1). +// ------------------------------------------------------------------------ + +#[test] +fn frame_offset_reads_next_step() { + // next - cur over main column 0. + let mut b = IrBuilder::::new(); + let cur = b.main(0, 0); + let next = b.main(1, 0); + let res = b.sub(next, cur); + b.emit(0, res); + let prog = b.finish(1); + + let step0 = TableView::::new(vec![vec![fp(10)]], vec![vec![]]); + let step1 = TableView::::new(vec![vec![fp(17)]], vec![vec![]]); + let frame = Frame::::new(vec![step0, step1]); + let periodic: Vec = vec![]; + let rap: Vec = vec![]; + let alpha: Vec = vec![]; + let offset = ExtE::zero(); + let shifts = PackingShifts::::new(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + + let mut base_evals = vec![FpE::zero()]; + let mut ext_evals: Vec = vec![]; + eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); + assert_eq!(base_evals[0], fp(17) - fp(10)); +} + +// ------------------------------------------------------------------------ +// Mixed Base×Ext arithmetic with auto-embed, and the explicit Embed op. +// ------------------------------------------------------------------------ + +#[test] +fn mixed_base_ext_auto_embeds() { + // aux (Ext) + main (Base) and main * aux: result Ext, base auto-embedded. + let mut b = IrBuilder::::new(); + let m = b.main(0, 0); // Base + let a = b.aux(0, 0); // Ext + let sum = b.add(a, m); + let prod = b.mul(m, a); + assert_eq!(sum.dim(), Dim::Ext); + assert_eq!(prod.dim(), Dim::Ext); + b.emit(0, sum); + b.emit(1, prod); + let prog = b.finish(0); // both roots are Ext + + let main_val = fp(5); + let aux_val = ext3(2, 3, 4); + let step = TableView::::new(vec![vec![main_val]], vec![vec![aux_val]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let rap: Vec = vec![]; + let alpha: Vec = vec![]; + let offset = ExtE::zero(); + let shifts = PackingShifts::::new(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + + let mut base_evals: Vec = vec![]; + let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; + eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); + // Mixed operators put the subfield on the left: F op E -> E. + assert_eq!(ext_evals[0], main_val + aux_val); + assert_eq!(ext_evals[1], main_val * aux_val); +} + +#[test] +fn explicit_embed_and_ext_neg() { + // Embed(main) and Neg over an Ext value: embed(m) + (-aux). + let mut b = IrBuilder::::new(); + let m = b.main(0, 0); + let e = b.embed(m); + assert_eq!(m.dim(), Dim::Base); + assert_eq!(e.dim(), Dim::Ext); + let a = b.aux(0, 0); + let na = b.neg(a); + assert_eq!(na.dim(), Dim::Ext); + let res = b.add(e, na); + b.emit(0, res); + let prog = b.finish(0); + assert!(prog.nodes.iter().any(|op| matches!(op, Op::Embed(_)))); + + let aux_val = ext3(1, 2, 3); + let step = TableView::::new(vec![vec![fp(9)]], vec![vec![aux_val]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let rap: Vec = vec![]; + let alpha: Vec = vec![]; + let offset = ExtE::zero(); + let shifts = PackingShifts::::new(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + + let mut base_evals: Vec = vec![]; + let mut ext_evals = vec![ExtE::zero()]; + eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); + assert_eq!(ext_evals[0], fp(9).to_extension::() - aux_val); +} + +// ------------------------------------------------------------------------ +// Every leaf kind: periodic, challenge, alpha_power, table_offset, aux. +// ------------------------------------------------------------------------ + +#[test] +fn all_leaf_kinds_logup_shaped() { + // A LogUp-shaped expression touching every leaf variety: + // periodic(0) * challenge(0) + alpha_pow(1) * aux(0,3) - table_offset() + let mut b = IrBuilder::::new(); + let p = b.periodic(0); // Base + let ch = b.challenge(0); // Ext + let ap = b.alpha_power(1); // Ext + let au = b.aux(0, 3); // Ext + let off = b.table_offset(); // Ext + assert_eq!(p.dim(), Dim::Base); + assert_eq!(ch.dim(), Dim::Ext); + assert_eq!(ap.dim(), Dim::Ext); + assert_eq!(au.dim(), Dim::Ext); + assert_eq!(off.dim(), Dim::Ext); + let t1 = b.mul(p, ch); // Base×Ext → Ext + let t2 = b.mul(ap, au); // Ext×Ext → Ext + let s = b.add(t1, t2); + let res = b.sub(s, off); + assert_eq!(res.dim(), Dim::Ext); + b.emit(0, res); + let prog = b.finish(0); + + let periodic = vec![fp(6)]; + let rap = vec![ext3(1, 0, 0), ext3(2, 2, 2)]; + let alpha = vec![ext3(9, 9, 9), ext3(3, 1, 4)]; + let offset = ext3(7, 7, 7); + let aux_row = vec![ext3(0, 0, 0), ext3(0, 0, 0), ext3(0, 0, 0), ext3(5, 5, 5)]; + + let expected = { + let t1 = periodic[0] * rap[0]; // periodic(0) * challenge(0) + let t2 = alpha[1] * aux_row[3]; + (t1 + t2) - offset + }; + + let step = TableView::::new(vec![vec![]], vec![aux_row]); + let frame = Frame::::new(vec![step]); + let shifts = PackingShifts::::new(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + + let mut base_evals: Vec = vec![]; + let mut ext_evals = vec![ExtE::zero()]; + eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); + assert_eq!(ext_evals[0], expected); +} + +// ------------------------------------------------------------------------ +// Prover & verifier full entry points on hand-built contexts (both variants). +// ------------------------------------------------------------------------ + +/// One base constraint (idx 0: `a - b`) and one ext constraint +/// (idx 1: `aux0 * alpha0`); `num_base = 1`. +fn two_constraint_program() -> ConstraintProgram { + let mut b = IrBuilder::::new(); + let a = b.main(0, 0); + let bb = b.main(0, 1); + let base_c = b.sub(a, bb); + b.emit(0, base_c); + let au = b.aux(0, 0); + let al = b.alpha_power(0); + let ext_c = b.mul(au, al); + b.emit(1, ext_c); + b.finish(1) +} + +#[test] +fn prover_entry_point_splits_base_and_ext() { + let prog = two_constraint_program(); + let aux_val = ext3(2, 0, 1); + let step = TableView::::new(vec![vec![fp(30), fp(12)]], vec![vec![aux_val]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let rap: Vec = vec![]; + let alpha = vec![ext3(3, 3, 3)]; + let offset = ExtE::zero(); + let shifts = PackingShifts::::new(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + + let mut base_evals = vec![FpE::zero()]; + let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; + eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); + + // Base root lands in base_evals[0]; ext root in ext_evals[1] (absolute idx). + assert_eq!(base_evals[0], fp(30) - fp(12)); + assert_eq!(ext_evals[1], aux_val * alpha[0]); +} + +#[test] +fn verifier_entry_point_promotes_base_roots() { + let prog = two_constraint_program(); + // Verifier frame holds extension elements only (Frame). + let aux_val = ext3(2, 0, 1); + let step = TableView::::new( + vec![vec![ext3(30, 0, 0), ext3(12, 0, 0)]], + vec![vec![aux_val]], + ); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let rap: Vec = vec![]; + let alpha = vec![ext3(3, 3, 3)]; + let offset = ExtE::zero(); + let shifts = PackingShifts::::new(); + let ctx = TransitionEvaluationContext::::new_verifier( + &frame, &periodic, &rap, &alpha, &offset, &shifts, + ); + + let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; + eval_program_verifier(&prog, &ctx, &mut ext_evals); + + // The base-rooted constraint is promoted into the extension on write. + assert_eq!(ext_evals[0], ext3(30, 0, 0) - ext3(12, 0, 0)); + assert_eq!(ext_evals[1], aux_val * alpha[0]); +} + +// ------------------------------------------------------------------------ +// roots indexed by emit(constraint_idx), in any emission order. +// ------------------------------------------------------------------------ + +#[test] +fn roots_indexed_by_constraint_idx_any_order() { + let mut b = IrBuilder::::new(); + let x = b.main(0, 0); + // Emit idx 2 before idx 0 — roots must still land in the right slots. + let x2 = b.mul(x, x); + b.emit(2, x2); + b.emit(0, x); + let one = b.one(); + let xp1 = b.add(x, one); + b.emit(1, xp1); + let prog = b.finish(3); + assert_eq!(prog.roots.len(), 3); + + let row = vec![fp(4)]; + assert_eq!(eval_program_base(&prog, 0, &row), fp(4)); + assert_eq!(eval_program_base(&prog, 1, &row), fp(4) + fp(1)); + assert_eq!(eval_program_base(&prog, 2, &row), fp(4) * fp(4)); +} + +// ------------------------------------------------------------------------ +// complete flag plumbing. +// ------------------------------------------------------------------------ + +#[test] +fn complete_flag_defaults_true_and_mark_unsupported_clears_it() { + let b = IrBuilder::::new(); + assert!(b.finish(0).complete); + + let mut b = IrBuilder::::new(); + b.mark_unsupported(); + assert!(!b.finish(0).complete); +} + +// ------------------------------------------------------------------------ +// Non-Goldilocks tower: E = F over the Baby-Bear-prime U32 test field. +// Exercises the reflexive IsSubFieldOf impl and proves the module is +// genuinely field-generic. (This trimmed math crate has no Stark252-style +// big field; U32TestField has a different modulus AND a different BaseType +// (u32), so it is a strict genericity check.) +// ------------------------------------------------------------------------ + +#[test] +fn non_goldilocks_reflexive_tower_builds_and_interprets() { + type G = U32TestField; + type GE = FieldElement; + fn g(v: u64) -> GE { + GE::from(v) + } + + // Base-only program for eval_program_base (which walks every node and + // accepts main leaves only): x * y + 3. + let mut b0 = IrBuilder::::new(); + let x = b0.main(0, 0); + let y = b0.main(0, 1); + let prod = b0.mul(x, y); + let three = b0.const_base(3); + let base_res = b0.add(prod, three); + b0.emit(0, base_res); + let base_prog = b0.finish(1); + let row = vec![g(6), g(7)]; + assert_eq!(eval_program_base(&base_prog, 0, &row), g(6) * g(7) + g(3)); + + // Program: idx 0 (base) = x * y + 3; idx 1 (ext = same field) = aux0 + 10. + let mut b = IrBuilder::::new(); + let x = b.main(0, 0); + let y = b.main(0, 1); + let prod = b.mul(x, y); + let three = b.const_base(3); + let base_res = b.add(prod, three); + b.emit(0, base_res); + + let au = b.aux(0, 0); // "Ext" (= G here) + let ec = b.const_ext(g(10)); + let ext_res = b.add(au, ec); + assert_eq!(ext_res.dim(), Dim::Ext); + b.emit(1, ext_res); + + let prog = b.finish(1); + // Const dedup with a non-u64 BaseType (u32) still works. + assert_eq!(prog.base_consts, vec![g(0), g(3)]); + assert_eq!(prog.ext_consts, vec![g(10)]); + + // Full prover entry point with F = E = G. + let step = TableView::::new(vec![vec![g(6), g(7)]], vec![vec![g(4)]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let rap: Vec = vec![]; + let alpha: Vec = vec![]; + let offset = g(0); + let shifts = PackingShifts::::new(); + let ctx = TransitionEvaluationContext::::new_prover( + &frame, &periodic, &rap, &alpha, &offset, &shifts, + ); + let mut base_evals = vec![GE::zero()]; + let mut ext_evals = vec![GE::zero(), GE::zero()]; + eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); + assert_eq!(base_evals[0], g(6) * g(7) + g(3)); + assert_eq!(ext_evals[1], g(4) + g(10)); + + // Verifier entry point too (the frame is Frame either way here). + let vctx = TransitionEvaluationContext::::new_verifier( + &frame, &periodic, &rap, &alpha, &offset, &shifts, + ); + let mut v_evals = vec![GE::zero(), GE::zero()]; + eval_program_verifier(&prog, &vctx, &mut v_evals); + assert_eq!(v_evals[0], g(6) * g(7) + g(3)); + assert_eq!(v_evals[1], g(4) + g(10)); +} + +// ------------------------------------------------------------------------ +// Random-row differential fuzz: a nontrivial base program vs direct math. +// ------------------------------------------------------------------------ + +struct SplitMix64(u64); +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } +} + +#[test] +fn random_rows_match_direct_arithmetic() { + // ((a + b) * c - a) * (b - c) + 5 + let mut bld = IrBuilder::::new(); + let a = bld.main(0, 0); + let b = bld.main(0, 1); + let c = bld.main(0, 2); + let ab = bld.add(a, b); + let abc = bld.mul(ab, c); + let abca = bld.sub(abc, a); + let bc = bld.sub(b, c); + let t = bld.mul(abca, bc); + let five = bld.const_base(5); + let res = bld.add(t, five); + bld.emit(0, res); + let prog = bld.finish(1); + + let mut rng = SplitMix64(0xDEAD_BEEF_CAFE_F00D); + for _ in 0..1000 { + let av = fp(rng.next_u64()); + let bv = fp(rng.next_u64()); + let cv = fp(rng.next_u64()); + let row = vec![av, bv, cv]; + let got = eval_program_base(&prog, 0, &row); + let expected = ((av + bv) * cv - av) * (bv - cv) + fp(5); + assert_eq!(got, expected); + } +} From b7da04c77629c065c38965afda62758345c70e55 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 1 Jul 2026 22:13:44 -0300 Subject: [PATCH 03/52] stark: add the ConstraintBuilder single-body constraint framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New module crypto/stark/src/constraints/builder.rs: one constraint body, written once against the ConstraintBuilder trait, is interpreted three ways depending on the implementation it runs over: - ProverEvalFolder (Expr = FieldElement): compiled per-row prover evaluation, constructed from the Prover TransitionEvaluationContext variant plus output slices; emit_ext writes ext_evals at the absolute constraint index. - VerifierEvalFolder (Expr = FieldElement): the same body at the OOD point over the all-extension frame; const_base embeds via FieldElement::::from(v).to_extension(). Monomorphized into the guest binary this is the recursion path — no capture, no hashing, no interpretation. - CaptureBuilder (Expr = owned Rc expression tree with eager dim + degree): one setup-time run that flattens into the constraint IR's IrBuilder (hash-consing there = structural CSE) and returns the program plus per-root tree-measured degrees. Also: ExprOps/ExtExprOps operator-bound aliases (mixed base/ext ops keep the base operand on the left, matching the field tower), ConstraintMeta + RootKind plain-data constraint metadata with the dense/idx-ordered/Base-prefix invariants (num_base_from_meta), the ConstraintSet per-table trait, and debug-build emit tracking asserting every constraint index is emitted exactly once. Tests: a sample ConstraintSet (EqXor-, IsBit- and Add-carry-pair-shaped bodies plus a LogUp-shaped extension constraint) checked on 1000 random rows three ways — prover folder vs direct arithmetic, prover folder vs interpreted capture, verifier folder vs interpreted capture — plus measured-vs-declared degrees, meta invariants, and the completeness asserts. Not wired into any production path; no behavior change. --- crypto/stark/src/constraints/builder.rs | 749 ++++++++++++++++++ crypto/stark/src/constraints/builder_tests.rs | 441 +++++++++++ crypto/stark/src/constraints/mod.rs | 3 + 3 files changed, 1193 insertions(+) create mode 100644 crypto/stark/src/constraints/builder.rs create mode 100644 crypto/stark/src/constraints/builder_tests.rs diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs new file mode 100644 index 000000000..305a1a3fc --- /dev/null +++ b/crypto/stark/src/constraints/builder.rs @@ -0,0 +1,749 @@ +//! The `ConstraintBuilder` single-body constraint front-end. +//! +//! One constraint body, written once against [`ConstraintBuilder`], is +//! interpreted three ways depending on the implementation it runs over: +//! - [`ProverEvalFolder`]: `Expr = FieldElement` — compiled per-row prover +//! evaluation (the CPU hot path). +//! - [`VerifierEvalFolder`]: `Expr = FieldElement` — the same body at the +//! OOD point (and, monomorphized into the guest binary, the recursion path; +//! no capture, no hashing, no interpretation in-circuit). +//! - [`CaptureBuilder`]: `Expr` = an owned expression tree — one setup-time run +//! that flattens into the flat [`ConstraintProgram`] IR for the CPU +//! interpreter and the GPU, measuring constraint degrees along the way. +//! +//! A table's constraints are packaged as a [`ConstraintSet`]: idx-ordered +//! [`ConstraintMeta`] (plain data: kind, declared degree, zerofier shape) plus +//! THE single `eval` body that emits every constraint. +//! +//! Fixed packing-shift constants (`2^8`/`2^16`/`2^24`) have no dedicated leaf: +//! bodies lower them through `const_base`, like any other structural constant. + +use std::marker::PhantomData; +use std::ops::{Add, Mul, Neg, Sub}; +use std::rc::Rc; + +use math::field::element::FieldElement; +use math::field::traits::{IsField, IsSubFieldOf}; + +use crate::constraint_ir::{ConstraintProgram, Dim, IrBuilder}; +use crate::frame::Frame; +use crate::traits::TransitionEvaluationContext; + +// ============================================================================= +// Operator-bound aliases +// ============================================================================= + +/// Base-field expression operations. `Ext` is the builder's extension +/// expression type; mixed ops keep the base operand on the LEFT (the field +/// tower only implements subfield ∘ superfield, not the reverse — see +/// `math::field::element` operator impls). +pub trait ExprOps: + Sized + + Clone + + Add + + Sub + + Mul + + Neg + + Add + + Sub + + Mul +{ +} +impl ExprOps for T where + T: Sized + + Clone + + Add + + Sub + + Mul + + Neg + + Add + + Sub + + Mul +{ +} + +/// Extension-field expression operations (self ops only; base×ext lives on +/// [`ExprOps`] so the base operand stays on the left). +pub trait ExtExprOps: + Sized + + Clone + + Add + + Sub + + Mul + + Neg +{ +} +impl ExtExprOps for T where + T: Sized + + Clone + + Add + + Sub + + Mul + + Neg +{ +} + +// ============================================================================= +// The trait +// ============================================================================= + +/// The single-body constraint front-end: leaves + emit sinks. Constraint +/// bodies are generic over an implementation of this trait; the associated +/// `Expr`/`ExprE` types decide what a run of the body *means*. +/// +/// `const_base`/`const_signed` are the ONLY constant path — there is no +/// `From>` on `Expr` (it would be wrong for +/// [`VerifierEvalFolder`], where `Expr = FieldElement`). +pub trait ConstraintBuilder { + /// Base-field expression. + type Expr: ExprOps; + /// Extension-field expression. + type ExprE: ExtExprOps; + + // ---- leaves --------------------------------------------------------- + fn main(&self, offset: usize, col: usize) -> Self::Expr; + fn aux(&self, offset: usize, col: usize) -> Self::ExprE; + fn periodic(&self, idx: usize) -> Self::Expr; + /// `rap_challenges[idx]`. + fn challenge(&self, idx: usize) -> Self::ExprE; + /// `logup_alpha_powers[idx]`. + fn alpha_pow(&self, idx: usize) -> Self::ExprE; + /// The LogUp table offset `L/N`. + fn table_offset(&self) -> Self::ExprE; + fn const_base(&self, v: u64) -> Self::Expr; + fn const_signed(&self, v: i64) -> Self::Expr; + fn one(&self) -> Self::Expr { + self.const_base(1) + } + fn zero(&self) -> Self::Expr { + self.const_base(0) + } + + // ---- sinks ---------------------------------------------------------- + /// Record base-field constraint `constraint_idx`'s value. + fn emit_base(&mut self, constraint_idx: usize, e: Self::Expr); + /// Record extension-field (LogUp) constraint `constraint_idx`'s value. + fn emit_ext(&mut self, constraint_idx: usize, e: Self::ExprE); +} + +// ============================================================================= +// Constraint metadata +// ============================================================================= + +/// Whether a constraint's root value lives in the base field or the extension. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum RootKind { + /// Base-field constraint (algebraic table constraints). + Base, + /// Extension-field constraint (LogUp). + Ext, +} + +/// Per-constraint metadata: plain data replacing the per-constraint trait +/// objects. `Base` entries MUST form a prefix of an idx-ordered, dense list — +/// see [`num_base_from_meta`]. +#[derive(Clone, Debug)] +pub struct ConstraintMeta { + pub constraint_idx: usize, + /// Base | Ext; Base entries MUST be a prefix. + pub kind: RootKind, + /// Declared degree; asserted == tree-measured degree (host-side test). + pub degree: usize, + /// Periodicity of application over the trace (default 1: every row). + pub period: usize, + /// Offset for periodic application (default 0). + pub offset: usize, + /// Periodicity of rows exempted within the applied rows (default None). + pub exemptions_period: Option, + /// Offset for the periodic exemptions (default None). + pub periodic_exemptions_offset: Option, + /// Number of exempted rows at the end of the trace (default 0). + pub end_exemptions: usize, +} + +impl ConstraintMeta { + /// A base-field constraint with default zerofier shape (every row, no + /// exemptions). + pub fn base(constraint_idx: usize, degree: usize) -> Self { + Self { + constraint_idx, + kind: RootKind::Base, + degree, + period: 1, + offset: 0, + exemptions_period: None, + periodic_exemptions_offset: None, + end_exemptions: 0, + } + } + + /// An extension-field (LogUp) constraint with default zerofier shape. + pub fn ext(constraint_idx: usize, degree: usize) -> Self { + Self { + kind: RootKind::Ext, + ..Self::base(constraint_idx, degree) + } + } + + pub fn with_period(mut self, period: usize) -> Self { + self.period = period; + self + } + + pub fn with_offset(mut self, offset: usize) -> Self { + self.offset = offset; + self + } + + pub fn with_exemptions(mut self, exemptions_period: usize, exemptions_offset: usize) -> Self { + self.exemptions_period = Some(exemptions_period); + self.periodic_exemptions_offset = Some(exemptions_offset); + self + } + + pub fn with_end_exemptions(mut self, end_exemptions: usize) -> Self { + self.end_exemptions = end_exemptions; + self + } +} + +/// Compute `num_base` from a table's metadata, debug-asserting the invariants: +/// the list is dense and idx-ordered (`meta[i].constraint_idx == i`) and +/// `RootKind::Base` entries form a prefix — the prefix length IS `num_base`, +/// matching the engine's existing base/ext split convention. +pub fn num_base_from_meta(meta: &[ConstraintMeta]) -> usize { + let num_base = meta.iter().take_while(|m| m.kind == RootKind::Base).count(); + #[cfg(debug_assertions)] + for (i, m) in meta.iter().enumerate() { + assert_eq!( + m.constraint_idx, i, + "constraint meta must be dense and idx-ordered: entry {i} has idx {}", + m.constraint_idx + ); + assert!( + (m.kind == RootKind::Base) == (i < num_base), + "RootKind::Base entries must form a prefix: entry {i} is {:?}", + m.kind + ); + } + num_base +} + +/// One table's constraints: metadata + THE single body. +pub trait ConstraintSet: Send + Sync { + /// Idx-ordered metadata (see [`num_base_from_meta`] for the invariants). + fn meta(&self) -> Vec; + /// The single constraint body: emits every constraint in `meta()` exactly + /// once. + fn eval>(&self, b: &mut B); +} + +// ============================================================================= +// Debug-build emit tracking (shared by the folders) +// ============================================================================= + +/// Debug-build bitset asserting every constraint index is emitted exactly +/// once. A zero-sized no-op in release builds. +struct EmitTracker { + #[cfg(debug_assertions)] + seen: Vec, +} + +impl EmitTracker { + fn new(_num_constraints: usize) -> Self { + Self { + #[cfg(debug_assertions)] + seen: vec![false; _num_constraints], + } + } + + #[inline] + fn mark(&mut self, _idx: usize) { + #[cfg(debug_assertions)] + { + assert!( + _idx < self.seen.len(), + "constraint idx {_idx} out of range ({} constraints)", + self.seen.len() + ); + assert!(!self.seen[_idx], "constraint {_idx} emitted twice"); + self.seen[_idx] = true; + } + } + + fn assert_complete(&self) { + #[cfg(debug_assertions)] + for (i, emitted) in self.seen.iter().enumerate() { + assert!(emitted, "constraint {i} was never emitted"); + } + } +} + +// ============================================================================= +// 1. ProverEvalFolder — compiled per-row evaluation (base-field frame) +// ============================================================================= + +/// Direct evaluation over one prover row: `Expr = FieldElement`, +/// `ExprE = FieldElement`. Constructed per row from the Prover +/// [`TransitionEvaluationContext`] variant plus the output slices; +/// `emit_base` writes `base_evals[idx]`, `emit_ext` writes `ext_evals[idx]` +/// (ABSOLUTE constraint index — `ext_evals` is sized to the total constraint +/// count). This is the CPU hot path: after inlining, a body run is the same +/// machine code as a hand-written `evaluate`. +pub struct ProverEvalFolder<'a, F, E> +where + F: IsSubFieldOf, + E: IsField, +{ + frame: &'a Frame, + periodic: &'a [FieldElement], + challenges: &'a [FieldElement], + alphas: &'a [FieldElement], + logup_table_offset: &'a FieldElement, + base_out: &'a mut [FieldElement], + ext_out: &'a mut [FieldElement], + tracker: EmitTracker, +} + +impl<'a, F, E> ProverEvalFolder<'a, F, E> +where + F: IsSubFieldOf, + E: IsField, +{ + /// Build a folder from the Prover context variant. `base_out` must be + /// sized `num_base`; `ext_out` must be sized to the total constraint + /// count (matching the engine's `compute_transition_prover` contract). + /// + /// Panics if `ctx` is the Verifier variant. + pub fn new( + ctx: &TransitionEvaluationContext<'a, F, E>, + base_out: &'a mut [FieldElement], + ext_out: &'a mut [FieldElement], + ) -> Self { + let TransitionEvaluationContext::Prover { + frame, + periodic_values, + rap_challenges, + logup_alpha_powers, + logup_table_offset, + .. + } = ctx + else { + unreachable!("ProverEvalFolder::new called with a Verifier context") + }; + let num_constraints = base_out.len().max(ext_out.len()); + Self { + frame, + periodic: periodic_values, + challenges: rap_challenges, + alphas: logup_alpha_powers, + logup_table_offset, + base_out, + ext_out, + tracker: EmitTracker::new(num_constraints), + } + } + + /// Debug-build check that every constraint index was emitted exactly + /// once (no-op in release builds). Call after running a body. + pub fn assert_all_emitted(&self) { + self.tracker.assert_complete(); + } +} + +impl ConstraintBuilder for ProverEvalFolder<'_, F, E> +where + F: IsSubFieldOf, + E: IsField, +{ + type Expr = FieldElement; + type ExprE = FieldElement; + + fn main(&self, offset: usize, col: usize) -> FieldElement { + self.frame + .get_evaluation_step(offset) + .get_main_evaluation_element(0, col) + .clone() + } + fn aux(&self, offset: usize, col: usize) -> FieldElement { + self.frame + .get_evaluation_step(offset) + .get_aux_evaluation_element(0, col) + .clone() + } + fn periodic(&self, idx: usize) -> FieldElement { + self.periodic[idx].clone() + } + fn challenge(&self, idx: usize) -> FieldElement { + self.challenges[idx].clone() + } + fn alpha_pow(&self, idx: usize) -> FieldElement { + self.alphas[idx].clone() + } + fn table_offset(&self) -> FieldElement { + self.logup_table_offset.clone() + } + fn const_base(&self, v: u64) -> FieldElement { + FieldElement::::from(v) + } + fn const_signed(&self, v: i64) -> FieldElement { + FieldElement::::from(v) + } + + fn emit_base(&mut self, constraint_idx: usize, e: FieldElement) { + self.tracker.mark(constraint_idx); + self.base_out[constraint_idx] = e; + } + fn emit_ext(&mut self, constraint_idx: usize, e: FieldElement) { + debug_assert!( + constraint_idx >= self.base_out.len(), + "emit_ext with a base-prefix index {constraint_idx}" + ); + self.tracker.mark(constraint_idx); + self.ext_out[constraint_idx] = e; + } +} + +// ============================================================================= +// 2. VerifierEvalFolder — same body at the OOD point (all-extension frame) +// ============================================================================= + +/// Direct evaluation at the OOD point: the frame holds only extension +/// elements, so `Expr = FieldElement` and base-constraint results are +/// already extension values. `const_base` embeds via +/// `FieldElement::::from(v).to_extension::()`; `emit_base` writes the +/// (already promoted) value into `ext_evals[idx]`, mirroring the old +/// adapter's `evaluate(..).to_extension()` promotion. Runs once per proof at +/// the OOD point; this exact monomorphization, compiled into the guest +/// binary, is the recursion-guest path. +pub struct VerifierEvalFolder<'a, F, E> +where + F: IsSubFieldOf, + E: IsField, +{ + frame: &'a Frame, + periodic: &'a [FieldElement], + challenges: &'a [FieldElement], + alphas: &'a [FieldElement], + logup_table_offset: &'a FieldElement, + ext_out: &'a mut [FieldElement], + tracker: EmitTracker, + _base_field: PhantomData, +} + +impl<'a, F, E> VerifierEvalFolder<'a, F, E> +where + F: IsSubFieldOf, + E: IsField, +{ + /// Build a folder from the Verifier context variant. `ext_out` must be + /// sized to the total constraint count (matching the engine's + /// `compute_transition` contract). + /// + /// Panics if `ctx` is the Prover variant. + pub fn new( + ctx: &TransitionEvaluationContext<'a, F, E>, + ext_out: &'a mut [FieldElement], + ) -> Self { + let TransitionEvaluationContext::Verifier { + frame, + periodic_values, + rap_challenges, + logup_alpha_powers, + logup_table_offset, + .. + } = ctx + else { + unreachable!("VerifierEvalFolder::new called with a Prover context") + }; + let num_constraints = ext_out.len(); + Self { + frame, + periodic: periodic_values, + challenges: rap_challenges, + alphas: logup_alpha_powers, + logup_table_offset, + ext_out, + tracker: EmitTracker::new(num_constraints), + _base_field: PhantomData, + } + } + + /// Debug-build check that every constraint index was emitted exactly + /// once (no-op in release builds). Call after running a body. + pub fn assert_all_emitted(&self) { + self.tracker.assert_complete(); + } +} + +impl ConstraintBuilder for VerifierEvalFolder<'_, F, E> +where + F: IsSubFieldOf, + E: IsField, +{ + type Expr = FieldElement; + type ExprE = FieldElement; + + fn main(&self, offset: usize, col: usize) -> FieldElement { + self.frame + .get_evaluation_step(offset) + .get_main_evaluation_element(0, col) + .clone() + } + fn aux(&self, offset: usize, col: usize) -> FieldElement { + self.frame + .get_evaluation_step(offset) + .get_aux_evaluation_element(0, col) + .clone() + } + fn periodic(&self, idx: usize) -> FieldElement { + self.periodic[idx].clone() + } + fn challenge(&self, idx: usize) -> FieldElement { + self.challenges[idx].clone() + } + fn alpha_pow(&self, idx: usize) -> FieldElement { + self.alphas[idx].clone() + } + fn table_offset(&self) -> FieldElement { + self.logup_table_offset.clone() + } + fn const_base(&self, v: u64) -> FieldElement { + FieldElement::::from(v).to_extension::() + } + fn const_signed(&self, v: i64) -> FieldElement { + FieldElement::::from(v).to_extension::() + } + + fn emit_base(&mut self, constraint_idx: usize, e: FieldElement) { + self.tracker.mark(constraint_idx); + self.ext_out[constraint_idx] = e; + } + fn emit_ext(&mut self, constraint_idx: usize, e: FieldElement) { + self.tracker.mark(constraint_idx); + self.ext_out[constraint_idx] = e; + } +} + +// ============================================================================= +// 3. CaptureBuilder — owned expression tree, flattened into the flat IR +// ============================================================================= + +/// One node of the capture tree. `degree` is eager (leaf var/periodic = 1, +/// constants/uniforms = 0, mul sums, add/sub max, neg passthrough — p3's +/// `degree_multiple`). +struct TreeNode { + kind: TreeKind, + dim: Dim, + degree: usize, +} + +enum TreeKind { + Main { + offset: u8, + col: u16, + }, + Aux { + offset: u8, + col: u16, + }, + Periodic(u16), + Challenge(u16), + AlphaPow(u16), + TableOffset, + /// Raw `u64` base-field constant; canonicalized (and value-deduplicated) + /// by the [`IrBuilder`] at flatten time. + ConstBase(u64), + /// Raw `i64` base-field constant; negatives map to `p - |v|` at flatten + /// time, exactly as `IrBuilder::const_signed`. + ConstSigned(i64), + Add(IrExpr, IrExpr), + Sub(IrExpr, IrExpr), + Mul(IrExpr, IrExpr), + Neg(IrExpr), +} + +/// Owned capture expression: `Rc` tree with operator overloading. Cloning is +/// a pointer bump; operators allocate nodes — no arena, no interior +/// mutability, no hashing (CSE happens at flatten time via [`IrBuilder`]). +/// Constants carry raw integers, so the tree needs no field type parameters. +#[derive(Clone)] +pub struct IrExpr(Rc); + +impl IrExpr { + fn leaf(kind: TreeKind, dim: Dim, degree: usize) -> Self { + IrExpr(Rc::new(TreeNode { kind, dim, degree })) + } + + fn join(a: Dim, b: Dim) -> Dim { + match (a, b) { + (Dim::Base, Dim::Base) => Dim::Base, + _ => Dim::Ext, + } + } + + fn binop(f: fn(IrExpr, IrExpr) -> TreeKind, degree: usize, a: IrExpr, b: IrExpr) -> Self { + let dim = Self::join(a.0.dim, b.0.dim); + IrExpr(Rc::new(TreeNode { + kind: f(a, b), + dim, + degree, + })) + } + + /// The tree-measured constraint degree (multivariate, in trace columns). + pub fn degree(&self) -> usize { + self.0.degree + } +} + +impl Add for IrExpr { + type Output = IrExpr; + fn add(self, rhs: IrExpr) -> IrExpr { + let d = self.0.degree.max(rhs.0.degree); + IrExpr::binop(TreeKind::Add, d, self, rhs) + } +} +impl Sub for IrExpr { + type Output = IrExpr; + fn sub(self, rhs: IrExpr) -> IrExpr { + let d = self.0.degree.max(rhs.0.degree); + IrExpr::binop(TreeKind::Sub, d, self, rhs) + } +} +impl Mul for IrExpr { + type Output = IrExpr; + // The degree of a product is the SUM of the factor degrees. + #[allow(clippy::suspicious_arithmetic_impl)] + fn mul(self, rhs: IrExpr) -> IrExpr { + let d = self.0.degree + rhs.0.degree; + IrExpr::binop(TreeKind::Mul, d, self, rhs) + } +} +impl Neg for IrExpr { + type Output = IrExpr; + fn neg(self) -> IrExpr { + let (dim, degree) = (self.0.dim, self.0.degree); + IrExpr(Rc::new(TreeNode { + kind: TreeKind::Neg(self), + dim, + degree, + })) + } +} + +/// Captures every emitted constraint into a [`ConstraintProgram`] by +/// flattening the finished trees into an [`IrBuilder`] (whose hash-consing +/// provides structural CSE, host-side, once at setup). Also records each +/// root's tree-measured degree — the degree-measurement API backing the +/// declared-vs-measured gate. +pub struct CaptureBuilder { + ir: IrBuilder, + /// `(constraint_idx, tree-measured degree)` per emit. + degrees: Vec<(usize, usize)>, +} + +impl Default for CaptureBuilder { + fn default() -> Self { + Self::new() + } +} + +impl CaptureBuilder { + pub fn new() -> Self { + Self { + ir: IrBuilder::new(), + degrees: Vec::new(), + } + } + + fn flatten(&mut self, e: &IrExpr) -> crate::constraint_ir::Expr { + match &e.0.kind { + TreeKind::Main { offset, col } => self.ir.main(*offset, *col as usize), + TreeKind::Aux { offset, col } => self.ir.aux(*offset, *col as usize), + TreeKind::Periodic(idx) => self.ir.periodic(*idx as usize), + TreeKind::Challenge(idx) => self.ir.challenge(*idx as usize), + TreeKind::AlphaPow(idx) => self.ir.alpha_power(*idx as usize), + TreeKind::TableOffset => self.ir.table_offset(), + TreeKind::ConstBase(v) => self.ir.const_base(*v), + TreeKind::ConstSigned(v) => self.ir.const_signed(*v), + TreeKind::Add(a, b) => { + let (fa, fb) = (self.flatten(a), self.flatten(b)); + self.ir.add(fa, fb) + } + TreeKind::Sub(a, b) => { + let (fa, fb) = (self.flatten(a), self.flatten(b)); + self.ir.sub(fa, fb) + } + TreeKind::Mul(a, b) => { + let (fa, fb) = (self.flatten(a), self.flatten(b)); + self.ir.mul(fa, fb) + } + TreeKind::Neg(a) => { + let fa = self.flatten(a); + self.ir.neg(fa) + } + } + } + + /// Finish capture: `(program, per-emit tree-measured degrees)`. + pub fn finish(self, num_base: usize) -> (ConstraintProgram, Vec<(usize, usize)>) { + (self.ir.finish(num_base), self.degrees) + } +} + +impl ConstraintBuilder for CaptureBuilder { + type Expr = IrExpr; + type ExprE = IrExpr; + + fn main(&self, offset: usize, col: usize) -> IrExpr { + IrExpr::leaf( + TreeKind::Main { + offset: offset as u8, + col: col as u16, + }, + Dim::Base, + 1, + ) + } + fn aux(&self, offset: usize, col: usize) -> IrExpr { + IrExpr::leaf( + TreeKind::Aux { + offset: offset as u8, + col: col as u16, + }, + Dim::Ext, + 1, + ) + } + fn periodic(&self, idx: usize) -> IrExpr { + IrExpr::leaf(TreeKind::Periodic(idx as u16), Dim::Base, 1) + } + fn challenge(&self, idx: usize) -> IrExpr { + IrExpr::leaf(TreeKind::Challenge(idx as u16), Dim::Ext, 0) + } + fn alpha_pow(&self, idx: usize) -> IrExpr { + IrExpr::leaf(TreeKind::AlphaPow(idx as u16), Dim::Ext, 0) + } + fn table_offset(&self) -> IrExpr { + IrExpr::leaf(TreeKind::TableOffset, Dim::Ext, 0) + } + fn const_base(&self, v: u64) -> IrExpr { + IrExpr::leaf(TreeKind::ConstBase(v), Dim::Base, 0) + } + fn const_signed(&self, v: i64) -> IrExpr { + IrExpr::leaf(TreeKind::ConstSigned(v), Dim::Base, 0) + } + + fn emit_base(&mut self, constraint_idx: usize, e: IrExpr) { + debug_assert_eq!(e.0.dim, Dim::Base, "emit_base on an extension expression"); + let root = self.flatten(&e); + self.ir.emit(constraint_idx, root); + self.degrees.push((constraint_idx, e.degree())); + } + fn emit_ext(&mut self, constraint_idx: usize, e: IrExpr) { + let root = self.flatten(&e); + self.ir.emit(constraint_idx, root); + self.degrees.push((constraint_idx, e.degree())); + } +} diff --git a/crypto/stark/src/constraints/builder_tests.rs b/crypto/stark/src/constraints/builder_tests.rs new file mode 100644 index 000000000..3860a4588 --- /dev/null +++ b/crypto/stark/src/constraints/builder_tests.rs @@ -0,0 +1,441 @@ +//! Tests for the `ConstraintBuilder` framework: one sample [`ConstraintSet`] +//! (EqXor-shaped, IsBit-shaped and Add-carry-pair-shaped bodies, plus a +//! LogUp-shaped extension constraint) checked three ways on random rows: +//! +//! 1. `ProverEvalFolder` output == direct `FieldElement` arithmetic; +//! 2. `ProverEvalFolder` output == `eval_program` over the captured program; +//! 3. `VerifierEvalFolder` output == `eval_program_verifier` over the captured +//! program; +//! +//! plus: capture-measured degrees == declared `meta.degree`, the meta +//! Base-prefix/density invariants, and the folders' debug-build +//! exactly-once/completeness asserts. + +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext; +use math::field::goldilocks::GoldilocksField as Fp; + +use crate::constraint_ir::{eval_program, eval_program_verifier}; +use crate::constraints::builder::{ + CaptureBuilder, ConstraintBuilder, ConstraintMeta, ConstraintSet, ProverEvalFolder, RootKind, + VerifierEvalFolder, num_base_from_meta, +}; +use crate::frame::Frame; +use crate::lookup::PackingShifts; +use crate::table::TableView; +use crate::traits::TransitionEvaluationContext; + +type FpE = FieldElement; +type ExtE = FieldElement; + +const TRIALS: usize = 1000; + +/// Deterministic SplitMix64. +struct SplitMix64(u64); +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + fn fp(&mut self) -> FpE { + FpE::from(self.next_u64()) + } + fn ext(&mut self) -> ExtE { + ExtE::from_raw([self.fp(), self.fp(), self.fp()]) + } +} + +// ============================================================================= +// The sample table: local column layout + single body +// ============================================================================= + +mod cols { + // EqXor: res = eq XOR invert. + pub const RES: usize = 0; + pub const EQ: usize = 1; + pub const INVERT: usize = 2; + // IsBit. + pub const BIT: usize = 3; + // Add carry pair (64-bit add split in 32-bit halves), gated by COND. + pub const COND: usize = 4; + pub const LHS_LO: usize = 5; + pub const LHS_HI: usize = 6; + pub const RHS_LO: usize = 7; + pub const RHS_HI: usize = 8; + pub const SUM_LO: usize = 9; + pub const SUM_HI: usize = 10; + pub const NUM_COLS: usize = 11; +} + +/// `2^-32` as a canonical Goldilocks `u64` (the add-carry repack constant). +fn inv_shift_32() -> u64 { + *FpE::from(1u64 << 32).inv().unwrap().value() +} + +/// Sample table: 4 base constraints + 1 LogUp-shaped extension constraint. +struct SampleSet; + +impl ConstraintSet for SampleSet { + fn meta(&self) -> Vec { + vec![ + ConstraintMeta::base(0, 2), // EqXor + ConstraintMeta::base(1, 2), // IsBit + ConstraintMeta::base(2, 3), // add carry 0 (cond-gated bit check) + ConstraintMeta::base(3, 3), // add carry 1 + ConstraintMeta::ext(4, 1), // LogUp-shaped + ] + } + + fn eval>(&self, b: &mut B) { + // idx 0 — EqXor: res − (eq + invert − 2·eq·invert). + let res = b.main(0, cols::RES); + let eq = b.main(0, cols::EQ); + let invert = b.main(0, cols::INVERT); + let two = b.const_base(2); + b.emit_base(0, res - (eq.clone() + invert.clone() - two * eq * invert)); + + // idx 1 — IsBit: x·(1 − x). + let x = b.main(0, cols::BIT); + let one = b.one(); + b.emit_base(1, x.clone() * (one - x)); + + // idx 2, 3 — the add carry pair: + // carry_0 = (lhs.lo + rhs.lo − sum.lo)·2⁻³² + // carry_1 = (lhs.hi + rhs.hi + carry_0 − sum.hi)·2⁻³² + // emit cond·carry_i·(1 − carry_i). + let inv_2_32 = b.const_base(inv_shift_32()); + let lhs_lo = b.main(0, cols::LHS_LO); + let lhs_hi = b.main(0, cols::LHS_HI); + let rhs_lo = b.main(0, cols::RHS_LO); + let rhs_hi = b.main(0, cols::RHS_HI); + let sum_lo = b.main(0, cols::SUM_LO); + let sum_hi = b.main(0, cols::SUM_HI); + let cond = b.main(0, cols::COND); + let one = b.one(); + let carry_0 = (lhs_lo + rhs_lo - sum_lo) * inv_2_32.clone(); + let carry_1 = (lhs_hi + rhs_hi + carry_0.clone() - sum_hi) * inv_2_32; + b.emit_base(2, cond.clone() * carry_0.clone() * (one.clone() - carry_0)); + b.emit_base(3, cond * carry_1.clone() * (one - carry_1)); + + // idx 4 — LogUp-shaped: (challenge₀ + aux₀)·alpha₀ − L/N. + let ch = b.challenge(0); + let au = b.aux(0, 0); + let alpha = b.alpha_pow(0); + let off = b.table_offset(); + b.emit_ext(4, (ch + au) * alpha - off); + } +} + +const NUM_BASE: usize = 4; +const NUM_CONSTRAINTS: usize = 5; + +/// Direct `FieldElement` arithmetic reference for the sample set's base +/// constraints on a main row. +fn direct_base(row: &[FpE]) -> [FpE; NUM_BASE] { + let two = FpE::from(2u64); + let one = FpE::one(); + let inv = FpE::from(1u64 << 32).inv().unwrap(); + + let c0 = row[cols::RES] + - (row[cols::EQ] + row[cols::INVERT] - two * row[cols::EQ] * row[cols::INVERT]); + let c1 = row[cols::BIT] * (one - row[cols::BIT]); + let carry_0 = (row[cols::LHS_LO] + row[cols::RHS_LO] - row[cols::SUM_LO]) * inv; + let carry_1 = (row[cols::LHS_HI] + row[cols::RHS_HI] + carry_0 - row[cols::SUM_HI]) * inv; + let c2 = row[cols::COND] * carry_0 * (one - carry_0); + let c3 = row[cols::COND] * carry_1 * (one - carry_1); + [c0, c1, c2, c3] +} + +/// Direct reference for the extension constraint. +fn direct_ext(aux0: &ExtE, challenge0: &ExtE, alpha0: &ExtE, offset: &ExtE) -> ExtE { + (*challenge0 + *aux0) * *alpha0 - *offset +} + +/// One random trial's inputs. +struct TrialData { + row: Vec, + aux0: ExtE, + challenge0: ExtE, + alpha0: ExtE, + offset: ExtE, +} + +fn random_trial(rng: &mut SplitMix64) -> TrialData { + TrialData { + row: (0..cols::NUM_COLS).map(|_| rng.fp()).collect(), + aux0: rng.ext(), + challenge0: rng.ext(), + alpha0: rng.ext(), + offset: rng.ext(), + } +} + +// ============================================================================= +// The three-way differential checks +// ============================================================================= + +#[test] +fn prover_folder_matches_direct_arithmetic() { + let shifts = PackingShifts::::new(); + let mut rng = SplitMix64(0x0001_F01D_u64 ^ 0xABCD); + for trial in 0..TRIALS { + let t = random_trial(&mut rng); + let step = TableView::::new(vec![t.row.clone()], vec![vec![t.aux0]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let challenges = vec![t.challenge0]; + let alphas = vec![t.alpha0]; + let ctx = TransitionEvaluationContext::new_prover( + &frame, + &periodic, + &challenges, + &alphas, + &t.offset, + &shifts, + ); + + let mut base_out = vec![FpE::zero(); NUM_BASE]; + let mut ext_out = vec![ExtE::zero(); NUM_CONSTRAINTS]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); + SampleSet.eval(&mut folder); + folder.assert_all_emitted(); + + let expected_base = direct_base(&t.row); + for (i, expected) in expected_base.iter().enumerate() { + assert_eq!(&base_out[i], expected, "base constraint {i}, trial {trial}"); + } + let expected_ext = direct_ext(&t.aux0, &t.challenge0, &t.alpha0, &t.offset); + assert_eq!(ext_out[4], expected_ext, "ext constraint, trial {trial}"); + } +} + +#[test] +fn prover_folder_matches_interpreted_capture() { + // Capture once (setup-time), interpret per row. + let mut cb = CaptureBuilder::::new(); + SampleSet.eval(&mut cb); + let (prog, _degrees) = cb.finish(NUM_BASE); + + let shifts = PackingShifts::::new(); + let mut rng = SplitMix64(0x0002_F01D_u64 ^ 0xABCD); + for trial in 0..TRIALS { + let t = random_trial(&mut rng); + let step = TableView::::new(vec![t.row.clone()], vec![vec![t.aux0]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let challenges = vec![t.challenge0]; + let alphas = vec![t.alpha0]; + let ctx = TransitionEvaluationContext::new_prover( + &frame, + &periodic, + &challenges, + &alphas, + &t.offset, + &shifts, + ); + + let mut folder_base = vec![FpE::zero(); NUM_BASE]; + let mut folder_ext = vec![ExtE::zero(); NUM_CONSTRAINTS]; + let mut folder = ProverEvalFolder::new(&ctx, &mut folder_base, &mut folder_ext); + SampleSet.eval(&mut folder); + folder.assert_all_emitted(); + + let mut interp_base = vec![FpE::zero(); NUM_BASE]; + let mut interp_ext = vec![ExtE::zero(); NUM_CONSTRAINTS]; + eval_program(&prog, &ctx, &mut interp_base, &mut interp_ext); + + assert_eq!(folder_base, interp_base, "base evals, trial {trial}"); + assert_eq!(folder_ext[4], interp_ext[4], "ext eval, trial {trial}"); + } +} + +#[test] +fn verifier_folder_matches_interpreted_capture() { + let mut cb = CaptureBuilder::::new(); + SampleSet.eval(&mut cb); + let (prog, _degrees) = cb.finish(NUM_BASE); + + let shifts = PackingShifts::::new(); + let mut rng = SplitMix64(0x0003_F01D_u64 ^ 0xABCD); + for trial in 0..TRIALS { + let t = random_trial(&mut rng); + // The verifier frame holds only extension elements (OOD evaluations). + let row_e: Vec = t.row.iter().map(|x| x.to_extension()).collect(); + let step = TableView::::new(vec![row_e], vec![vec![t.aux0]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let challenges = vec![t.challenge0]; + let alphas = vec![t.alpha0]; + let ctx = TransitionEvaluationContext::::new_verifier( + &frame, + &periodic, + &challenges, + &alphas, + &t.offset, + &shifts, + ); + + let mut folder_ext = vec![ExtE::zero(); NUM_CONSTRAINTS]; + let mut folder = VerifierEvalFolder::new(&ctx, &mut folder_ext); + SampleSet.eval(&mut folder); + folder.assert_all_emitted(); + + let mut interp_ext = vec![ExtE::zero(); NUM_CONSTRAINTS]; + eval_program_verifier(&prog, &ctx, &mut interp_ext); + + assert_eq!(folder_ext, interp_ext, "ood evals, trial {trial}"); + } +} + +// ============================================================================= +// Degree measurement + meta invariants +// ============================================================================= + +#[test] +fn capture_measured_degrees_match_declared_meta() { + let mut cb = CaptureBuilder::::new(); + SampleSet.eval(&mut cb); + let (prog, degrees) = cb.finish(NUM_BASE); + assert!(prog.complete); + assert_eq!(prog.roots.len(), NUM_CONSTRAINTS); + + let meta = SampleSet.meta(); + assert_eq!(degrees.len(), meta.len()); + for (i, &(idx, measured)) in degrees.iter().enumerate() { + assert_eq!(idx, i, "emit order != idx order"); + assert_eq!( + measured, meta[idx].degree, + "constraint {idx}: tree-measured degree {measured} != declared {}", + meta[idx].degree + ); + } +} + +#[test] +fn meta_base_prefix_gives_num_base() { + assert_eq!(num_base_from_meta(&SampleSet.meta()), NUM_BASE); + + // Pure-base and pure-ext lists. + let pure_base = vec![ConstraintMeta::base(0, 1), ConstraintMeta::base(1, 2)]; + assert_eq!(num_base_from_meta(&pure_base), 2); + let pure_ext = vec![ConstraintMeta::ext(0, 1), ConstraintMeta::ext(1, 1)]; + assert_eq!(num_base_from_meta(&pure_ext), 0); + assert_eq!(num_base_from_meta(&[]), 0); + + // RootKind sanity on the sample. + let meta = SampleSet.meta(); + assert!(meta[..NUM_BASE].iter().all(|m| m.kind == RootKind::Base)); + assert!(meta[NUM_BASE..].iter().all(|m| m.kind == RootKind::Ext)); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "must form a prefix")] +fn meta_base_after_ext_panics() { + let bad = vec![ + ConstraintMeta::base(0, 1), + ConstraintMeta::ext(1, 1), + ConstraintMeta::base(2, 1), + ]; + num_base_from_meta(&bad); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "dense and idx-ordered")] +fn meta_non_dense_panics() { + let bad = vec![ConstraintMeta::base(0, 1), ConstraintMeta::base(2, 1)]; + num_base_from_meta(&bad); +} + +// ============================================================================= +// Folder completeness asserts (debug builds) +// ============================================================================= + +/// Run a body that emits only constraint 0 of 2, then check completeness. +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "never emitted")] +fn prover_folder_missing_emit_asserts() { + let shifts = PackingShifts::::new(); + let step = TableView::::new(vec![vec![FpE::zero(); cols::NUM_COLS]], vec![vec![]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let challenges: Vec = vec![]; + let alphas: Vec = vec![]; + let offset = ExtE::zero(); + let ctx = TransitionEvaluationContext::new_prover( + &frame, + &periodic, + &challenges, + &alphas, + &offset, + &shifts, + ); + + let mut base_out = vec![FpE::zero(); 2]; + let mut ext_out = vec![ExtE::zero(); 2]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); + let x = folder.main(0, 0); + folder.emit_base(0, x); // constraint 1 never emitted + folder.assert_all_emitted(); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "emitted twice")] +fn prover_folder_double_emit_asserts() { + let shifts = PackingShifts::::new(); + let step = TableView::::new(vec![vec![FpE::zero(); cols::NUM_COLS]], vec![vec![]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let challenges: Vec = vec![]; + let alphas: Vec = vec![]; + let offset = ExtE::zero(); + let ctx = TransitionEvaluationContext::new_prover( + &frame, + &periodic, + &challenges, + &alphas, + &offset, + &shifts, + ); + + let mut base_out = vec![FpE::zero(); 2]; + let mut ext_out = vec![ExtE::zero(); 2]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); + let x = folder.main(0, 0); + folder.emit_base(0, x); + let x = folder.main(0, 0); + folder.emit_base(0, x); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "never emitted")] +fn verifier_folder_missing_emit_asserts() { + let shifts = PackingShifts::::new(); + let step = TableView::::new(vec![vec![ExtE::zero(); cols::NUM_COLS]], vec![vec![]]); + let frame = Frame::::new(vec![step]); + let periodic: Vec = vec![]; + let challenges: Vec = vec![]; + let alphas: Vec = vec![]; + let offset = ExtE::zero(); + let ctx = TransitionEvaluationContext::::new_verifier( + &frame, + &periodic, + &challenges, + &alphas, + &offset, + &shifts, + ); + + let mut ext_out = vec![ExtE::zero(); 2]; + let mut folder = VerifierEvalFolder::new(&ctx, &mut ext_out); + let x = folder.main(0, 0); + folder.emit_base(1, x); + folder.assert_all_emitted(); +} diff --git a/crypto/stark/src/constraints/mod.rs b/crypto/stark/src/constraints/mod.rs index 3811523b5..589e726e3 100644 --- a/crypto/stark/src/constraints/mod.rs +++ b/crypto/stark/src/constraints/mod.rs @@ -1,3 +1,6 @@ pub mod boundary; +pub mod builder; +#[cfg(test)] +mod builder_tests; pub mod evaluator; pub mod transition; From 87ddd90b29e68270d36f457dc872ce7a51b429dd Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 1 Jul 2026 22:14:04 -0300 Subject: [PATCH 04/52] stark: zerofier evaluation as free functions of ConstraintMeta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New module crypto/stark/src/constraints/zerofier.rs: the bodies of the TransitionConstraintEvaluator default methods (end_exemptions_roots, end_exemptions_lde_evaluations, zerofier_evaluations_on_extended_domain, evaluate_zerofier) relocated verbatim to free functions consuming plain ConstraintMeta — they only ever read the metadata getters. The trait defaults remain untouched and stay the production path until tables convert to ConstraintSet. Tests assert the free functions match the trait defaults bit-for-bit on a configurable boxed constraint across every branch: the default shape, end exemptions, period/offset combinations, and periodic exemptions with and without end exemptions, over both the LDE-domain and OOD entry points. --- crypto/stark/src/constraints/mod.rs | 1 + crypto/stark/src/constraints/zerofier.rs | 443 +++++++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 crypto/stark/src/constraints/zerofier.rs diff --git a/crypto/stark/src/constraints/mod.rs b/crypto/stark/src/constraints/mod.rs index 589e726e3..cf3921629 100644 --- a/crypto/stark/src/constraints/mod.rs +++ b/crypto/stark/src/constraints/mod.rs @@ -4,3 +4,4 @@ pub mod builder; mod builder_tests; pub mod evaluator; pub mod transition; +pub mod zerofier; diff --git a/crypto/stark/src/constraints/zerofier.rs b/crypto/stark/src/constraints/zerofier.rs new file mode 100644 index 000000000..debc0c389 --- /dev/null +++ b/crypto/stark/src/constraints/zerofier.rs @@ -0,0 +1,443 @@ +//! Zerofier evaluation as free functions of [`ConstraintMeta`]. +//! +//! These are the bodies of the `TransitionConstraintEvaluator` default +//! methods (`crate::constraints::transition`), relocated verbatim to consume +//! plain constraint metadata instead of trait getters — they only ever read +//! `period` / `offset` / `exemptions_period` / `periodic_exemptions_offset` / +//! `end_exemptions`. The engine's zerofier machinery moves onto these once +//! tables convert to [`ConstraintSet`](crate::constraints::builder::ConstraintSet); +//! until then the trait defaults remain the production path (equivalence is +//! asserted by the tests below). + +use core::ops::Div; + +use math::field::element::FieldElement; +use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; + +use crate::constraints::builder::ConstraintMeta; +use crate::domain::Domain; + +/// Roots of the end-exemptions polynomial `∏(x - rᵢ)`. +/// +/// The end-exemptions polynomial vanishes on the last `end_exemptions` rows +/// the constraint must skip. This returns its roots `rᵢ` so callers can +/// evaluate the product `∏(x - rᵢ)` directly at the points they need. +pub fn end_exemptions_roots( + meta: &ConstraintMeta, + trace_primitive_root: &FieldElement, + trace_length: usize, +) -> Vec> { + let end_exemptions = meta.end_exemptions; + if end_exemptions == 0 { + return Vec::new(); + } + // Last row in the constraint's evaluation domain is g^(offset + N - period); + // walking backward by g^period gives the remaining end-exemption roots. + let period = meta.period; + let decrement = trace_primitive_root.pow(trace_length - period); + let mut current = trace_primitive_root.pow(meta.offset + trace_length - period); + let mut roots = Vec::with_capacity(end_exemptions); + for _ in 0..end_exemptions { + roots.push(current.clone()); + current = ¤t * &decrement; + } + roots +} + +/// Evaluations of the end-exemptions polynomial `∏(x - rᵢ)` over the LDE +/// domain. +/// +/// The product has degree `end_exemptions` (≤ 2 in practice), so the direct +/// `O(N · end_exemptions)` product over the precomputed LDE coset is cheaper +/// than an `O(N log N)` FFT. With no exemptions this yields all ones. +pub fn end_exemptions_lde_evaluations( + meta: &ConstraintMeta, + domain: &Domain, +) -> Vec> { + let roots = end_exemptions_roots( + meta, + &domain.trace_primitive_root, + domain.trace_roots_of_unity.len(), + ); + domain + .lde_roots_of_unity_coset + .iter() + .map(|x| { + roots + .iter() + .fold(FieldElement::::one(), |acc, r| acc * (x - r)) + }) + .collect() +} + +/// Compute evaluations of the constraint's zerofier over a LDE domain. +/// +/// With no end exemptions the zerofier is cyclic, so a short period-length +/// vector is returned and the consumer cycles it (same contract as the trait +/// default this body was moved from). +#[allow(unstable_name_collisions)] +pub fn zerofier_evaluations_on_extended_domain( + meta: &ConstraintMeta, + domain: &Domain, +) -> Vec> { + let blowup_factor = domain.blowup_factor; + let trace_length = domain.trace_roots_of_unity.len(); + let trace_primitive_root = &domain.trace_primitive_root; + let coset_offset = &domain.coset_offset; + let lde_root_order = u64::from((blowup_factor * trace_length).trailing_zeros()); + let lde_root = F::get_primitive_root_of_unity(lde_root_order).unwrap(); + + // If there is an exemptions period defined for this constraint, the evaluations are calculated directly + // by computing P_exemptions(x) / Zerofier(x) + if let Some(exemptions_period) = meta.exemptions_period { + debug_assert!(exemptions_period.is_multiple_of(meta.period)); + debug_assert!(meta.periodic_exemptions_offset.is_some()); + + // The elements of the domain have order `trace_length * blowup_factor`, so the zerofier evaluations + // without the end exemptions, repeat their values after `blowup_factor * exemptions_period` iterations, + // so we only need to compute those. + let last_exponent = blowup_factor * exemptions_period; + let numerator_power = trace_length / exemptions_period; + let denominator_power = trace_length / meta.period; + let offset_exponent = + trace_length * meta.periodic_exemptions_offset.unwrap() / exemptions_period; + let numerator_offset = trace_primitive_root.pow(offset_exponent); + let denominator_offset = trace_primitive_root.pow(meta.offset * denominator_power); + let numerator_step = lde_root.pow(numerator_power); + let denominator_step = lde_root.pow(denominator_power); + let mut numerator_eval = coset_offset.pow(numerator_power); + let mut denominator_eval = coset_offset.pow(denominator_power); + + let mut numerators = Vec::with_capacity(last_exponent); + let mut denominators = Vec::with_capacity(last_exponent); + for _ in 0..last_exponent { + numerators.push(&numerator_eval - &numerator_offset); + denominators.push(&denominator_eval - &denominator_offset); + numerator_eval = &numerator_eval * &numerator_step; + denominator_eval = &denominator_eval * &denominator_step; + } + + // Batch inversion: O(3N) muls + 1 inversion instead of N individual inversions. + // Denominators are guaranteed non-zero because the sets of powers of + // `offset_times_x` and `trace_primitive_root` are disjoint, provided that the + // offset is neither an element of the interpolation domain nor part of a + // subgroup with order less than n. + FieldElement::inplace_batch_inverse(&mut denominators).unwrap(); + + let evaluations: Vec<_> = numerators + .iter() + .zip(denominators.iter()) + .map(|(num, denom_inv)| num * denom_inv) + .collect(); + + // Mirror the else-branch fast path: with no end exemptions the zerofier stays + // cyclic, so return the short period-length vector and let the consumer cycle. + if meta.end_exemptions == 0 { + return evaluations; + } + + let end_exemption_evaluations = end_exemptions_lde_evaluations(meta, domain); + + let cycled_evaluations = evaluations + .iter() + .cycle() + .take(end_exemption_evaluations.len()); + + core::iter::zip(cycled_evaluations, end_exemption_evaluations) + .map(|(eval, exemption_eval)| eval * exemption_eval) + .collect() + + // In this else branch, the zerofiers are computed as the numerator, then inverted + // using batch inverse and then multiplied by P_exemptions(x). This way we don't do + // useless divisions. + } else { + let last_exponent = blowup_factor * meta.period; + let denominator_power = trace_length / meta.period; + let denominator_offset = trace_primitive_root.pow(meta.offset * denominator_power); + let denominator_step = lde_root.pow(denominator_power); + let mut denominator_eval = coset_offset.pow(denominator_power); + + let mut evaluations = Vec::with_capacity(last_exponent); + for _ in 0..last_exponent { + evaluations.push(&denominator_eval - &denominator_offset); + denominator_eval = &denominator_eval * &denominator_step; + } + + FieldElement::inplace_batch_inverse(&mut evaluations).unwrap(); + + // Fast path: when end_exemptions == 0 there are no exemption roots, so + // the zerofier stays cyclic — return the short period-length vector + // directly instead of expanding it over the full LDE domain. + if meta.end_exemptions == 0 { + return evaluations; + } + + let end_exemption_evaluations = end_exemptions_lde_evaluations(meta, domain); + + let cycled_evaluations = evaluations + .iter() + .cycle() + .take(end_exemption_evaluations.len()); + + core::iter::zip(cycled_evaluations, end_exemption_evaluations) + .map(|(eval, exemption_eval)| eval * exemption_eval) + .collect() + } +} + +/// Evaluation of the constraint's zerofier at some point `z`, which may be in +/// a field extension. +#[allow(unstable_name_collisions)] +pub fn evaluate_zerofier( + meta: &ConstraintMeta, + z: &FieldElement, + trace_primitive_root: &FieldElement, + trace_length: usize, +) -> FieldElement +where + F: IsSubFieldOf, + E: IsField, +{ + let roots = end_exemptions_roots(meta, trace_primitive_root, trace_length); + // Factor `z - rᵢ` written as `-(rᵢ - z)`: the field ops only go + // subfield − superfield, and `rᵢ ∈ F`, `z ∈ E`. + let end_exemptions_eval = roots.iter().fold(FieldElement::::one(), |acc, root| { + acc * -(root.clone() - z.clone()) + }); + + if let Some(exemptions_period) = meta.exemptions_period { + debug_assert!(exemptions_period.is_multiple_of(meta.period)); + debug_assert!(meta.periodic_exemptions_offset.is_some()); + + let periodic_exemptions_offset = meta.periodic_exemptions_offset.unwrap(); + let offset_exponent = trace_length * periodic_exemptions_offset / exemptions_period; + + let numerator = + -trace_primitive_root.pow(offset_exponent) + z.pow(trace_length / exemptions_period); + let denominator = -trace_primitive_root.pow(meta.offset * trace_length / meta.period) + + z.pow(trace_length / meta.period); + // The denominator is non-zero: z is sampled outside the set of primitive roots. + return numerator + .div(denominator) + .expect("zerofier denominator is non-zero: z is sampled out-of-domain") + * &end_exemptions_eval; + } + + (-trace_primitive_root.pow(meta.offset * trace_length / meta.period) + + z.pow(trace_length / meta.period)) + .inv() + .unwrap() + * &end_exemptions_eval +} + +// ============================================================================= +// Equivalence tests: free functions == the trait defaults they were moved from +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use crate::constraints::transition::TransitionConstraintEvaluator; + use crate::traits::TransitionEvaluationContext; + use math::fft::roots_of_unity::get_powers_of_primitive_root_coset; + use math::field::element::FieldElement; + use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext; + use math::field::goldilocks::GoldilocksField as Fp; + + type FpE = FieldElement; + type ExtE = FieldElement; + + /// A boxed-path constraint whose zerofier shape is configurable — used to + /// run the ORIGINAL trait-default bodies for comparison. + struct DummyConstraint { + period: usize, + offset: usize, + exemptions_period: Option, + periodic_exemptions_offset: Option, + end_exemptions: usize, + } + + impl TransitionConstraintEvaluator for DummyConstraint { + fn degree(&self) -> usize { + 1 + } + fn constraint_idx(&self) -> usize { + 0 + } + fn evaluate_verifier( + &self, + _ctx: &TransitionEvaluationContext, + _evals: &mut [ExtE], + ) { + } + fn period(&self) -> usize { + self.period + } + fn offset(&self) -> usize { + self.offset + } + fn exemptions_period(&self) -> Option { + self.exemptions_period + } + fn periodic_exemptions_offset(&self) -> Option { + self.periodic_exemptions_offset + } + fn end_exemptions(&self) -> usize { + self.end_exemptions + } + } + + impl DummyConstraint { + fn meta(&self) -> ConstraintMeta { + let mut m = ConstraintMeta::base(0, 1) + .with_period(self.period) + .with_offset(self.offset) + .with_end_exemptions(self.end_exemptions); + if let Some(p) = self.exemptions_period { + m = m.with_exemptions(p, self.periodic_exemptions_offset.unwrap()); + } + m + } + } + + /// Hand-built prover Domain: trace length 16, blowup 4, coset offset 7. + fn sample_domain() -> Domain { + let trace_length: usize = 16; + let blowup_factor: usize = 4; + let coset_offset = FpE::from(7u64); + let root_order = trace_length.trailing_zeros(); + let trace_primitive_root = + ::get_primitive_root_of_unity(root_order as u64) + .unwrap(); + let trace_roots_of_unity = get_powers_of_primitive_root_coset( + root_order as u64, + trace_length, + &FieldElement::one(), + ) + .unwrap(); + let lde_root_order = (trace_length * blowup_factor).trailing_zeros(); + let lde_roots_of_unity_coset = get_powers_of_primitive_root_coset( + lde_root_order as u64, + trace_length * blowup_factor, + &coset_offset, + ) + .unwrap(); + Domain { + root_order, + lde_roots_of_unity_coset, + trace_primitive_root, + trace_roots_of_unity, + coset_offset, + blowup_factor, + interpolation_domain_size: trace_length, + } + } + + /// The zerofier configurations exercised: every branch of the moved + /// bodies (default; end exemptions; period/offset; periodic exemptions + /// with and without end exemptions). + fn sample_configs() -> Vec { + vec![ + // Every row, no exemptions (the common case). + DummyConstraint { + period: 1, + offset: 0, + exemptions_period: None, + periodic_exemptions_offset: None, + end_exemptions: 0, + }, + // End exemptions only. + DummyConstraint { + period: 1, + offset: 0, + exemptions_period: None, + periodic_exemptions_offset: None, + end_exemptions: 2, + }, + // Periodic constraint with offset. + DummyConstraint { + period: 2, + offset: 1, + exemptions_period: None, + periodic_exemptions_offset: None, + end_exemptions: 0, + }, + // Periodic constraint with offset and end exemptions. + DummyConstraint { + period: 4, + offset: 3, + exemptions_period: None, + periodic_exemptions_offset: None, + end_exemptions: 1, + }, + // Periodic exemptions (bit_flags-shaped), cyclic fast path. + DummyConstraint { + period: 1, + offset: 0, + exemptions_period: Some(4), + periodic_exemptions_offset: Some(3), + end_exemptions: 0, + }, + // Periodic exemptions + end exemptions. + DummyConstraint { + period: 1, + offset: 0, + exemptions_period: Some(4), + periodic_exemptions_offset: Some(1), + end_exemptions: 2, + }, + ] + } + + #[test] + fn lde_zerofier_evaluations_match_trait_defaults() { + let domain = sample_domain(); + for (i, c) in sample_configs().iter().enumerate() { + let expected = c.zerofier_evaluations_on_extended_domain(&domain); + let got = zerofier_evaluations_on_extended_domain(&c.meta(), &domain); + assert_eq!(got, expected, "config {i} mismatch"); + } + } + + #[test] + fn ood_zerofier_evaluations_match_trait_defaults() { + let domain = sample_domain(); + let trace_length = domain.trace_roots_of_unity.len(); + let root = &domain.trace_primitive_root; + // A few arbitrary OOD points with all three extension components set. + let zs = [ + ExtE::from_raw([FpE::from(123u64), FpE::from(456u64), FpE::from(789u64)]), + ExtE::from_raw([ + FpE::from(0xDEAD_BEEFu64), + FpE::from(0xCAFE_F00Du64), + FpE::from(0x1234_5678u64), + ]), + ]; + for (i, c) in sample_configs().iter().enumerate() { + for z in &zs { + let expected = c.evaluate_zerofier(z, root, trace_length); + let got = evaluate_zerofier(&c.meta(), z, root, trace_length); + assert_eq!(got, expected, "config {i} mismatch at z={z:?}"); + } + } + } + + #[test] + fn end_exemptions_helpers_match_trait_defaults() { + let domain = sample_domain(); + let trace_length = domain.trace_roots_of_unity.len(); + let root = &domain.trace_primitive_root; + for (i, c) in sample_configs().iter().enumerate() { + assert_eq!( + end_exemptions_roots(&c.meta(), root, trace_length), + c.end_exemptions_roots(root, trace_length), + "config {i} roots mismatch" + ); + assert_eq!( + end_exemptions_lde_evaluations(&c.meta(), &domain), + c.end_exemptions_lde_evaluations(&domain), + "config {i} lde evaluations mismatch" + ); + } + } +} From e72e1b3c2f972bd1970f2563c33f3f577612184d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 1 Jul 2026 23:14:40 -0300 Subject: [PATCH 05/52] docs(gpu-constraint-eval): single-source constraints plan + constraint front-end survey --- .../impl-plan-single-source-constraints.md | 503 ++++++++++++++++++ .../survey-constraint-frontends.md | 162 ++++++ 2 files changed, 665 insertions(+) create mode 100644 thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md create mode 100644 thoughts/gpu-constraint-eval/survey-constraint-frontends.md diff --git a/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md b/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md new file mode 100644 index 000000000..041690b0a --- /dev/null +++ b/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md @@ -0,0 +1,503 @@ +# Implementation plan: single-source constraints (Phase 2.5, PRs A + B) + +**Audience: the implementing agent.** Self-contained: read this + the referenced code +and you can build it without the design discussion that produced it. All file:line +refs verified on branch `spike/constraint-ir-builder-part2` (PR #757, head of the +constraint-IR stack: #739 = Part 1, #757 = Part 2). + +**Companion docs** (context, not required reading to implement): +- `survey-constraint-frontends.md` — how Plonky3/OpenVM/SP1/risc0/zisk/airbender do this. +- `roadmap.md` — the overall GPU-constraint-eval program (this plan = its Phase 2.5). +- `plan-generic-ir-fable.md` — superseded by this file. + +--- + +## 1. Goal and non-goals + +**Goal.** Every transition constraint is defined exactly **once**, and from that +single definition we derive: (a) the compiled CPU prover evaluation, (b) the +verifier evaluation at the OOD point (identical code path in the recursion guest), +(c) the flat IR (`ConstraintProgram`) that the CPU interpreter and the future GPU +kernel consume. Today every constraint is written **twice** (`evaluate` + +`capture`); that duplication is the thing being deleted. + +**Non-goals / hard constraints:** +- The stark engine **stays generic** over `, E>`. Do not + concretize the prover/verifier to Goldilocks. +- **Do not** make the interpreter the CPU proving path. This was measured + (2026-07-01, ABBA on the bench server, ethrex 20-transfer fixture, + `spike/constraint-ir-default-on` vs `spike/constraint-ir-builder-part2`): + interpreted constraint eval costs **~9% total prove time** (pairs: −8.54%, + −9.36%). The compiled folder path is mandatory. +- No DSL, no codegen, no checked-in generated files. +- Protocol semantics are untouchable: same constraints, same zerofier structure + (per-constraint period/offset/exemptions + grouped evaluation), same transcript. + Proofs must be **bit-for-bit identical** before/after (golden-proof gate below). +- The recursion guest (verifier compiled to RISC-V) must never hash and never + interpret: its constraint evaluation is the compiled folder. Capture (which + hash-conses) must not run on the guest path — see §4.6. + +## 2. Settled decisions (do not relitigate) + +| Decision | Choice | Why (evidence) | +|---|---|---| +| Single-source mechanism | One generic body per **table**, `fn eval` | The Plonky3/SP1/OpenVM `Air` pattern (survey §1-3); object-safety handled by monomorphizing inside concrete impls, so `&dyn AIR` keeps working | +| CPU prover path | Compiled `EvalFolder` (re-run body per row) | Bench: interpreter = −9% prove time; p3+SP1 do the same | +| GPU path | Capture → flat `ConstraintProgram` → device interpreter (roadmap Phase 4) | The whole point of the program; OpenVM/zisk-validated | +| Per-constraint objects | **Deleted.** Constraints are expressions emitted by the table body; metadata is plain data (`Vec`) | Simplest model; removes `Vec>`, the adapter, `boxed()`, per-constraint structs | +| Constants in the IR | Side tables (`Op::ConstBase(u32)` → `base_consts: Vec>`) | Keeps `Op` POD `Copy+Eq+Hash` with zero bounds on F; `IsField::BaseType` has no Eq/Hash (`crypto/math/src/field/traits.rs:101`), and `FieldElement`'s derived-Hash/manual-Eq disagree on non-canonical reps (`element.rs:47`, `goldilocks.rs:411`) — inline constants would poison the CSE map's key type | +| "FieldConsts" associated consts (roadmap §2.5 step 1) | **Not needed** | Every residue-using constraint is concretely `` (`prover/src/constraints/templates.rs:81,543`, `cpu.rs:112-749`); field-generic code (lookup.rs) uses only structural u64/i64 constants that `FieldElement::::from` handles for any field | +| `degree()` | Stays **declared**, in `ConstraintMeta`; host-side test asserts declared == measured-from-IR | Measuring requires capture; capture must not run in the guest (verifier needs `composition_poly_degree_bound`, `lookup.rs:1006-1020`) | +| CSE / hashing | Only in the flatten step (existing `IrBuilder` hash-consing), host-side, once per AIR, lazily | p3 doesn't CSE at all; OpenVM only Arc-identity. Guest never flattens | +| Emission order | Explicit `constraint_idx` everywhere (`emit_*` takes idx; meta is idx-ordered) | Order/index alignment is load-bearing in every surveyed system; we keep it explicit + debug-assert completeness | + +## 3. Architecture (end state) + +``` +per table (e.g. eq.rs): + EqConstraints (small struct: nothing or col config) + ├─ fn meta(&self) -> Vec // idx-ordered metadata, plain data + └─ fn eval>(&self, b) // THE single body: emits every constraint + +framework (lookup.rs): LogUp constraints emitted the same way, generated from the + interaction config (single definitions = today's capture helpers, generalized) + +three interpretations of the same body: + ProverEvalFolder Expr = FieldElement → per LDE row, compiled (CPU prover hot path) + VerifierEvalFolder Expr = FieldElement → once at OOD point (verifier + recursion guest) + CaptureBuilder Expr = owned expr tree → once at setup, host (flatten → ConstraintProgram) + ├─ CPU interpreter (tests / GPU parity) + └─ GPU lowering (Phase 4) + +engine (unchanged shape): &dyn AIR; AirWithBuses stores the table's ConstraintSet + + Vec; zerofier machinery reads meta; one virtual call per row per table. +``` + +## 4. PR A — generic IR + +Self-contained first PR. Makes `constraint_ir` generic so `CaptureBuilder` can +target it for any field and the `unsafe` bridge dies. **Behavior identical** — +gates are bit-for-bit. + +### 4A.1 `crypto/stark/src/constraint_ir/ir.rs` +- Rename `Dim::{D1, D3}` → `Dim::{Base, Ext}`. +- Replace `Op::Const1(u64)` / `Op::Const3([u64;3])` with `Op::ConstBase(u32)` / + `Op::ConstExt(u32)` — indices into new fields on the program: + ```rust + pub struct ConstraintProgram { + pub nodes: Vec, // Op stays Copy+Eq+Hash (u32 payloads only) + pub dims: Vec, + pub base_consts: Vec>, + pub ext_consts: Vec>, + pub roots: Vec, + pub num_base: usize, + pub complete: bool, + } + ``` +- Bounds: `F: IsField, E: IsField` only. Default type params = Goldilocks tower so + existing concrete code compiles unchanged during migration. +- Verified: `Const1`/`Const3`/`const_ext` have **zero** users outside + `constraint_ir/` (including tests), so the const redesign is module-contained. + +### 4A.2 `crypto/stark/src/constraint_ir/builder.rs` +- `IrBuilder`; fields gain + `base_consts`/`ext_consts`; delete `const_cache: HashMap`. +- `const_base(v: u64)` / `const_signed(v: i64)`: `FieldElement::::from(v)` + (generic `From` exists, `element.rs:149`), then intern. +- `intern_base(fe)`: linear scan `base_consts.iter().position(|c| c == &fe)` + (PartialEq → `F::eq`, canonicalizing — exact dedup, no Hash needed; tables are + tiny and this runs once at setup), push if absent, then + `push(Op::ConstBase(idx), Dim::Base)` (the `(Op, Dim)` cse map is unchanged — + `Op` is still POD). Same for `intern_ext`. +- `const_ext` signature becomes `const_ext(v: FieldElement)`. +- Keep id-0 convention: `new()` interns zero first → node 0 = `ConstBase(0)`, + `base_consts[0] = 0`. Node ids are assigned in first-use order exactly as + today ⇒ **node counts in `prover/src/tests/constraint_ir_tests.rs` must not + change** (product_zero 4, is_bit_uncond 5, is_bit_cond 7, add_carry_0 14, + add_carry_1 21; full-table: CPU 616 nodes / EQ 142). + +### 4A.3 trait plumbing (temporary — PR B replaces it) +- `Capture` in `constraint_ir/mod.rs:43`; + `TransitionConstraintEvaluator::capture(&self, b: &mut IrBuilder)` + (`constraints/transition.rs:40`) — object-safe (F,E are trait params; precedent: + `evaluate_verifier` already takes `&TransitionEvaluationContext`). + With the default type params, the ~35 concrete `impl Capture for …` in the + prover crate compile **unchanged**. `AIR::constraint_program()` + (`traits.rs:330`) returns `ConstraintProgram`. +- lookup.rs capture helpers (`capture_multiplicity` etc., `lookup.rs:1733-1997`) + and the two LogUp `capture` overrides (`lookup.rs:2130,2336`) gain ``. + +### 4A.4 `crypto/stark/src/constraint_ir/interp.rs` + delete the bridge +- `Value { Base(FieldElement), Ext(FieldElement) }` — `Clone`, not + `Copy` (not provable for generic F); use `.clone()`; for Goldilocks these + compile to register copies. +- `eval_program` / `eval_program_verifier` / `eval_program_base` become generic + `, E: IsField>` (add `IsFFTField`/`'static`/`Send+Sync` only + if call sites force it). Const resolution reads the side tables (no more + per-row `Fp::from` re-reduction). +- **Delete `constraint_ir/bridge.rs`** (99 lines, all the module's `unsafe`). +- `constraints/evaluator.rs`: field becomes + `Option>` (line 30); the hook at + lines 110-125 loses the `ran` fallback boolean — call `eval_program` directly + when the program is `Some`. The `complete:false → None` guard at :239-243 stays. +- `verifier.rs:254-274`: call `eval_program_verifier` directly; keep the + `prog.complete` boxed fallback. + +### 4A.5 PR A gates +- `cargo test -p lambda-vm-prover constraint_ir_tests -- --nocapture` — node + counts + full-table prover/verifier diff gates bit-identical. +- `cargo test --release -p lambda-vm-prover --features stark/constraint-ir` + (incl. `test_prove_elfs_*`) and the default suite; `cargo test -p stark`. +- New test: capture+interpret over a non-Goldilocks tower (Stark252, `E = F`; + reflexive `IsSubFieldOf` impl at `traits.rs:28`) — proves the genericity. +- `grep -rn unsafe crypto/stark/src/constraint_ir/` → empty. +- `cargo fmt` + `cargo clippy` clean (required before every push). + +## 5. PR B — single-source constraints + +### 5.1 Step 0 — readability spike ✅ DONE (2026-07-01) + +**Outcome: operator style wins; the trait surface in §5.2 is PINNED.** Reference +implementation (concrete Goldilocks): branch `spike/constraint-builder-step0`, +commit `57ee832e` — `crypto/stark/src/constraints/builder.rs` + +`prover/src/tests/constraint_builder_spike.rs`. All differential gates passed +first try (EqXor / IsBit / Add-pair: ProverEvalFolder == old `evaluate::`, +VerifierEvalFolder == old `evaluate::`, capture→flatten→interpret == old +evaluate, 1000 rows each; tree-measured degree == declared). Ergonomics: clone +noise 2/1/2 per body (only for genuine reuse; Rc clone = pointer bump), **zero +`.into()`** (leaves return `Expr` directly — no `Var`/`Expr` split, dodging SP1's +noise), zero borrow-checker fights. Converted EqXor body, verbatim: + +```rust +let res = b.main(0, eq_cols::RES); +let eq = b.main(0, eq_cols::EQ); +let invert = b.main(0, eq_cols::INVERT); +let two = b.const_base(2); +b.emit_base(idx, res - (eq.clone() + invert.clone() - two * eq * invert)); +``` + +Bonus finding: emitting the Add lo/hi pair from ONE template function lets the +IrBuilder hash-consing share the whole `carry_0` subtree across the pair — 24 +nodes vs 14+21 for the old separate per-constraint captures. Per-table programs +will therefore be smaller than the sum of the Phase-0 per-constraint node counts; +**PR 2 gates compare folder-vs-interpreter values, never node counts.** + +### 5.2 Trait surface — PINNED by the spike (`crypto/stark/src/constraints/builder.rs`) + +Generic lift of the spike's concrete form (add ``, +`FieldElement`/`` for `Fp`/`Fp3`; the verifier folder's const embed needs +`F: IsSubFieldOf`): + +```rust +pub trait ExprOps: Sized + Clone + + Add + Sub + + Mul + Neg + + Add + Sub + Mul {} +// + blanket impl for any type meeting the bounds + +pub trait ExtExprOps: Sized + Clone + + Add + Sub + + Mul + Neg {} + +pub trait ConstraintBuilder { + type Expr: ExprOps; + type ExprE: ExtExprOps; + fn main(&self, offset: usize, col: usize) -> Self::Expr; + fn aux(&self, offset: usize, col: usize) -> Self::ExprE; + fn periodic(&self, idx: usize) -> Self::Expr; + fn challenge(&self, idx: usize) -> Self::ExprE; // rap_challenges[idx] + fn alpha_pow(&self, idx: usize) -> Self::ExprE; // logup_alpha_powers[idx] + fn table_offset(&self) -> Self::ExprE; // logup L/N + fn const_base(&self, v: u64) -> Self::Expr; // ONLY constant path + fn const_signed(&self, v: i64) -> Self::Expr; + fn one(&self) -> Self::Expr { self.const_base(1) } // keep these defaults + fn zero(&self) -> Self::Expr { self.const_base(0) } + fn emit_base(&mut self, constraint_idx: usize, e: Self::Expr); + fn emit_ext(&mut self, constraint_idx: usize, e: Self::ExprE); +} +``` + +Spike corrections to the original sketch (binding for PR 1b — do not deviate): +- The alias shape is `Expr: ExprOps` — cross-field ops live on the + **base** side with base always the LEFT operand (the field tower only + implements subfield∘superfield); `ExtExprOps` takes **no** type params. +- **No `From>` bound on `Expr`** — wrong for `VerifierEvalFolder` + where `Expr = FieldElement`; `const_base`/`const_signed` are the only + constant path (verifier folder: `FieldElement::::from(v).to_extension()`). +- `CaptureBuilder::finish(num_base)` returns + `(ConstraintProgram, Vec<(usize, usize)>)` — per-root (idx, measured + degree); this IS the degree-measurement API for gate §5.9.2. +- Concrete folder leaves use `*` derefs (clippy `clone_on_copy`); generic folders + use `.clone()` (no lint fires on generics). The capture tree's `Mul` needs + `#[allow(clippy::suspicious_arithmetic_impl)]` (degree = sum of operands). + +```rust +/// One table's constraints: metadata + THE single body. +pub trait ConstraintSet: Send + Sync { + fn meta(&self) -> Vec; // idx-ordered + fn eval>(&self, b: &mut B); // emits every constraint +} + +pub struct ConstraintMeta { + pub constraint_idx: usize, + pub kind: RootKind, // Base | Ext; Base entries MUST be a prefix + pub degree: usize, // declared; asserted == measured (test, §5.8) + pub period: usize, // default 1 + pub offset: usize, // default 0 + pub exemptions_period: Option, + pub periodic_exemptions_offset: Option, + pub end_exemptions: usize, // default 0 +} +``` +Invariants (debug-assert in the folders and at AIR construction): meta is dense +and idx-ordered; `kind == Base` entries form a prefix (this IS `num_base`, matching +the existing convention, `traits.rs:239-243`); `eval` emits **every** idx exactly +once (folders track a seen-bitset in debug builds). + +### 5.3 The three builder implementations (framework, written once) + +1. **`ProverEvalFolder<'a, F, E>`** — `Expr = FieldElement`, + `ExprE = FieldElement`. Constructed per row from the Prover + `TransitionEvaluationContext` (`traits.rs:73-95`) + output slices; leaves read + the frame exactly as `interp.rs:176-192` does today + (`frame.get_evaluation_step(offset).get_main_evaluation_element(0, col)`); + `emit_base` writes `base_evals[idx]`, `emit_ext` writes `ext_evals[idx]`. + All ops are plain `FieldElement` arithmetic — after inlining this is the same + machine code as today's `evaluate` bodies. **This is the CPU hot path.** +2. **`VerifierEvalFolder<'a, F, E>`** — `Expr = FieldElement` (the OOD frame is + `Frame`), `ExprE = FieldElement`; `const_base` embeds via + `FieldElement::::from(v).to_extension::()`; `emit_base` promotes and + writes `ext_evals[idx]` (mirrors `TransitionConstraintAdapter`, + `constraints/transition.rs:459`). Runs once at the OOD point. **This exact + monomorphization, compiled into the guest binary, is the recursion-guest + path — no capture, no hashing, no interpretation in-circuit.** +3. **`CaptureBuilder`** — `Expr`/`ExprE` = small owned tree + (`enum IrExpr { Leaf(...), Add(Rc, Rc), … }`, each node also + storing an eagerly-computed `degree` — leaf var 1, const 0, mul sums, add/sub + max, p3's `degree_multiple`). Operators allocate nodes — **no arena, no + RefCell, no thread-local, no hashing during capture**. `emit_*` flattens the + finished tree into the PR A `IrBuilder` (recursive walk; hash-consing + there = structural CSE, host-side) and records the root + measured degree. + Produces `ConstraintProgram` + measured degrees. + +### 5.4 Table conversion (the bulk — mechanical) + +Per table (17 production tables in `prover/src/tables/*.rs`): replace the +`*_constraints(idx_start) -> (Vec>, usize)` function with a +`XxxConstraints` struct implementing `ConstraintSet`. Recipe, using EQ +(`prover/src/tables/eq.rs:253-345`) as the model: + +```rust +pub struct EqConstraints; // holds col config only if the table needs it + +impl ConstraintSet for EqConstraints { + fn meta(&self) -> Vec { + let mut m = templates::add_pair_meta(0); // idx 0,1: b + diff = a + m.extend(templates::is_bit_meta(2, 1)); // idx 2: IS_BIT(invert) + m.push(ConstraintMeta::base(3, /*degree*/ 2)); // idx 3: res = eq XOR invert + m + } + fn eval>(&self, b: &mut B) { + templates::emit_add_pair(b, 0, vec![], AddOperand::dword(cols::B_0), + AddOperand::from_dword_hl(cols::DIFF_0), AddOperand::dword(cols::A_0)); + templates::emit_is_bit(b, 2, cols::INVERT, None); + let (res, eq, invert) = (b.main(0, cols::RES), b.main(0, cols::EQ), b.main(0, cols::INVERT)); + let two = b.const_base(2); + b.emit_base(3, res - (eq + invert - two * eq * invert)); + } +} +``` + +- **Templates become functions**: `AddConstraint`/`IsBitConstraint`/ + `ProductZeroConstraint`/the cpu.rs constraint structs + (`prover/src/constraints/{templates,cpu}.rs`) turn into `emit_*` + + `*_meta` function pairs in the same files. Their existing `capture` bodies are + the starting point for `emit_*` (they're already builder-call style); their + `evaluate` operator text is the readability reference. Delete both old bodies + and the structs' trait impls when each table converts. +- The multi-kind mega-constraints (Dvrm 11 kinds / Cpu32 8 / Shift 7 / Lt·Load·Mul 6) + convert the same way — their `compute()` loops are statically bounded and + already unrolled in the existing `capture` impls. +- Index bookkeeping: the old `idx_start` threading disappears; each table's meta + is self-contained 0..n. (LogUp indices are appended by the framework — §5.5.) + +### 5.5 LogUp (framework side, `crypto/stark/src/lookup.rs`) + +- Reduce `LookupBatchedTermConstraint` / `LookupAccumulatedConstraint` to plain + config data (a `LogUpLayout`: committed pairs, absorbed interactions, + `term_column_idx`s, `acc_column_idx`, `num_term_columns`) — this is exactly + what `AirWithBuses::new` already computes at `lookup.rs:858-880` + (`split_interactions`, absorbed slice). +- The single definitions are the **existing capture helpers** + (`capture_multiplicity`, `capture_linear_terms`, `capture_packing_fingerprint`, + `capture_fingerprint`, `lookup.rs:1733-1997`) generalized over + `B: ConstraintBuilder`, plus two `emit_logup_batched_term` / + `emit_logup_accumulated` functions transcribed from the current `capture` + overrides (`lookup.rs:2130`, `:2336` — including the 1-absorbed vs 2-absorbed + branches and the `aux(1, col)` next-row reads). +- **Delete** the `evaluate_*` twins (`evaluate_batched_term_constraint`, + `evaluate_accumulated_constraint`) and the two structs' boxed-trait impls + (`lookup.rs:2039-2196`, `:2197+`). +- Framework meta: reproduce the current structs' `period/offset/end_exemptions` + answers exactly (read them off the current impls before deleting). +- The runtime `BusValue::Linear` zero-skip optimization is already intentionally + not reproduced in capture (value-preserving; see the honesty note at + `lookup.rs:1725-1729`) — with one body this asymmetry disappears entirely; + verify the golden-proof gate still passes (it must: the skip is value-neutral). + +### 5.6 Engine rewiring + +- **`AirWithBuses`** (`lookup.rs:805-830`): gains a type param + `CS: ConstraintSet`; field `transition_constraints: Vec>` is + replaced by `constraint_set: CS`, `logup: LogUpLayout`, and + `meta: Vec` (= `cs.meta()` + framework-appended LogUp meta; + compute `num_base` from the Base-prefix). `new` (`lookup.rs:849`) takes the + `CS` value instead of the boxed vec; everything else it computes stays. +- **`AIR` trait** (`crypto/stark/src/traits.rs`): + - `transition_constraints()` (`:315-317`) — **deleted**. + - New: `fn constraints_meta(&self) -> &[ConstraintMeta]`. + - `compute_transition_prover` (`:255`) / `compute_transition` (`:224`) lose + their boxed-loop defaults and become required methods. `AirWithBuses` + implements them as one-liners into free generic helpers: + `run_transition_prover(&self.constraint_set, &self.logup, ctx, base, ext)` + (constructs `ProverEvalFolder`, runs `cs.eval` + `emit_logup_*`); same for + the verifier folder and for `constraint_program()` (capture + flatten, + **lazily, cached in a `OnceLock` — the guest never calls it**, see §5.7). + - `composition_poly_degree_bound` (`lookup.rs:1006-1020`): max over + `meta.degree` instead of `c.degree()`. +- **Zerofier machinery**: `transition_zerofier_evaluations_grouped` + (`traits.rs:343-370`) reads `ZerofierGroupKey` fields from + `constraints_meta()`; the big default methods + `zerofier_evaluations_on_extended_domain` / `evaluate_zerofier` / + `end_exemptions_*` (`constraints/transition.rs:127-337`) become free functions + of `(&ConstraintMeta, &Domain | z)` in a new `constraints/zerofier.rs` — they + only ever consumed the metadata getters (verified). Bodies move verbatim. +- **`ConstraintEvaluator`** (`constraints/evaluator.rs`): unchanged flow; the + `eval_row` hook calls `air.compute_transition_prover(&ctx, base_buf, transition_buf)` + as today (now one virtual call into the monomorphized folder run instead of 33). + The `constraint-ir` feature hook from PR A stays as the interpreter reference + path for tests/GPU parity — **off by default** (bench: −9%). +- **Delete**: `TransitionConstraintEvaluator`, `TransitionConstraintAdapter`, + `TransitionConstraint` (old signature), `Capture`, `boxed()` — + all of `constraints/transition.rs` except what moves to `zerofier.rs`. + +### 5.7 Guest-safety rule (recursion) + +The verifier path must run: AIR construction (no capture — `constraint_program` +is lazy and only the prover/GPU/tests force it) → `VerifierEvalFolder` at the OOD +point. Add a test or debug assertion that the verify path never constructs an +`IrBuilder` (e.g. feature-gate a counter, or simply grep-audit + document). +Degree is read from declared meta, so `composition_poly_degree_bound` needs no +capture. This preserves the no-HashMap-in-guest rule with zero special-casing. + +### 5.8 Examples + tests migration + +- The 13 example AIRs (`crypto/stark/src/examples/*.rs`) and + `tests/transition_tests.rs` implement `TransitionConstraintEvaluator` directly + today; each becomes a `ConstraintSet` impl (bodies are 1-3 trivial constraints) + + the three forwarding one-liners on their `AIR` impls. The + `complete: false` fallback machinery (`ConstraintProgram::complete`, + `IrBuilder::mark_unsupported`) can then be **retired** — every AIR captures. +- `prover/src/tests/constraint_ir_tests.rs`: the per-constraint Phase-0 diff + tests convert to compare `ProverEvalFolder` output vs interpreted program on + random rows (same assertion, derived from one body now). Full-table gates + unchanged in spirit: folder vs interpreter vs (during migration only) the old + boxed path. + +### 5.9 PR B gates — all must pass + +0. **Pre-flight, from the PR 1 fresh-eyes review (do these FIRST, before any + conversion):** + - `num_base` has two independent sources of truth — the interpreter routes by + `c < prog.num_base` (panics via `.as_base()` on mismatch) while the folders + route by which `emit_*` the body calls, and `CaptureBuilder::finish(num_base)` + takes it as a bare argument. Everywhere PR 2 wires these, the value MUST be + `num_base_from_meta(&meta)`, and add a test asserting it equals the captured + base-emit count (release-checked, not debug-only). + - Extend the folder↔capture differential test to cover an `aux(1, col)` + next-row read and a second alpha index — the real 1-/2-absorbed LogUp bodies + use both and the PR 1 sample body covers neither. +1. **Golden proofs** (the transcription safety net — this replaces the oracle + role the duplication accidentally provided): proofs are deterministic given + trace+params. Before starting conversion, record proof-bytes hashes for a + fixed ELF set (e.g. the `test_prove_elfs_*` inputs) on the pre-PR-B commit; + assert identical hashes after. Any slip in any constraint body changes the + composition polynomial and flips the hash. +2. **Degree assert**: for every table, measured degree (CaptureBuilder trees) == + declared `meta.degree`. +3. **Backend consistency**: folder vs interpreted `ConstraintProgram` on 1000 + random rows, every production table (extends the existing gate pattern). +4. Full suite: `cargo test --release -p lambda-vm-prover` (default) and with + `--features stark/constraint-ir`; `cargo test -p stark`. +5. **ABBA sanity** on the bench server (expect ≈ 0, possibly small win from + removing 33 virtual calls/row): + `scripts/bench_abba.sh origin/ origin/spike/constraint-ir-builder-part2 20`. +6. `cargo fmt` + `cargo clippy` before every push. No AI attribution anywhere + (commits, PR bodies) — repo rule. + +## 6. Sequencing, branch mechanics & PR packaging + +**The spike PRs (#737, #739, #757) are NOT merged and get closed** once the new +branches exist — the user wants human reviewers to see only the real design, +never the transitional scaffolding (bridge `unsafe`, `Capture`-alongside- +`evaluate` duplication, boxed adapter). Their branches stay in the remote as +provenance; close each with "superseded by the single-source constraints PRs; +code absorbed". **Work from their code, not their PRs**: develop on a branch cut +from `spike/constraint-ir-builder-part2` (it has the IR/interpreter/capture +bodies to absorb), but the PRs opened against `main` present the end state +fresh. + +Ship as **two PRs against main**, both containing only end-state code: + +- **PR 1 — framework** (≈ §4 + §5.2-5.3): `constraint_ir` module arriving + *already generic* (main never sees a concrete-Goldilocks IR or a bridge) + + `ConstraintBuilder` + `ProverEvalFolder`/`VerifierEvalFolder` + + `CaptureBuilder` + `ConstraintMeta` + zerofier free functions. Not wired into + production paths; fully exercised by its own tests (§4A.5 gates reshaped as + folder-vs-interpreter, the non-Goldilocks-tower test, spike-derived node-count + tests). Zero behavior change. +- **PR 2 — the switch** (≈ §5.4-5.9): all tables + LogUp converted, + `AirWithBuses`/`AIR`/zerofier rewired, old trait machinery deleted, golden + proofs byte-identical. Structure the commits per table group so the diff reads + as old-`evaluate`-deleted next to new-body-added in each file. + +Notes: +- The internal build order within the work branch can still follow §4 then §5 + (the PR A/PR B labels elsewhere in this doc = the work phases; PR 1/PR 2 = the + review packaging). The golden-proof baseline hashes are taken on `main` + immediately before PR 2's conversion starts. +- GPU work (roadmap Phase 4) consumes `ConstraintProgram` — stable after + PR 1 merges; it can proceed in parallel with PR 2. +- Housekeeping: close #737 now; close #739/#757 when PR 1 opens; delete the + bench branch `spike/constraint-ir-default-on` after PR 2 lands (keep it until + then for ABBA re-runs). + +## 7. What NOT to do (guardrails) + +- Do not interpret constraints in the CPU prover default path (−9%, measured). +- Do not put `FieldElement` values inside `Op` (breaks POD/CSE; §2 table). +- Do not introduce hashing, capture, or interpretation into the verifier path + (recursion guest). `VerifierEvalFolder` only. +- Do not change constraint semantics, indexing, zerofier structure, `num_base` + ordering, or anything transcript-visible — golden proofs must hold. +- Do not add a `degree`-measuring pass to the verifier; declared meta + host test. +- Do not build packed/SIMD folders, register allocation, or codec work now — + that's roadmap Phase 6, gated on GPU profiles. + +## 8. Current-code map (for orientation) + +| What | Where (verified) | +|---|---| +| IR + interpreter + builder + bridge | `crypto/stark/src/constraint_ir/{ir,interp,builder,bridge,mod}.rs` (758 lines total) | +| Boxed constraint trait + adapter + zerofier defaults | `crypto/stark/src/constraints/transition.rs` | +| Prover eval loop + IR hook | `crypto/stark/src/constraints/evaluator.rs:89-160` | +| Verifier OOD eval + IR hook | `crypto/stark/src/verifier.rs:241-274` | +| AIR trait (compute_transition*, zerofier grouping, constraint_program) | `crypto/stark/src/traits.rs:224-336,343-370` | +| AirWithBuses (the one production AIR) + LogUp constraints + capture helpers | `crypto/stark/src/lookup.rs:805+,965+,1733-2400` | +| Constraint templates + CPU constraints (evaluate/capture pairs) | `prover/src/constraints/{templates,cpu}.rs` | +| Table constraint builders (eq, lt, mul, dvrm, shift, …) | `prover/src/tables/*.rs` (e.g. `eq.rs:253-345`) | +| Existing diff-test gates | `prover/src/tests/constraint_ir_tests.rs` | +| Goldilocks residue constants | `prover/src/tables/types.rs:387-423` | +| Example AIRs (to migrate) | `crypto/stark/src/examples/*.rs` (13 files) | +| Bench harness | `scripts/bench_abba.sh` (runs on the bench server only) | diff --git a/thoughts/gpu-constraint-eval/survey-constraint-frontends.md b/thoughts/gpu-constraint-eval/survey-constraint-frontends.md new file mode 100644 index 000000000..7089ab8bd --- /dev/null +++ b/thoughts/gpu-constraint-eval/survey-constraint-frontends.md @@ -0,0 +1,162 @@ +# Survey: constraint front-ends across production STARK provers + +How six production systems **define constraints once** and derive CPU-prover eval, +verifier eval, recursion-guest eval, and the GPU form from that single definition. +Compiled 2026-07-01 from the reference clones in `others/` (agent-verified file:line +refs are into those clones). Companion to `plan-generic-ir-fable.md`, which turns +these findings into our design. + +Motivating question: lambda_vm currently hand-writes **two bodies per constraint** +(`evaluate` + `capture`). Is that ever necessary, and what should the single +source of truth look like? + +## The matrix + +| | Source of truth | CPU prover hot path | Verifier @ OOD | Recursion guest | GPU form | Dedup/CSE | +|---|---|---|---|---|---|---| +| **Plonky3** | one `Air::eval` body | re-run body per packed row (compiled folder) | re-run body, all-ext folder | — | — | none (tree = metadata only) | +| **OpenVM** | one `Air::eval` body | **interpret** captured DAG (self-documented as slower; AOT = future work) | interpret DAG | interpret DAG with circuit-var types | transpile DAG → 3-addr `u128` codec | Arc-pointer identity only | +| **SP1** | one `Air::eval` body | re-run body per packed row (compiled folder) | re-run body (folder) | re-run body, `Expr = SymbolicExt` DSL AST → staged straight-line circuit code | closed-source (moongate server) | — | +| **risc0** | Zirgen DSL (external tool) | generated straight-line C++/CUDA/Metal (old) or one shared C++ template (M3) | **interpret** compact SSA op-stream (`PolyExtStepDef`) | verifier compiled to ZKR bytecode on a micro-op VM | generated straight-line CUDA | generator's problem | +| **zisk** | PIL2 DSL | **interpret** bytecode, AVX-packed ×128 rows | interpret same bytecode, `domainSize=1`, all-ext | interpret (verify circuits are PIL airs) | interpret the **identical** bytecode in CUDA | compiler's problem | +| **airbender** | one imperative builder run | **interpret** deg-≤2 term-lists | generated straight-line Rust (checked-in, 9.5k lines) | generated straight-line (compiled) | flatten term-lists → metadata | none | + +## Per-project notes + +### Plonky3 (`others/Plonky3` — a fork; symbolic lives in `air/src/symbolic/`) +- `Air::eval(&mut AB)` is the one body (`air/src/air.rs:199`); associated types + `Expr: Algebra + Algebra`, `Var: Into + Copy` with explicit + `Add/Sub/Mul` bounds give **infix operators** (`air/src/builder.rs:12-43`). +- Folders: `ProverConstraintFolder` (`Expr = PackedVal`, SIMD, per quotient row, + `uni-stark/src/folder.rs:113`), `VerifierConstraintFolder` (`Expr = Challenge`, + once at ζ, Horner accumulate, `folder.rs:185,216`), `SymbolicAirBuilder` + (`Expr = SymbolicExpression`, once at setup, `symbolic/builder.rs:277`), + `DebugConstraintBuilder` (plain `F`, per trace row). +- Symbolic tree: `Arc` children, **no hash-consing, no CSE of any kind** — + used only for constraint count / degree (`degree_multiple` cached per node, + Mul sums, `symbolic/mod.rs:179`) / base-ext layout. Hot path never touches it. +- Selectors are builder methods (`is_first_row`/`is_transition`, `when_*` wraps a + `FilteredAirBuilder` that multiplies the condition in, `filtered.rs:60`) — no + per-constraint period/offset/exemptions metadata (ours is richer; keep ours). +- **Trap they document**: emission order is load-bearing — symbolic pass and + folder pass must agree on constraint indexing (`folder.rs:99`). + +### OpenVM (`others/openvm-stark-backend`) +- One body, run **once at keygen** by `SymbolicRapBuilder`; the captured DAG + (`SymbolicExpressionDag`, `dag.rs:51`) is stored in the proving key. Production + CPU quotient, verifier-at-OOD, and the CUDA transpiler are all **interpreters + of that DAG** — the body never runs in production again (only the debug builder + re-runs it). Their own README (`prover/cpu/quotient/README.md:13-28`) flags + interpreter overhead vs p3's compiled folders; AOT-compile is listed future work. +- Dedup = `Arc::ptr_eq` identity only (`dag.rs:140-208`); structurally identical + but separately built subtrees are NOT merged. +- Verifier folder is generic over `Var/Expr` precisely so a recursive verifier can + interpret the same DAG with circuit types (`verifier/folder.rs:33`); explicit + warning that the naive tree walk is exponential — use the linear DAG walk + (`folder.rs:127`). +- Interactions declared in-body (`push_interaction`); the framework generates the + LogUp constraints into the same constraint list (`interaction/rap.rs:28-43`) — + same architecture as our `BusInteraction` + framework constraints. +- **Gotchas to avoid**: GPU rules are re-transpiled+re-encoded on *every prove* + (`SymbolicRulesOnGpu::new` per call — cache per AIR instead); codec packs + constants as 32-bit (`as_canonical_u32`, `codec.rs:101-139`) — hard-assumes a + 31-bit field, doesn't fit Goldilocks. + +### SP1 (`others/sp1` = v6.2.1 hypercube, `others/sp1_4` = v4.2.1 FRI/quotient — mechanism identical) +- One `Air` body at the scale of hundreds of chips. CPU prover = compiled + packed folder per row-group (`sp1_4/crates/stark/src/quotient.rs:57-160`); + symbolic run happens once at chip construction for metadata only (degree is + **measured**, not declared — `chip.rs:83`). +- **The recursion answer**: `GenericVerifierConstraintFolder` + (`folder.rs:163`) instantiated with DSL types + (`Expr = SymbolicExt` — a 3-variant AST with operator overloading, + `recursion/compiler/src/ir/symbolic.rs:31`) so `chip.eval(&mut folder)` **stages + straight-line circuit code**. Zero hashing, zero interpretation in-circuit + (`recursion/circuit/src/constraints.rs:19-118`). v6 kept the exact pattern. +- **Ergonomics cost, visible at scale**: pervasive `.into()` / `.clone()` noise in + bodies (Expr isn't Copy), and the generic folder's trait bounds are enormous — + every `Add/Sub/Mul` combination spelled out per impl (`folder.rs:197-219`). +- No GPU constraint IR in public code (CUDA prover = closed gRPC server). + +### risc0 (`others/risc0` — Zirgen NOT in repo, only generated artifacts) +- Old style emits the **same constraint DAG four times** (Rust verifier bytecode + `poly_ext.rs` 923 KB + straight-line `poly_fp.cpp` 24.7k lines + `eval_check.cu` + + `.metal`); rv32im's `poly_ext.rs` is 1.05 MB, keccak's is **18.9 MB** — all + checked in; consistency rests on the external generator. M3 style collapses + witgen+eval into one Context-parametrized C++ template body. +- Verifier-side representation worth mirroring: `PolyExtStepDef` — a compact SSA + op-stream (`Const/Get/Add/Sub/Mul/AndEqz/AndCond` + taps metadata) interpreted + at the OOD point (`zkp/src/adapter.rs:156-233`). Recursion runs the verifier as + ZKR bytecode on a tiny micro-op VM (3 ops/cycle). +- Lesson: the codegen route costs an external toolchain, MB-scale generated files, + FFI boundaries, and slow iteration; the compact interpreted op-list for the + verifier is the part that aged well. + +### zisk / pil2-proofman (`others/zisk`, `others/pil2-proofman`) — our exact field (Goldilocks + cubic ext, LogUp) +- One PIL2-compiled `.bin` of expression programs; **three interpreters of the + identical artifact**: CPU (AVX2/AVX512, `NROWS_PACK=128`, + `expressions_pack.hpp:351-483`), CUDA (same `ops/args/numbers` uploaded, + same switch, shared-mem scratch, `expressions_gpu.cu:680-919`), verifier + (`domainSize=1`, all-extension, `stark_verify.hpp:310-351`). Zero codegen, + zero duplication; recursive verify circuits are themselves PIL airs. +- **Instruction encoding (production template for our device IR)**: 1 dim-signature + byte (dest/src dims ∈ {(1,1,1),(3,3,1),(3,3,3)}) + 8 `u16` args + `[arith_op, dest_pos, (type,pos,stride)×2]`, `u64` Goldilocks constant pool; + 4 arith ops (`add/sub/mul/sub_swap`). Rotations = per-operand `stride` index + into the openings table. LogUp compiles to ordinary expressions — no special + opcodes. +- Interpreter overhead is mitigated by 128-lane packing (dispatch amortized). + +### airbender (`others/airbender`) +- One imperative `Circuit`/`BasicAssembly` run authors constraints AND registers + witness resolvers in the same pass; lowered once to `CompiledCircuitArtifact` + (degree-≤2 term-lists — quadratic enforced at authoring, `constraint.rs:513`). + Four consumers derive from it: CPU prover interprets term-lists + (`prover/src/prover_stages/stage3.rs:629-683`), GPU flattener + (`stage_3_kernels.rs:102-172`), verifier **codegen** (checked-in 9,577-line + unrolled `evaluate_quotient`), witness codegen (CPU Rust + GPU `.cuh`). +- Recursion guest = the generated straight-line verifier — compiled, no hashing. +- **Caveat that argues for trait-instantiation over codegen**: the generated + verifier files are checked in and refreshed by a script (`recreate_verifiers.sh`) + — freshness depends on CI discipline, not the compiler. +- Degree-≤2 term-lists don't fit our degree-3 op-DAG (known from the roadmap). + +## Implications for lambda_vm + +1. **Nobody hand-writes two bodies.** Every system has one source of truth; our + `evaluate`+`capture` duplication is an anomaly with no precedent. It must go. +2. **The Rust-native one-body mechanism is the `Air` / builder-trait pattern** + (p3, OpenVM, SP1). The DSL/codegen alternatives (risc0, zisk, airbender's + verifier) buy the same single-source guarantee but cost external toolchains, + MB-scale checked-in artifacts, or CI-enforced freshness. For a Rust codebase, + trait instantiation gives the same guarantee compiler-enforced at every build. +3. **CPU hot path — compile it.** p3 and SP1 re-run the monomorphized body per + (packed) row; OpenVM interprets and self-documents the cost; zisk interprets + but amortizes over 128 SIMD lanes. We chase ~1% prover deltas ⇒ folder-style + compiled eval. (Packed/SIMD folders are a future opportunity our design leaves + open; our current scalar-per-row `evaluate` maps 1:1 onto a scalar folder.) +4. **Recursion guest — compile it too.** SP1 (staged DSL) and airbender (codegen) + both evaluate constraints as straight-line compiled code in-guest; only zisk + interprets. Our guest verifier is ordinary Rust compiled to RISC-V, so the + eval folder instantiated at `FieldElement` IS the compiled guest path — + zero hashing, zero interpretation, no staging machinery needed. +5. **Capture stays out of the hot path and out of the guest.** Symbolic capture + runs once at setup (keygen), host-side (p3, OpenVM, SP1 unanimously). CSE is + *not* done during capture by anyone (p3: none; OpenVM: Arc-identity only); + dedup belongs in the flatten/lowering step, where hashing is host-setup-only. +6. **GPU encoding template = zisk** (same field: 64-bit Goldilocks constants, + 3 dim-combos, stride-indexed rotations), with OpenVM's three-address register + allocation as the alternative; OpenVM's 31-bit constant packing does not fit. + Cache the lowered form per AIR (OpenVM forgets to — re-transpiles every prove). +7. **Emission order / constraint indexing is load-bearing** across every one-body + system (p3 documents it; OpenVM's layout depends on it). Our explicit + `constraint_idx` + indexed `emit` already handles this — keep it. +8. **Metadata**: p3/SP1 measure degree by running the symbolic builder instead of + declaring it (one less hand-maintained number — we can measure per-root degree + from the captured IR). Our per-constraint zerofier metadata + (period/offset/exemptions) is richer than their `is_first/last/transition` + selector trio — keep ours declared. +9. **LogUp architecture confirmed**: OpenVM/SP1 declare interactions in-body and + let the framework generate the LogUp constraints. Our declarative + `BusInteraction` + framework-generated lookup constraints is the same shape; + the LogUp constraint bodies become single builder bodies like everything else. From 6ba06f599b7fa128c04cf8767692dcfac298d61d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 1 Jul 2026 23:21:03 -0300 Subject: [PATCH 06/52] stark: pre-flight guards for the constraint switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two additions to the builder framework tests, per the review pre-flight: - num_base alignment guard (release-checked): a counting capture wrapper records which emit_* sink the sample body calls per index and asserts the base-emit set is exactly the num_base_from_meta prefix, and that every captured root's dim matches the interpreter's c < num_base routing (the .as_base() panic condition). num_base has independent sources of truth in meta, the folders and finish(num_base) — this pins them together. - next-row + multi-alpha differential: a LogUp-accumulator-shaped sample set reading aux(1, col) (next-row accumulator) and two distinct alpha powers — both used by the real 1-/2-absorbed LogUp bodies and covered by neither existing sample — checked three ways on 1000 random two-step frames (prover folder vs direct arithmetic vs interpreted capture; verifier folder vs interpreted capture). --- crypto/stark/src/constraints/builder_tests.rs | 242 +++++++++++++++++- 1 file changed, 241 insertions(+), 1 deletion(-) diff --git a/crypto/stark/src/constraints/builder_tests.rs b/crypto/stark/src/constraints/builder_tests.rs index 3860a4588..8cec6d46d 100644 --- a/crypto/stark/src/constraints/builder_tests.rs +++ b/crypto/stark/src/constraints/builder_tests.rs @@ -15,7 +15,7 @@ use math::field::element::FieldElement; use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext; use math::field::goldilocks::GoldilocksField as Fp; -use crate::constraint_ir::{eval_program, eval_program_verifier}; +use crate::constraint_ir::{Dim, eval_program, eval_program_verifier}; use crate::constraints::builder::{ CaptureBuilder, ConstraintBuilder, ConstraintMeta, ConstraintSet, ProverEvalFolder, RootKind, VerifierEvalFolder, num_base_from_meta, @@ -439,3 +439,243 @@ fn verifier_folder_missing_emit_asserts() { folder.emit_base(1, x); folder.assert_all_emitted(); } + +// ============================================================================= +// PR-2 pre-flight: num_base alignment guard (release-checked) +// ============================================================================= + +/// A capture wrapper that records which `emit_*` sink each constraint index +/// used, so the meta-derived `num_base` can be checked against the body's +/// actual base-emit count (the folders route by the sink called; the +/// interpreter routes by `c < prog.num_base` — these must agree). +struct CountingCapture { + inner: CaptureBuilder, + base_idxs: Vec, + ext_idxs: Vec, +} + +impl ConstraintBuilder for CountingCapture { + type Expr = crate::constraints::builder::IrExpr; + type ExprE = crate::constraints::builder::IrExpr; + + fn main(&self, offset: usize, col: usize) -> Self::Expr { + self.inner.main(offset, col) + } + fn aux(&self, offset: usize, col: usize) -> Self::ExprE { + self.inner.aux(offset, col) + } + fn periodic(&self, idx: usize) -> Self::Expr { + self.inner.periodic(idx) + } + fn challenge(&self, idx: usize) -> Self::ExprE { + self.inner.challenge(idx) + } + fn alpha_pow(&self, idx: usize) -> Self::ExprE { + self.inner.alpha_pow(idx) + } + fn table_offset(&self) -> Self::ExprE { + self.inner.table_offset() + } + fn const_base(&self, v: u64) -> Self::Expr { + self.inner.const_base(v) + } + fn const_signed(&self, v: i64) -> Self::Expr { + self.inner.const_signed(v) + } + fn emit_base(&mut self, constraint_idx: usize, e: Self::Expr) { + self.base_idxs.push(constraint_idx); + self.inner.emit_base(constraint_idx, e); + } + fn emit_ext(&mut self, constraint_idx: usize, e: Self::ExprE) { + self.ext_idxs.push(constraint_idx); + self.inner.emit_ext(constraint_idx, e); + } +} + +/// `num_base` has two independent sources of truth: the meta Base-prefix +/// (what the engine wires everywhere) and which `emit_*` sink the body +/// actually calls (what the folders route by; the interpreter panics via +/// `.as_base()` if `prog.num_base` disagrees with the root dims). This +/// asserts they all agree for the sample set — with plain (release-checked) +/// asserts, per plan §5.9.0. +#[test] +fn num_base_from_meta_matches_captured_base_emits() { + let meta = SampleSet.meta(); + let num_base = num_base_from_meta(&meta); + + let mut counting = CountingCapture { + inner: CaptureBuilder::new(), + base_idxs: Vec::new(), + ext_idxs: Vec::new(), + }; + SampleSet.eval(&mut counting); + let CountingCapture { + inner, + mut base_idxs, + mut ext_idxs, + } = counting; + let (prog, _degrees) = inner.finish(num_base); + + // 1. The body's base-emit count equals the meta-derived num_base, and the + // emitted indices are exactly the meta prefix / suffix. + base_idxs.sort_unstable(); + ext_idxs.sort_unstable(); + assert_eq!(base_idxs.len(), num_base); + assert_eq!(base_idxs, (0..num_base).collect::>()); + assert_eq!(ext_idxs, (num_base..meta.len()).collect::>()); + + // 2. The interpreter's routing criterion agrees: every base-prefix root is + // Dim::Base (otherwise eval_program's `.as_base()` would panic) and + // every remaining root is Dim::Ext. + assert_eq!(prog.num_base, num_base); + assert_eq!(prog.roots.len(), meta.len()); + for (c, &root) in prog.roots.iter().enumerate() { + let dim = prog.dims[root as usize]; + if c < num_base { + assert_eq!(dim, Dim::Base, "base-prefix constraint {c} has an ext root"); + } else { + assert_eq!(dim, Dim::Ext, "ext constraint {c} has a base root"); + } + } +} + +// ============================================================================= +// PR-2 pre-flight: next-row aux read + two alpha indices (LogUp shape) +// ============================================================================= + +/// LogUp-accumulator-shaped sample: the real 1-/2-absorbed LogUp bodies read +/// `aux(1, col)` (next-row accumulator) and use several alpha powers — the +/// primary sample covers neither. +struct NextRowLogUpSet; + +mod lcols { + /// A main witness column. + pub const VAL: usize = 0; + pub const NUM_MAIN: usize = 1; + /// Aux: a term column and the accumulator. + pub const TERM: usize = 0; + pub const ACC: usize = 1; + pub const NUM_AUX: usize = 2; +} + +impl ConstraintSet for NextRowLogUpSet { + fn meta(&self) -> Vec { + vec![ + ConstraintMeta::base(0, 1), + ConstraintMeta::ext(1, 1).with_end_exemptions(1), + ] + } + + fn eval>(&self, b: &mut B) { + // idx 0 (base): next-row main read — main(1, VAL) − main(0, VAL). + let cur = b.main(0, lcols::VAL); + let next = b.main(1, lcols::VAL); + b.emit_base(0, next - cur); + + // idx 1 (ext): acc' − acc − (challenge₀·α₀ + term·α₁) + L/N, + // with acc' read from the NEXT row (aux offset 1). + let acc = b.aux(0, lcols::ACC); + let acc_next = b.aux(1, lcols::ACC); + let term = b.aux(0, lcols::TERM); + let ch = b.challenge(0); + let a0 = b.alpha_pow(0); + let a1 = b.alpha_pow(1); + let off = b.table_offset(); + b.emit_ext(1, acc_next - acc - (ch * a0 + term * a1) + off); + } +} + +/// Three-way differential for [`NextRowLogUpSet`] on random two-step frames: +/// prover folder == direct arithmetic == interpreted capture, and verifier +/// folder == interpreted capture. +#[test] +fn next_row_aux_and_multi_alpha_folder_matches_capture() { + let meta = NextRowLogUpSet.meta(); + let num_base = num_base_from_meta(&meta); + let mut cb = CaptureBuilder::::new(); + NextRowLogUpSet.eval(&mut cb); + let (prog, degrees) = cb.finish(num_base); + for &(idx, measured) in °rees { + assert_eq!(measured, meta[idx].degree, "constraint {idx} degree"); + } + + let shifts = PackingShifts::::new(); + let vshifts = PackingShifts::::new(); + let mut rng = SplitMix64(0x0004_F01D_u64 ^ 0xABCD); + for trial in 0..TRIALS { + // Two frame steps with distinct main and aux rows. + let rows: Vec> = (0..2) + .map(|_| (0..lcols::NUM_MAIN).map(|_| rng.fp()).collect()) + .collect(); + let auxs: Vec> = (0..2) + .map(|_| (0..lcols::NUM_AUX).map(|_| rng.ext()).collect()) + .collect(); + let challenges = vec![rng.ext()]; + let alphas = vec![rng.ext(), rng.ext()]; + let offset = rng.ext(); + + // --- prover folder vs direct arithmetic vs interpreter --- + let steps: Vec> = (0..2) + .map(|s| TableView::new(vec![rows[s].clone()], vec![auxs[s].clone()])) + .collect(); + let frame = Frame::::new(steps); + let periodic: Vec = vec![]; + let ctx = TransitionEvaluationContext::new_prover( + &frame, + &periodic, + &challenges, + &alphas, + &offset, + &shifts, + ); + + let mut folder_base = vec![FpE::zero(); num_base]; + let mut folder_ext = vec![ExtE::zero(); meta.len()]; + let mut folder = ProverEvalFolder::new(&ctx, &mut folder_base, &mut folder_ext); + NextRowLogUpSet.eval(&mut folder); + folder.assert_all_emitted(); + + let direct_base = rows[1][lcols::VAL] - rows[0][lcols::VAL]; + let direct_ext = auxs[1][lcols::ACC] + - auxs[0][lcols::ACC] + - (challenges[0] * alphas[0] + auxs[0][lcols::TERM] * alphas[1]) + + offset; + assert_eq!(folder_base[0], direct_base, "trial {trial} base direct"); + assert_eq!(folder_ext[1], direct_ext, "trial {trial} ext direct"); + + let mut interp_base = vec![FpE::zero(); num_base]; + let mut interp_ext = vec![ExtE::zero(); meta.len()]; + eval_program(&prog, &ctx, &mut interp_base, &mut interp_ext); + assert_eq!(folder_base, interp_base, "trial {trial} base interp"); + assert_eq!(folder_ext[1], interp_ext[1], "trial {trial} ext interp"); + + // --- verifier folder vs interpreter --- + let steps_e: Vec> = (0..2) + .map(|s| { + TableView::new( + vec![rows[s].iter().map(|x| x.to_extension()).collect()], + vec![auxs[s].clone()], + ) + }) + .collect(); + let frame_e = Frame::::new(steps_e); + let periodic_e: Vec = vec![]; + let vctx = TransitionEvaluationContext::::new_verifier( + &frame_e, + &periodic_e, + &challenges, + &alphas, + &offset, + &vshifts, + ); + + let mut vfolder_ext = vec![ExtE::zero(); meta.len()]; + let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vfolder_ext); + NextRowLogUpSet.eval(&mut vfolder); + vfolder.assert_all_emitted(); + + let mut vinterp_ext = vec![ExtE::zero(); meta.len()]; + eval_program_verifier(&prog, &vctx, &mut vinterp_ext); + assert_eq!(vfolder_ext, vinterp_ext, "trial {trial} verifier interp"); + } +} From 3ab24cb75dfe007d090fac2917bf8024676e3bda Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 1 Jul 2026 23:58:47 -0300 Subject: [PATCH 07/52] prover: single-body emit twins for the template and CPU constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-destructive emit_*/​*_meta function pairs in constraints/{templates, cpu}.rs, written once against the generic ConstraintBuilder so one body serves the compiled prover folder, the verifier folder and IR capture: - templates: emit_is_bit (conditional + unconditional), emit_add_pair (the carry pair from ONE body, covering every AddOperand variant via shared operand/term helpers) - cpu: emit_product_zero, emit_arg2_exclusive, emit_mem_flags_bit, emit_reg_not_read_is_zero, emit_arg2, emit_rvd_eq_res, emit_branch_rvd_pair, emit_branch_cond, emit_next_pc_add_pair (the two pc + instruction_length carry pairs share one gated body) Each *_meta returns the idx-ordered ConstraintMeta (declared degree, default zerofier shape — none of these constraints override period/ offset/exemptions, matching the structs). The old boxed constraint structs stay untouched: they are the differential oracle for the transcription until the table conversion deletes them. --- prover/src/constraints/cpu.rs | 259 ++++++++++++++++++++++++++++ prover/src/constraints/templates.rs | 153 ++++++++++++++++ 2 files changed, 412 insertions(+) diff --git a/prover/src/constraints/cpu.rs b/prover/src/constraints/cpu.rs index facc9e16d..f269ce3f1 100644 --- a/prover/src/constraints/cpu.rs +++ b/prover/src/constraints/cpu.rs @@ -709,3 +709,262 @@ pub fn create_all_cpu_constraints() -> ( (is_bit, add_constraints, other, next_idx) } + +// ========================================================================= +// Single-body emit functions (ConstraintBuilder front-end) +// ========================================================================= +// +// Non-destructive twins of the boxed constraint structs above, written once +// against the generic `ConstraintBuilder` so one body serves the compiled +// prover folder, the verifier folder and IR capture. The structs stay for +// now (they are the differential oracle for the emit functions); the table +// conversion deletes them. +// +// All constraints here use the default zerofier shape (every row, no +// exemptions), matching the structs (none override period/offset/ +// exemptions). + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta}; + +use super::templates::INV_SHIFT_32; + +/// `col_a · col_b = 0`. Twin of [`ProductZeroConstraint`]. +pub fn emit_product_zero>( + b: &mut B, + idx: usize, + col_a: usize, + col_b: usize, +) { + let root = b.main(0, col_a) * b.main(0, col_b); + b.emit_base(idx, root); +} + +/// Metadata for [`emit_product_zero`]. +pub fn product_zero_meta(idx: usize) -> ConstraintMeta { + ConstraintMeta::base(idx, 2) +} + +/// `(1 − MEMORY − BRANCH) · read_register2 · imm[i] = 0`. Twin of +/// [`Arg2ExclusiveConstraint`]. +pub fn emit_arg2_exclusive>( + b: &mut B, + idx: usize, + imm_col: usize, +) { + let one = b.one(); + let memory = b.main(0, cols::MEMORY); + let branch = b.main(0, cols::BRANCH); + let rr2 = b.main(0, cols::READ_REGISTER2); + let imm = b.main(0, imm_col); + b.emit_base(idx, (one - memory - branch) * rr2 * imm); +} + +/// Metadata for [`emit_arg2_exclusive`]. +pub fn arg2_exclusive_meta(idx: usize) -> ConstraintMeta { + ConstraintMeta::base(idx, 3) +} + +/// `(1 − MEMORY) · mem_flags · (1 − mem_flags) = 0`. Twin of +/// [`MemFlagsBitConstraint`]. +pub fn emit_mem_flags_bit>( + b: &mut B, + idx: usize, +) { + let one = b.one(); + let memory = b.main(0, cols::MEMORY); + let mem_flags = b.main(0, cols::MEM_FLAGS); + b.emit_base( + idx, + (one.clone() - memory) * mem_flags.clone() * (one - mem_flags), + ); +} + +/// Metadata for [`emit_mem_flags_bit`]. +pub fn mem_flags_bit_meta(idx: usize) -> ConstraintMeta { + ConstraintMeta::base(idx, 3) +} + +/// `(1 − flag) · value = 0`. Twin of [`RegNotReadIsZeroConstraint`]. +pub fn emit_reg_not_read_is_zero>( + b: &mut B, + idx: usize, + flag_col: usize, + value_col: usize, +) { + let one = b.one(); + let flag = b.main(0, flag_col); + let value = b.main(0, value_col); + b.emit_base(idx, (one - flag) * value); +} + +/// Metadata for [`emit_reg_not_read_is_zero`]. +pub fn reg_not_read_is_zero_meta(idx: usize) -> ConstraintMeta { + ConstraintMeta::base(idx, 2) +} + +/// `arg2` multiplex for word index `word_idx ∈ {0, 1}`. Twin of +/// [`Arg2Constraint`]: +/// +/// ```text +/// arg2[i] − (MEMORY·imm[i] + BRANCH·rv2[i] + (1−MEMORY−BRANCH)·(rv2[i] + imm[i])) +/// ``` +pub fn emit_arg2>( + b: &mut B, + idx: usize, + word_idx: usize, +) { + let (arg2_col, imm_col, rv2_col) = if word_idx == 0 { + (cols::ARG2_0, cols::IMM_0, cols::RV2_0) + } else { + (cols::ARG2_1, cols::IMM_1, cols::RV2_1) + }; + let one = b.one(); + let arg2 = b.main(0, arg2_col); + let imm = b.main(0, imm_col); + let rv2 = b.main(0, rv2_col); + let memory = b.main(0, cols::MEMORY); + let branch = b.main(0, cols::BRANCH); + + let expected = memory.clone() * imm.clone() + + branch.clone() * rv2.clone() + + (one - memory - branch) * (rv2 + imm); + b.emit_base(idx, arg2 - expected); +} + +/// Metadata for [`emit_arg2`] (degree 2 relies on the live `MEMORY·BRANCH = 0` +/// mutex, as with the struct). +pub fn arg2_meta(idx: usize) -> ConstraintMeta { + ConstraintMeta::base(idx, 2) +} + +/// `cast(res, DWordWL)` word from the four `res` halves (DWordHL); twin of +/// [`res_word`]. +fn res_word_expr>( + b: &B, + high: bool, +) -> B::Expr { + let (lo_col, hi_col) = if high { + (cols::RES_2, cols::RES_3) + } else { + (cols::RES_0, cols::RES_1) + }; + b.main(0, lo_col) + b.main(0, hi_col) * b.const_base(SHIFT_16) +} + +/// `(1 − MEMORY − BRANCH) · (rvd[i] − cast(res, WL)[i]) = 0`. Twin of +/// [`RvdEqResConstraint`]. +pub fn emit_rvd_eq_res>( + b: &mut B, + idx: usize, + word_idx: usize, +) { + let high = word_idx == 1; + let rvd_col = if high { cols::RVD_1 } else { cols::RVD_0 }; + let one = b.one(); + let memory = b.main(0, cols::MEMORY); + let branch = b.main(0, cols::BRANCH); + let rvd = b.main(0, rvd_col); + let res_w = res_word_expr(b, high); + b.emit_base(idx, (one - memory - branch) * (rvd - res_w)); +} + +/// Metadata for [`emit_rvd_eq_res`]. +pub fn rvd_eq_res_meta(idx: usize) -> ConstraintMeta { + ConstraintMeta::base(idx, 2) +} + +/// The `pc + instruction_length` carry pair against a destination dword +/// (`rvd` or `next_pc`), gated by `gate`; shared body of +/// [`emit_branch_rvd_pair`] and [`emit_next_pc_add_pair`]: +/// +/// ```text +/// carry_0 = (pc[0] + 2·half_len − dst[0])·2⁻³² +/// carry_1 = (pc[1] + carry_0 − dst[1])·2⁻³² +/// emit: gate·carry_i·(1 − carry_i) at idx, idx+1 +/// ``` +fn emit_pc_len_add_pair>( + b: &mut B, + idx: usize, + dst_lo_col: usize, + dst_hi_col: usize, + gate: fn(&B) -> B::Expr, +) { + let inv_2_32 = b.const_base(INV_SHIFT_32); + let pc_lo = b.main(0, cols::PC_0); + let pc_hi = b.main(0, cols::PC_1); + let dst_lo = b.main(0, dst_lo_col); + let dst_hi = b.main(0, dst_hi_col); + let half_len = b.main(0, cols::HALF_INSTRUCTION_LENGTH); + let instr_len = half_len.clone() + half_len; // real byte length = 2 · half + let carry_0 = (pc_lo + instr_len - dst_lo) * inv_2_32.clone(); + let carry_1 = (pc_hi + carry_0.clone() - dst_hi) * inv_2_32; + + let one = b.one(); + let g = gate(b); + b.emit_base(idx, g * carry_0.clone() * (one - carry_0)); + let one = b.one(); + let g = gate(b); + b.emit_base(idx + 1, g * carry_1.clone() * (one - carry_1)); +} + +/// `BRANCH · carry · (1 − carry) = 0` for `rvd = pc + instruction_length` +/// (two instances at `idx`, `idx + 1`). Twin of +/// [`BranchRvdConstraint::new_pair`]. +pub fn emit_branch_rvd_pair>( + b: &mut B, + idx: usize, +) { + emit_pc_len_add_pair(b, idx, cols::RVD_0, cols::RVD_1, |b| { + b.main(0, cols::BRANCH) + }); +} + +/// Metadata for [`emit_branch_rvd_pair`]. +pub fn branch_rvd_meta(idx: usize) -> [ConstraintMeta; 2] { + [ + ConstraintMeta::base(idx, 3), + ConstraintMeta::base(idx + 1, 3), + ] +} + +/// `branch_cond − (BRANCH·JALR + BRANCH·(1−JALR)·res[0])`. Twin of +/// [`BranchCondConstraint`]. +pub fn emit_branch_cond>( + b: &mut B, + idx: usize, +) { + let one = b.one(); + let branch = b.main(0, cols::BRANCH); + let jalr = b.main(0, cols::MEM_FLAGS); + let res0 = b.main(0, cols::RES_0); + let branch_cond = b.main(0, cols::BRANCH_COND); + + let expected = branch.clone() * jalr.clone() + branch * (one - jalr) * res0; + b.emit_base(idx, branch_cond - expected); +} + +/// Metadata for [`emit_branch_cond`]. +pub fn branch_cond_meta(idx: usize) -> ConstraintMeta { + ConstraintMeta::base(idx, 3) +} + +/// `(1 − branch_cond) · carry · (1 − carry) = 0` for +/// `next_pc = pc + instruction_length` (two instances at `idx`, `idx + 1`). +/// Twin of [`NextPcAddConstraint::new_pair`]. +pub fn emit_next_pc_add_pair>( + b: &mut B, + idx: usize, +) { + emit_pc_len_add_pair(b, idx, cols::NEXT_PC_0, cols::NEXT_PC_1, |b| { + let one = b.one(); + one - b.main(0, cols::BRANCH_COND) + }); +} + +/// Metadata for [`emit_next_pc_add_pair`]. +pub fn next_pc_add_meta(idx: usize) -> [ConstraintMeta; 2] { + [ + ConstraintMeta::base(idx, 3), + ConstraintMeta::base(idx + 1, 3), + ] +} diff --git a/prover/src/constraints/templates.rs b/prover/src/constraints/templates.rs index ef5b6c036..e5adbd9b7 100644 --- a/prover/src/constraints/templates.rs +++ b/prover/src/constraints/templates.rs @@ -509,3 +509,156 @@ pub fn new_is_bit_constraints( (constraints, constraint_idx_start + value_cols.len()) } + +// ========================================================================= +// Single-body emit functions (ConstraintBuilder front-end) +// ========================================================================= +// +// Non-destructive twins of the boxed constraint structs above, written once +// against the generic `ConstraintBuilder` so one body serves the compiled +// prover folder, the verifier folder and IR capture. The structs stay for +// now (they are the differential oracle for the emit functions); the table +// conversion deletes them. +// +// Each `emit_*` takes the constraint index it emits at; the matching +// `*_meta` returns the idx-ordered metadata (declared degree; default +// zerofier shape — none of these templates override period/offset/ +// exemptions). + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta}; + +/// IS_BIT: `x·(1−x)`, optionally gated by a condition column: +/// `cond·x·(1−x)`. Twin of [`IsBitConstraint`]. +pub fn emit_is_bit>( + b: &mut B, + idx: usize, + value_col: usize, + cond_col: Option, +) { + let x = b.main(0, value_col); + let one = b.one(); + let root = match cond_col { + Some(c) => { + let cond = b.main(0, c); + cond * x.clone() * (one - x) + } + None => x.clone() * (one - x), + }; + b.emit_base(idx, root); +} + +/// Metadata for [`emit_is_bit`]: degree 3 gated, 2 ungated. +pub fn is_bit_meta(idx: usize, conditional: bool) -> ConstraintMeta { + ConstraintMeta::base(idx, if conditional { 3 } else { 2 }) +} + +/// One [`AddLinearTerm`]: `column · coefficient` or a constant +/// (matches `AddLinearTerm::eval`'s operand order). +fn add_term_expr>( + b: &B, + t: &AddLinearTerm, +) -> B::Expr { + match t { + AddLinearTerm::Column { + coefficient, + column, + } => b.main(0, *column) * b.const_signed(*coefficient), + AddLinearTerm::Constant(v) => b.const_signed(*v), + } +} + +/// Sum of terms, from zero (matches `eval_terms`). +fn add_terms_expr>( + b: &B, + terms: &[AddLinearTerm], +) -> B::Expr { + let mut acc = b.zero(); + for t in terms { + acc = acc + add_term_expr(b, t); + } + acc +} + +/// An operand's low word (matches `AddOperand::eval_lo`). +fn add_operand_lo>( + b: &B, + op: &AddOperand, +) -> B::Expr { + match op { + AddOperand::DWordWL { start_column } => b.main(0, *start_column), + AddOperand::Linear { lo, .. } => add_terms_expr(b, lo), + } +} + +/// An operand's high word (matches `AddOperand::eval_hi`). +fn add_operand_hi>( + b: &B, + op: &AddOperand, +) -> B::Expr { + match op { + AddOperand::DWordWL { start_column } => b.main(0, *start_column + 1), + AddOperand::Linear { hi, .. } => add_terms_expr(b, hi), + } +} + +/// The ADD carry pair, emitted from ONE body at `idx` and `idx + 1` +/// (twin of [`AddConstraint::new_pair`]): +/// +/// ```text +/// carry_0 = (lhs.lo + rhs.lo − sum.lo)·2⁻³² +/// carry_1 = (lhs.hi + rhs.hi + carry_0 − sum.hi)·2⁻³² +/// emit: [cond·] carry_i·(1 − carry_i) at idx, idx+1 +/// ``` +/// +/// `cond` is the sum of the `cond_cols` flags (empty = unconditional). +pub fn emit_add_pair>( + b: &mut B, + idx: usize, + cond_cols: &[usize], + lhs: &AddOperand, + rhs: &AddOperand, + sum: &AddOperand, +) { + let inv_2_32 = b.const_base(INV_SHIFT_32); + let carry_0 = (add_operand_lo(b, lhs) + add_operand_lo(b, rhs) - add_operand_lo(b, sum)) + * inv_2_32.clone(); + let carry_1 = (add_operand_hi(b, lhs) + add_operand_hi(b, rhs) + carry_0.clone() + - add_operand_hi(b, sum)) + * inv_2_32; + + let cond = |b: &B| -> Option { + if cond_cols.is_empty() { + None + } else { + let mut acc = b.zero(); + for &c in cond_cols { + acc = acc + b.main(0, c); + } + Some(acc) + } + }; + let bit = |b: &B, cond: Option, carry: B::Expr| -> B::Expr { + let one = b.one(); + match cond { + Some(c) => c * carry.clone() * (one - carry), + None => carry.clone() * (one - carry), + } + }; + + let c0 = cond(b); + let root_0 = bit(b, c0, carry_0); + b.emit_base(idx, root_0); + let c1 = cond(b); + let root_1 = bit(b, c1, carry_1); + b.emit_base(idx + 1, root_1); +} + +/// Metadata for [`emit_add_pair`]: two constraints at `idx`, `idx + 1`; +/// degree 3 gated, 2 ungated. +pub fn add_pair_meta(idx: usize, conditional: bool) -> [ConstraintMeta; 2] { + let degree = if conditional { 3 } else { 2 }; + [ + ConstraintMeta::base(idx, degree), + ConstraintMeta::base(idx + 1, degree), + ] +} From 1e64bb99e03dd18739e629daabb904cdf17b8859 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 1 Jul 2026 23:59:07 -0300 Subject: [PATCH 08/52] prover: old-vs-new random-row differentials for the emit twins The transcription gate for the constraints switch: every emit_* body is checked against the old boxed struct's evaluate on 1000 random rows (off-trace points, where a weakened or slipped transcription diverges with overwhelming probability), three ways per constraint: - ProverEvalFolder output vs old evaluate:: - VerifierEvalFolder output vs old evaluate:: - CaptureBuilder -> flatten -> interpret vs old evaluate:: plus tree-measured degree == declared meta degree == old degree(), and *_meta zerofier parameters == the old struct's period/offset/ exemptions_period/periodic_exemptions_offset/end_exemptions. Covers all eleven kinds; the ADD pair runs three configurations (conditional dword, unconditional DWordHL + negative-coefficient linear, multi-column condition with Word/Constant/DWordBL operands) so every AddOperand variant and both const_signed signs are exercised. --- prover/src/tests/constraint_emit_tests.rs | 565 ++++++++++++++++++++++ prover/src/tests/mod.rs | 2 + 2 files changed, 567 insertions(+) create mode 100644 prover/src/tests/constraint_emit_tests.rs diff --git a/prover/src/tests/constraint_emit_tests.rs b/prover/src/tests/constraint_emit_tests.rs new file mode 100644 index 000000000..db2d7aa88 --- /dev/null +++ b/prover/src/tests/constraint_emit_tests.rs @@ -0,0 +1,565 @@ +//! Differential tests for the single-body `emit_*` constraint functions. +//! +//! Each `emit_*` function in `constraints::{templates, cpu}` is checked +//! against the OLD boxed constraint struct it transcribes (the structs stay +//! in-branch as this oracle until the final deletion phase), on +//! [`TRIALS`] random rows — off-trace points, where a weakened or slipped +//! transcription diverges with overwhelming probability: +//! +//! 1. `ProverEvalFolder` output == old `evaluate::`; +//! 2. `VerifierEvalFolder` output == old `evaluate::` (embedded); +//! 3. `CaptureBuilder` → flatten → interpret == old `evaluate::`; +//! +//! plus per constraint: tree-measured degree == declared `meta.degree` == +//! old `degree()`, and `*_meta` zerofier parameters == the old struct's +//! `period`/`offset`/`exemptions_period`/`periodic_exemptions_offset`/ +//! `end_exemptions`. + +use math::field::element::FieldElement; +use stark::constraint_ir::eval_program_base; +use stark::constraints::builder::{ + CaptureBuilder, ConstraintBuilder, ConstraintMeta, ProverEvalFolder, RootKind, + VerifierEvalFolder, num_base_from_meta, +}; +use stark::constraints::transition::TransitionConstraint; +use stark::frame::Frame; +use stark::lookup::PackingShifts; +use stark::table::TableView; +use stark::traits::TransitionEvaluationContext; + +use crate::constraints::cpu::{ + Arg2Constraint, Arg2ExclusiveConstraint, BranchCondConstraint, BranchRvdConstraint, + MemFlagsBitConstraint, NextPcAddConstraint, ProductZeroConstraint, RegNotReadIsZeroConstraint, + RvdEqResConstraint, emit_arg2, emit_arg2_exclusive, emit_branch_cond, emit_branch_rvd_pair, + emit_mem_flags_bit, emit_next_pc_add_pair, emit_product_zero, emit_reg_not_read_is_zero, + emit_rvd_eq_res, +}; +use crate::constraints::cpu::{ + arg2_exclusive_meta, arg2_meta, branch_cond_meta, branch_rvd_meta, mem_flags_bit_meta, + next_pc_add_meta, product_zero_meta, reg_not_read_is_zero_meta, rvd_eq_res_meta, +}; +use crate::constraints::templates::{ + AddConstraint, AddLinearTerm, AddOperand, IsBitConstraint, add_pair_meta, emit_add_pair, + emit_is_bit, is_bit_meta, +}; +use crate::tables::cpu::cols; +use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; + +type Gl = GoldilocksField; +type Gl3 = GoldilocksExtension; +type Fp3 = FieldElement; + +const TRIALS: usize = 1000; +const NUM_COLS: usize = cols::NUM_COLUMNS; + +/// Deterministic SplitMix64. +struct SplitMix64(u64); +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } +} + +/// An emit body under test: emits `n` base constraints at indices `0..n`. +trait EmitBody { + fn n(&self) -> usize; + fn eval>(&self, b: &mut B); +} + +macro_rules! emit_body { + ($name:ident, $n:expr, |$b:ident| $body:block) => { + struct $name; + impl EmitBody for $name { + fn n(&self) -> usize { + $n + } + fn eval>(&self, $b: &mut B) { + $body + } + } + }; +} + +/// Old-struct evaluator at the prover instantiation ``. +type OldEval<'a> = &'a dyn Fn(&TableView) -> FE; +/// Old-struct evaluator at the verifier instantiation ``. +type OldEvalExt<'a> = &'a dyn Fn(&TableView) -> Fp3; + +/// Zerofier/degree parameters read off an old constraint struct. +struct OldParams { + degree: usize, + period: usize, + offset: usize, + exemptions_period: Option, + periodic_exemptions_offset: Option, + end_exemptions: usize, +} + +fn old_params>(c: &T) -> OldParams { + OldParams { + degree: c.degree(), + period: c.period(), + offset: c.offset(), + exemptions_period: c.exemptions_period(), + periodic_exemptions_offset: c.periodic_exemptions_offset(), + end_exemptions: c.end_exemptions(), + } +} + +/// The full differential check for one emit body against its old structs. +fn check_emit_vs_old( + label: &str, + body: &T, + meta: &[ConstraintMeta], + olds: &[OldEval<'_>], + olds_ext: &[OldEvalExt<'_>], + old_params: &[OldParams], +) { + let n = body.n(); + assert_eq!(meta.len(), n, "[{label}] meta length"); + assert_eq!(olds.len(), n); + assert_eq!(olds_ext.len(), n); + assert_eq!(old_params.len(), n); + + // --- meta parity vs the old structs --- + assert_eq!(num_base_from_meta(meta), n, "[{label}] all-base num_base"); + for (i, (m, p)) in meta.iter().zip(old_params.iter()).enumerate() { + assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); + assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); + assert_eq!(m.degree, p.degree, "[{label}] degree {i}"); + assert_eq!(m.period, p.period, "[{label}] period {i}"); + assert_eq!(m.offset, p.offset, "[{label}] offset {i}"); + assert_eq!( + m.exemptions_period, p.exemptions_period, + "[{label}] exemptions_period {i}" + ); + assert_eq!( + m.periodic_exemptions_offset, p.periodic_exemptions_offset, + "[{label}] periodic_exemptions_offset {i}" + ); + assert_eq!( + m.end_exemptions, p.end_exemptions, + "[{label}] end_exemptions {i}" + ); + } + + // --- capture once; tree-measured degree == declared == old --- + let mut cb = CaptureBuilder::::new(); + body.eval(&mut cb); + let (prog, degrees) = cb.finish(n); + assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); + for &(idx, measured) in °rees { + assert_eq!( + measured, meta[idx].degree, + "[{label}] constraint {idx}: tree degree {measured} != declared {}", + meta[idx].degree + ); + } + + let shifts = PackingShifts::::new(); + let vshifts = PackingShifts::::new(); + let no_periodic: Vec = vec![]; + let no_periodic_e: Vec = vec![]; + let no_ch: Vec = vec![]; + let offset_e = Fp3::zero(); + + let mut rng = SplitMix64(0x5EED_0000_0000_0000 ^ label.len() as u64); + for trial in 0..TRIALS { + let row: Vec = (0..NUM_COLS).map(|_| FE::from(rng.next_u64())).collect(); + let row_e: Vec = row.iter().map(|x| x.to_extension()).collect(); + + let step: TableView = TableView::new(vec![row.clone()], vec![Vec::new()]); + let step_e: TableView = TableView::new(vec![row_e.clone()], vec![Vec::new()]); + + // --- 1. ProverEvalFolder == old evaluate:: --- + let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); + let ctx = TransitionEvaluationContext::new_prover( + &frame, + &no_periodic, + &no_ch, + &no_ch, + &offset_e, + &shifts, + ); + let mut base_out = vec![FE::zero(); n]; + let mut ext_out = vec![Fp3::zero(); n]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); + body.eval(&mut folder); + folder.assert_all_emitted(); + for (i, old) in olds.iter().enumerate() { + assert_eq!( + base_out[i], + old(&step), + "[{label}] prover folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- 2. VerifierEvalFolder == old evaluate:: --- + let frame_e = + Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); + let vctx = TransitionEvaluationContext::::new_verifier( + &frame_e, + &no_periodic_e, + &no_ch, + &no_ch, + &offset_e, + &vshifts, + ); + let mut vext_out = vec![Fp3::zero(); n]; + let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); + body.eval(&mut vfolder); + vfolder.assert_all_emitted(); + for (i, old) in olds_ext.iter().enumerate() { + assert_eq!( + vext_out[i], + old(&step_e), + "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- 3. capture → flatten → interpret == old evaluate --- + for (i, old) in olds.iter().enumerate() { + assert_eq!( + eval_program_base(&prog, i, &row), + old(&step), + "[{label}] interpreter mismatch, constraint {i}, trial {trial}" + ); + } + } +} + +// ============================================================================= +// templates.rs: IS_BIT +// ============================================================================= + +#[test] +fn emit_is_bit_matches_old() { + emit_body!(Uncond, 1, |b| { emit_is_bit(b, 0, 7, None) }); + let old = IsBitConstraint::unconditional(7, 0); + check_emit_vs_old( + "is_bit_unconditional", + &Uncond, + &[is_bit_meta(0, false)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); + + emit_body!(Cond, 1, |b| { emit_is_bit(b, 0, 5, Some(3)) }); + let old = IsBitConstraint::new(3, 5, 0); + check_emit_vs_old( + "is_bit_conditional", + &Cond, + &[is_bit_meta(0, true)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); +} + +// ============================================================================= +// templates.rs: ADD pair +// ============================================================================= + +/// Run the pair check for one (cond, lhs, rhs, sum) configuration. +fn check_add_pair_case( + label: &str, + body: &T, + cond_cols: Vec, + lhs: AddOperand, + rhs: AddOperand, + sum: AddOperand, +) { + let conditional = !cond_cols.is_empty(); + let (old0, old1) = AddConstraint::new_pair(cond_cols, lhs, rhs, sum, 0); + check_emit_vs_old( + label, + body, + &add_pair_meta(0, conditional), + &[&|s| old0.evaluate::(s), &|s| { + old1.evaluate::(s) + }], + &[&|s| old0.evaluate::(s), &|s| { + old1.evaluate::(s) + }], + &[old_params(&old0), old_params(&old1)], + ); +} + +#[test] +fn emit_add_pair_matches_old_conditional_dword() { + emit_body!(Body, 2, |b| { + emit_add_pair( + b, + 0, + &[0], + &AddOperand::dword(1), + &AddOperand::dword(3), + &AddOperand::dword(5), + ) + }); + check_add_pair_case( + "add_pair_conditional_dword", + &Body, + vec![0], + AddOperand::dword(1), + AddOperand::dword(3), + AddOperand::dword(5), + ); +} + +#[test] +fn emit_add_pair_matches_old_linear_unconditional() { + // DWordHL repack lhs; negative-coefficient + constant linear rhs — + // exercises const_signed on both signs. + fn rhs() -> AddOperand { + AddOperand::linear( + vec![ + AddLinearTerm::Column { + coefficient: -2, + column: 2, + }, + AddLinearTerm::Constant(4), + ], + vec![], + ) + } + emit_body!(Body, 2, |b| { + emit_add_pair( + b, + 0, + &[], + &AddOperand::from_dword_hl(8), + &rhs(), + &AddOperand::dword(5), + ) + }); + check_add_pair_case( + "add_pair_linear_unconditional", + &Body, + vec![], + AddOperand::from_dword_hl(8), + rhs(), + AddOperand::dword(5), + ); +} + +#[test] +fn emit_add_pair_matches_old_multi_cond_bytes() { + // Multi-column condition (flag sum), Word + Constant operands, and a + // DWordBL byte-repacked sum — the remaining AddOperand variants. + emit_body!(Body, 2, |b| { + emit_add_pair( + b, + 0, + &[0, 2], + &AddOperand::from_word(4), + &AddOperand::constant(300), + &AddOperand::from_dword_bl(20), + ) + }); + check_add_pair_case( + "add_pair_multi_cond_bytes", + &Body, + vec![0, 2], + AddOperand::from_word(4), + AddOperand::constant(300), + AddOperand::from_dword_bl(20), + ); +} + +// ============================================================================= +// cpu.rs: decode / assumption constraints +// ============================================================================= + +#[test] +fn emit_product_zero_matches_old() { + emit_body!(Body, 1, |b| { emit_product_zero(b, 0, 12, 17) }); + let old = ProductZeroConstraint::new(12, 17, 0); + check_emit_vs_old( + "product_zero", + &Body, + &[product_zero_meta(0)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); +} + +#[test] +fn emit_arg2_exclusive_matches_old() { + for imm_col in [cols::IMM_0, cols::IMM_1] { + let old = Arg2ExclusiveConstraint::new(imm_col, 0); + // Body reads `imm_col` from the environment via two fixed cases. + if imm_col == cols::IMM_0 { + emit_body!(Body0, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_0) }); + check_emit_vs_old( + "arg2_exclusive_imm0", + &Body0, + &[arg2_exclusive_meta(0)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); + } else { + emit_body!(Body1, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_1) }); + check_emit_vs_old( + "arg2_exclusive_imm1", + &Body1, + &[arg2_exclusive_meta(0)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); + } + } +} + +#[test] +fn emit_mem_flags_bit_matches_old() { + emit_body!(Body, 1, |b| { emit_mem_flags_bit(b, 0) }); + let old = MemFlagsBitConstraint::new(0); + check_emit_vs_old( + "mem_flags_bit", + &Body, + &[mem_flags_bit_meta(0)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); +} + +#[test] +fn emit_reg_not_read_is_zero_matches_old() { + emit_body!(Body, 1, |b| { + emit_reg_not_read_is_zero(b, 0, cols::READ_REGISTER1, cols::RV1_0) + }); + let old = RegNotReadIsZeroConstraint::new(cols::READ_REGISTER1, cols::RV1_0, 0); + check_emit_vs_old( + "reg_not_read_is_zero_rv1", + &Body, + &[reg_not_read_is_zero_meta(0)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); + + emit_body!(Body2, 1, |b| { + emit_reg_not_read_is_zero(b, 0, cols::READ_REGISTER2, cols::RV2_1) + }); + let old = RegNotReadIsZeroConstraint::new(cols::READ_REGISTER2, cols::RV2_1, 0); + check_emit_vs_old( + "reg_not_read_is_zero_rv2", + &Body2, + &[reg_not_read_is_zero_meta(0)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); +} + +// ============================================================================= +// cpu.rs: alu / mem / branch groups +// ============================================================================= + +#[test] +fn emit_arg2_matches_old() { + emit_body!(Body0, 1, |b| { emit_arg2(b, 0, 0) }); + emit_body!(Body1, 1, |b| { emit_arg2(b, 0, 1) }); + let old0 = Arg2Constraint::new(0, 0); + check_emit_vs_old( + "arg2_word0", + &Body0, + &[arg2_meta(0)], + &[&|s| old0.evaluate::(s)], + &[&|s| old0.evaluate::(s)], + &[old_params(&old0)], + ); + let old1 = Arg2Constraint::new(1, 0); + check_emit_vs_old( + "arg2_word1", + &Body1, + &[arg2_meta(0)], + &[&|s| old1.evaluate::(s)], + &[&|s| old1.evaluate::(s)], + &[old_params(&old1)], + ); +} + +#[test] +fn emit_rvd_eq_res_matches_old() { + emit_body!(Body0, 1, |b| { emit_rvd_eq_res(b, 0, 0) }); + emit_body!(Body1, 1, |b| { emit_rvd_eq_res(b, 0, 1) }); + let old0 = RvdEqResConstraint::new(0, 0); + check_emit_vs_old( + "rvd_eq_res_word0", + &Body0, + &[rvd_eq_res_meta(0)], + &[&|s| old0.evaluate::(s)], + &[&|s| old0.evaluate::(s)], + &[old_params(&old0)], + ); + let old1 = RvdEqResConstraint::new(1, 0); + check_emit_vs_old( + "rvd_eq_res_word1", + &Body1, + &[rvd_eq_res_meta(0)], + &[&|s| old1.evaluate::(s)], + &[&|s| old1.evaluate::(s)], + &[old_params(&old1)], + ); +} + +#[test] +fn emit_branch_rvd_pair_matches_old() { + emit_body!(Body, 2, |b| { emit_branch_rvd_pair(b, 0) }); + let (old0, old1) = BranchRvdConstraint::new_pair(0); + check_emit_vs_old( + "branch_rvd_pair", + &Body, + &branch_rvd_meta(0), + &[&|s| old0.evaluate::(s), &|s| { + old1.evaluate::(s) + }], + &[&|s| old0.evaluate::(s), &|s| { + old1.evaluate::(s) + }], + &[old_params(&old0), old_params(&old1)], + ); +} + +#[test] +fn emit_branch_cond_matches_old() { + emit_body!(Body, 1, |b| { emit_branch_cond(b, 0) }); + let old = BranchCondConstraint::new(0); + check_emit_vs_old( + "branch_cond", + &Body, + &[branch_cond_meta(0)], + &[&|s| old.evaluate::(s)], + &[&|s| old.evaluate::(s)], + &[old_params(&old)], + ); +} + +#[test] +fn emit_next_pc_add_pair_matches_old() { + emit_body!(Body, 2, |b| { emit_next_pc_add_pair(b, 0) }); + let (old0, old1) = NextPcAddConstraint::new_pair(0); + check_emit_vs_old( + "next_pc_add_pair", + &Body, + &next_pc_add_meta(0), + &[&|s| old0.evaluate::(s), &|s| { + old1.evaluate::(s) + }], + &[&|s| old0.evaluate::(s), &|s| { + old1.evaluate::(s) + }], + &[old_params(&old0), old_params(&old1)], + ); +} diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 9e650422f..151b47b07 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -15,6 +15,8 @@ pub mod commit_tests; #[cfg(test)] pub mod compute_commit_bus_offset_tests; #[cfg(test)] +pub mod constraint_emit_tests; +#[cfg(test)] pub mod constraints_tests; #[cfg(all(test, feature = "disk-spill"))] pub mod count_table_lengths_drift_tests; From dece3d9445824581d3211f42a045a624c2ba32af Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:02:09 -0300 Subject: [PATCH 09/52] docs(gpu-constraint-eval): retire golden-proof gate (nondeterministic by design); adopt cross-version verification + random-row differentials --- .../impl-plan-single-source-constraints.md | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md b/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md index 041690b0a..95ae65f96 100644 --- a/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md +++ b/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md @@ -418,12 +418,38 @@ capture. This preserves the no-HashMap-in-guest rule with zero special-casing. - Extend the folder↔capture differential test to cover an `aux(1, col)` next-row read and a second alpha index — the real 1-/2-absorbed LogUp bodies use both and the PR 1 sample body covers neither. -1. **Golden proofs** (the transcription safety net — this replaces the oracle - role the duplication accidentally provided): proofs are deterministic given - trace+params. Before starting conversion, record proof-bytes hashes for a - fixed ELF set (e.g. the `test_prove_elfs_*` inputs) on the pre-PR-B commit; - assert identical hashes after. Any slip in any constraint body changes the - composition polynomial and flips the hash. +1. **Transcription gate = old-vs-new random-row differentials — NOT golden + proofs.** (Golden proofs were the original plan and are RETIRED: proof bytes + are nondeterministic BY DESIGN in this system — grinding, plus order-free + trace tables built via std HashMap (LT and others have no canonical row + order) — and the verifier is robust to that. Do not chase proof determinism.) + The dangerous bug class is a *weakened* constraint (new body drops a term + that vanishes on honest traces — prove→verify stays green). The differential + tests compare the OLD `evaluate` vs the NEW body on **random rows** (off-trace + points), where any such divergence shows with overwhelming probability — the + old constraint structs stay in-branch until the final deletion phase precisely + to serve as this oracle. Required coverage: every converted constraint, + ≥1000 random rows, prover folder AND capture→interpret both vs old evaluate. +1b. **Count/meta parity** (catches "constraint silently dropped from BOTH + sides", which differentials can't see): while the old code still exists + in-branch, assert per table that the new `ConstraintMeta` list matches the + old boxed list in count, `num_base`, per-constraint degree, and zerofier + params (period/offset/exemptions/end_exemptions). +1c. **Cross-version verification (the primary end-to-end gate, user-specified):** + build the `cli` at the PR-2 base (old semantics) and at the PR-2 tip; proofs + from the NEW prover MUST verify under the OLD verifier, and old proofs under + the NEW verifier (both directions, a few small ELFs each; `cli prove` / + `cli verify` — `bin/cli/src/main.rs:150,477`). Needs no determinism: the + verifier recomputes the OOD constraint evaluations from ITS OWN constraint + definitions against the other side's commitments, so any semantic difference + in the constraint system fails loudly. This catches the wiring-level slips + the per-constraint differentials can't see — constraint (re)ordering, + `num_base` split, alpha-power indexing, zerofier grouping, transcript + changes. **The invariant this gate enforces: constraint order/indices are + preserved EXACTLY 1:1 from the old system** (the β coefficients and OOD + checks are index-bound — any reordering fails verification, by design). + Implementation: a two-binary script mirroring `scripts/bench_abba.sh`'s + build-both-refs worktree pattern. 2. **Degree assert**: for every table, measured degree (CaptureBuilder trees) == declared `meta.degree`. 3. **Backend consistency**: folder vs interpreted `ConstraintProgram` on 1000 From 53b0745dea8a1f8373131f3a2c152a28b99a502a Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:08:37 -0300 Subject: [PATCH 10/52] spike(stark): convert EQ + STORE tables to single-source ConstraintSet Add EqConstraints / StoreConstraints implementing ConstraintSet, mirroring the old boxed builders index-for-index. New differential test module constraint_set_tests_b compares old evaluate_prover/evaluate_verifier and capture->interpret against the new single body on 1000 random rows, plus meta parity and measured-vs-declared degree. --- prover/src/tables/eq.rs | 47 +++- prover/src/tables/store.rs | 49 ++++- prover/src/tests/constraint_set_tests_b.rs | 241 +++++++++++++++++++++ prover/src/tests/mod.rs | 2 + 4 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 prover/src/tests/constraint_set_tests_b.rs diff --git a/prover/src/tables/eq.rs b/prover/src/tables/eq.rs index 453caa928..e6c467c48 100644 --- a/prover/src/tables/eq.rs +++ b/prover/src/tables/eq.rs @@ -28,8 +28,13 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::{AddConstraint, AddOperand, new_is_bit_constraints}; +use crate::constraints::templates::{ + AddConstraint, AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, + new_is_bit_constraints, +}; // ========================================================================= // Column indices for EQ table @@ -324,3 +329,43 @@ pub fn eq_constraints( (constraints, idx) } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// The EQ table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`eq_constraints`] index-for-index: +/// - idx 0,1: `ADD` pair `b + diff = a` (unconditional); +/// - idx 2: `IS_BIT(invert)` (unconditional); +/// - idx 3: `res = eq XOR invert`. +pub struct EqConstraints; + +impl ConstraintSet for EqConstraints { + fn meta(&self) -> Vec { + let mut m = add_pair_meta(0, false).to_vec(); // idx 0,1: b + diff = a + m.push(is_bit_meta(2, false)); // idx 2: IS_BIT(invert) + m.push(ConstraintMeta::base(3, 2)); // idx 3: res = eq XOR invert + m + } + + fn eval>(&self, b: &mut B) { + // diff = a - b, encoded as b + diff = a (unconditional). + emit_add_pair( + b, + 0, + &[], + &AddOperand::dword(cols::B_0), + &AddOperand::from_dword_hl(cols::DIFF_0), + &AddOperand::dword(cols::A_0), + ); + // IS_BIT(invert) + emit_is_bit(b, 2, cols::INVERT, None); + // res = eq XOR invert = eq + invert - 2*eq*invert + let res = b.main(0, cols::RES); + let eq = b.main(0, cols::EQ); + let invert = b.main(0, cols::INVERT); + let two = b.const_base(2); + b.emit_base(3, res - (eq.clone() + invert.clone() - two * eq * invert)); + } +} diff --git a/prover/src/tables/store.rs b/prover/src/tables/store.rs index 1cdf0334e..fef2fd223 100644 --- a/prover/src/tables/store.rs +++ b/prover/src/tables/store.rs @@ -26,8 +26,10 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; -use crate::constraints::templates::new_is_bit_constraints; +use crate::constraints::templates::{emit_is_bit, is_bit_meta, new_is_bit_constraints}; // ========================================================================= // Column indices for STORE table @@ -335,3 +337,48 @@ pub fn store_constraints( (constraints, idx) } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// The STORE table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`store_constraints`] index-for-index: +/// - idx 0-3: `IS_BIT` on `write2`, `write4`, `write8`, `μ` (unconditional); +/// - idx 4: `(Σ width)·(1 − Σ width) = 0` (width sum is a bit); +/// - idx 5: `(Σ width)·(1 − μ) = 0` (width ⇒ μ). +pub struct StoreConstraints; + +impl ConstraintSet for StoreConstraints { + fn meta(&self) -> Vec { + vec![ + is_bit_meta(0, false), // write2 + is_bit_meta(1, false), // write4 + is_bit_meta(2, false), // write8 + is_bit_meta(3, false), // μ + ConstraintMeta::base(4, 2), // width sum is bit + ConstraintMeta::base(5, 2), // width ⇒ μ + ] + } + + fn eval>(&self, b: &mut B) { + emit_is_bit(b, 0, cols::WRITE2, None); + emit_is_bit(b, 1, cols::WRITE4, None); + emit_is_bit(b, 2, cols::WRITE8, None); + emit_is_bit(b, 3, cols::MU, None); + + let w2 = b.main(0, cols::WRITE2); + let w4 = b.main(0, cols::WRITE4); + let w8 = b.main(0, cols::WRITE8); + let sum = w2 + w4 + w8; + + // width sum is bit: sum * (1 - sum) + let one = b.one(); + b.emit_base(4, sum.clone() * (one - sum.clone())); + + // width ⇒ μ: sum * (1 - μ) + let one = b.one(); + let mu = b.main(0, cols::MU); + b.emit_base(5, sum * (one - mu)); + } +} diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs new file mode 100644 index 000000000..67a2fa1b8 --- /dev/null +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -0,0 +1,241 @@ +//! Differential tests for the per-table [`ConstraintSet`] single-body +//! conversions (PR B, group B tables). +//! +//! Each converted table exposes a new `XxxConstraints: ConstraintSet` whose +//! `meta()`/`eval()` must reproduce, index-for-index, the OLD boxed builder +//! function (`eq_constraints`, `store_constraints`, …) that still lives in the +//! same file as the differential oracle. For every table we assert, on +//! [`TRIALS`] random rows (off-trace points, where a weakened or slipped +//! transcription diverges with overwhelming probability): +//! +//! 1. `ProverEvalFolder` output == old `evaluate_prover` (base field); +//! 2. `VerifierEvalFolder` output == old `evaluate_verifier` (extension); +//! 3. `CaptureBuilder` → flatten → `eval_program_base` == old `evaluate_prover`; +//! +//! plus meta parity vs the old boxed objects (count, `num_base`, and per-idx +//! degree / period / offset / exemptions_period / periodic_exemptions_offset / +//! end_exemptions), and tree-measured degree (`CaptureBuilder::finish`) == +//! declared `meta.degree`. +//! +//! All group-B tables read the current row only (offset 0) and are entirely +//! base-field, so `eval_program_base` (single `main_row`, row 0) is the +//! interpreter oracle. + +use math::field::element::FieldElement; +use stark::constraint_ir::eval_program_base; +use stark::constraints::builder::{ + CaptureBuilder, ConstraintSet, ProverEvalFolder, RootKind, VerifierEvalFolder, + num_base_from_meta, +}; +use stark::constraints::transition::TransitionConstraintEvaluator; +use stark::frame::Frame; +use stark::lookup::PackingShifts; +use stark::table::TableView; +use stark::traits::TransitionEvaluationContext; + +use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; + +type Gl = GoldilocksField; +type Gl3 = GoldilocksExtension; +type Fp3 = FieldElement; + +const TRIALS: usize = 1000; + +/// Deterministic SplitMix64. +struct SplitMix64(u64); +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } +} + +/// The OLD boxed constraint builder result, used as the differential oracle. +type OldVec = Vec>>; + +/// Run the full three-way differential + meta-parity check for one table. +/// +/// * `old` — the OLD boxed constraints built at `idx_start = 0`. +/// * `set` — the NEW [`ConstraintSet`]. +/// * `num_cols` — the table's `cols::NUM_COLUMNS`. +fn check_table>(label: &str, old: &OldVec, set: &CS, num_cols: usize) { + let n = old.len(); + let meta = set.meta(); + + // --- count / meta parity vs the old boxed objects --- + assert_eq!(meta.len(), n, "[{label}] constraint count"); + assert_eq!( + num_base_from_meta(&meta), + n, + "[{label}] all-base num_base (group-B tables are entirely base-field)" + ); + for (i, m) in meta.iter().enumerate() { + assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); + assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); + // The old boxed objects expose their idx and zerofier params directly. + let o = &old[i]; + assert_eq!(o.constraint_idx(), i, "[{label}] old idx {i} out of order"); + assert_eq!(m.degree, o.degree(), "[{label}] degree {i}"); + assert_eq!(m.period, o.period(), "[{label}] period {i}"); + assert_eq!(m.offset, o.offset(), "[{label}] offset {i}"); + assert_eq!( + m.exemptions_period, + o.exemptions_period(), + "[{label}] exemptions_period {i}" + ); + assert_eq!( + m.periodic_exemptions_offset, + o.periodic_exemptions_offset(), + "[{label}] periodic_exemptions_offset {i}" + ); + assert_eq!( + m.end_exemptions, + o.end_exemptions(), + "[{label}] end_exemptions {i}" + ); + } + + // --- capture once; tree-measured degree == declared --- + let mut cb = CaptureBuilder::::new(); + set.eval(&mut cb); + let (prog, degrees) = cb.finish(n); + assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); + for &(idx, measured) in °rees { + assert_eq!( + measured, meta[idx].degree, + "[{label}] constraint {idx}: tree degree {measured} != declared {}", + meta[idx].degree + ); + } + + let shifts = PackingShifts::::new(); + let vshifts = PackingShifts::::new(); + let no_periodic: Vec = vec![]; + let no_periodic_e: Vec = vec![]; + let no_ch: Vec = vec![]; + let offset_e = Fp3::zero(); + + let mut rng = SplitMix64(0x5EED_0000_0000_0000 ^ label.len() as u64); + for trial in 0..TRIALS { + let row: Vec = (0..num_cols).map(|_| FE::from(rng.next_u64())).collect(); + let row_e: Vec = row.iter().map(|x| x.to_extension()).collect(); + + // --- OLD oracle: prover (base) + verifier (ext) evaluations --- + let old_frame = + Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); + let old_ctx = TransitionEvaluationContext::new_prover( + &old_frame, + &no_periodic, + &no_ch, + &no_ch, + &offset_e, + &shifts, + ); + let mut old_base = vec![FE::zero(); n]; + let mut old_ext_p = vec![Fp3::zero(); n]; + for c in old.iter() { + c.evaluate_prover(&old_ctx, &mut old_base, &mut old_ext_p); + } + + let old_frame_e = + Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); + let old_vctx = TransitionEvaluationContext::::new_verifier( + &old_frame_e, + &no_periodic_e, + &no_ch, + &no_ch, + &offset_e, + &vshifts, + ); + let mut old_ext_v = vec![Fp3::zero(); n]; + for c in old.iter() { + c.evaluate_verifier(&old_vctx, &mut old_ext_v); + } + + // --- 1. ProverEvalFolder == old evaluate_prover (base) --- + let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); + let ctx = TransitionEvaluationContext::new_prover( + &frame, + &no_periodic, + &no_ch, + &no_ch, + &offset_e, + &shifts, + ); + let mut base_out = vec![FE::zero(); n]; + let mut ext_out = vec![Fp3::zero(); n]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); + set.eval(&mut folder); + folder.assert_all_emitted(); + for i in 0..n { + assert_eq!( + base_out[i], old_base[i], + "[{label}] prover folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- 2. VerifierEvalFolder == old evaluate_verifier (ext) --- + let frame_e = + Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); + let vctx = TransitionEvaluationContext::::new_verifier( + &frame_e, + &no_periodic_e, + &no_ch, + &no_ch, + &offset_e, + &vshifts, + ); + let mut vext_out = vec![Fp3::zero(); n]; + let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); + set.eval(&mut vfolder); + vfolder.assert_all_emitted(); + for i in 0..n { + assert_eq!( + vext_out[i], old_ext_v[i], + "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- 3. capture → flatten → interpret == old evaluate_prover (base) --- + for i in 0..n { + assert_eq!( + eval_program_base(&prog, i, &row), + old_base[i], + "[{label}] interpreter mismatch, constraint {i}, trial {trial}" + ); + } + } +} + +// ============================================================================= +// eq.rs +// ============================================================================= + +mod eq { + use super::*; + use crate::tables::eq::{EqConstraints, cols, eq_constraints}; + + #[test] + fn eq_constraint_set_matches_old() { + let (old, _) = eq_constraints(0); + check_table("eq", &old, &EqConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// store.rs +// ============================================================================= + +mod store { + use super::*; + use crate::tables::store::{StoreConstraints, cols, store_constraints}; + + #[test] + fn store_constraint_set_matches_old() { + let (old, _) = store_constraints(0); + check_table("store", &old, &StoreConstraints, cols::NUM_COLUMNS); + } +} diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 151b47b07..63b533bd4 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -17,6 +17,8 @@ pub mod compute_commit_bus_offset_tests; #[cfg(test)] pub mod constraint_emit_tests; #[cfg(test)] +pub mod constraint_set_tests_b; +#[cfg(test)] pub mod constraints_tests; #[cfg(all(test, feature = "disk-spill"))] pub mod count_table_lengths_drift_tests; From 454789009981a6347ff6b97bbcc73bb134834421 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:09:10 -0300 Subject: [PATCH 11/52] sscs: convert LT table to single-source ConstraintSet + differential test --- prover/src/tables/lt.rs | 108 +++++++++++ prover/src/tests/constraint_set_tests_a.rs | 213 +++++++++++++++++++++ prover/src/tests/mod.rs | 2 + 3 files changed, 323 insertions(+) create mode 100644 prover/src/tests/constraint_set_tests_a.rs diff --git a/prover/src/tables/lt.rs b/prover/src/tables/lt.rs index 02ed029bd..c8351d5b0 100644 --- a/prover/src/tables/lt.rs +++ b/prover/src/tables/lt.rs @@ -603,3 +603,111 @@ pub fn lt_constraints(constraint_idx_start: usize) -> (Vec, usize) ]; (constraints, idx) } + +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// Non-destructive twin of `LtConstraint` / `lt_constraints` above, written +// once against the generic `ConstraintBuilder` so one body serves the compiled +// prover folder, the verifier folder and IR capture. The old structs stay for +// now (they are the differential oracle); the final deletion phase removes +// them. Constraint indices 0..6 match `lt_constraints(0)` exactly. + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +/// LT table constraints as a single-source [`ConstraintSet`]. No column +/// configuration is needed (the LT layout is fixed via `cols`). +pub struct LtConstraints; + +impl LtConstraints { + /// `cast(lhs_sub_rhs, DWordWL)[0] = sub_0 + 2^16 · sub_1`. + fn carry_0>(b: &B) -> B::Expr { + let lhs_0 = b.main(0, cols::LHS_0); + let rhs_0 = b.main(0, cols::RHS_0); + let sub_0 = b.main(0, cols::LHS_SUB_RHS_0); + let sub_1 = b.main(0, cols::LHS_SUB_RHS_1); + let shift_16 = b.const_base(SHIFT_16); + let sub_lo = sub_0 + sub_1 * shift_16; + // carry[0] = (rhs[0] + sub_lo - lhs[0]) / 2^32 + let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); + (rhs_0 + sub_lo - lhs_0) * inv_2_32 + } + + /// carry[1] = (rhs_hi + sub_hi + carry_0 - lhs_hi) / 2^32. + fn carry_1>(b: &B) -> B::Expr { + let lhs_1 = b.main(0, cols::LHS_1); + let lhs_2 = b.main(0, cols::LHS_2); + let rhs_1 = b.main(0, cols::RHS_1); + let rhs_2 = b.main(0, cols::RHS_2); + let sub_2 = b.main(0, cols::LHS_SUB_RHS_2); + let sub_3 = b.main(0, cols::LHS_SUB_RHS_3); + let shift_16 = b.const_base(SHIFT_16); + // cast(lhs, DWordWL)[1] = lhs[1] + 2^16 * lhs[2] + let lhs_hi = lhs_1 + lhs_2 * shift_16.clone(); + // cast(rhs, DWordWL)[1] = rhs[1] + 2^16 * rhs[2] + let rhs_hi = rhs_1 + rhs_2 * shift_16.clone(); + // cast(lhs_sub_rhs, DWordWL)[1] = sub_2 + 2^16 * sub_3 + let sub_hi = sub_2 + sub_3 * shift_16; + let carry_0 = Self::carry_0(b); + let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); + (rhs_hi + sub_hi + carry_0 - lhs_hi) * inv_2_32 + } +} + +impl ConstraintSet for LtConstraints { + fn meta(&self) -> Vec { + vec![ + ConstraintMeta::base(0, 2), // Carry0IsBit + ConstraintMeta::base(1, 2), // Carry1IsBit + ConstraintMeta::base(2, 3), // LtFormula + ConstraintMeta::base(3, 2), // OutXorInvert + ConstraintMeta::base(4, 2), // InvertIsBit + ConstraintMeta::base(5, 2), // SignedIsBit + ] + } + + fn eval>(&self, b: &mut B) { + // idx 0: IS_BIT: carry[0] * (1 - carry[0]) + let c0 = Self::carry_0(b); + let one = b.one(); + b.emit_base(0, c0.clone() * (one - c0)); + + // idx 1: IS_BIT: carry[1] * (1 - carry[1]) + let c1 = Self::carry_1(b); + let one = b.one(); + b.emit_base(1, c1.clone() * (one - c1)); + + // idx 2: LT formula: lt - (signed*signed_lt + (1-signed)*unsigned_lt) + // signed_lt = A*(1-B) + A*C + (1-B)*C; unsigned_lt = C = carry[1] + let lt = b.main(0, cols::LT); + let signed = b.main(0, cols::SIGNED); + let a = b.main(0, cols::LHS_MSB); + let bb = b.main(0, cols::RHS_MSB); + let c = Self::carry_1(b); + let unsigned_lt = c.clone(); + let one = b.one(); + let one_minus_b = one - bb; + let signed_lt = a.clone() * one_minus_b.clone() + a * c.clone() + one_minus_b * c; + let one = b.one(); + let expected_lt = signed.clone() * signed_lt + (one - signed) * unsigned_lt; + b.emit_base(2, lt - expected_lt); + + // idx 3: out = lt XOR invert = lt + invert - 2*lt*invert + let out = b.main(0, cols::OUT); + let lt = b.main(0, cols::LT); + let invert = b.main(0, cols::INVERT); + let two = b.const_base(2); + b.emit_base(3, out - (lt.clone() + invert.clone() - two * lt * invert)); + + // idx 4: invert * (1 - invert) + let invert = b.main(0, cols::INVERT); + let one = b.one(); + b.emit_base(4, invert.clone() * (one - invert)); + + // idx 5: signed * (1 - signed) + let signed = b.main(0, cols::SIGNED); + let one = b.one(); + b.emit_base(5, signed.clone() * (one - signed)); + } +} diff --git a/prover/src/tests/constraint_set_tests_a.rs b/prover/src/tests/constraint_set_tests_a.rs new file mode 100644 index 000000000..5183a5623 --- /dev/null +++ b/prover/src/tests/constraint_set_tests_a.rs @@ -0,0 +1,213 @@ +//! Differential tests for the single-source `ConstraintSet` table conversions +//! (group A: dvrm, shift, mul, lt, load, ecsm, ecdas, ec_scalar). +//! +//! Each table's new `XxxConstraints` implementing [`ConstraintSet`] is checked +//! against the OLD boxed constraint builder it transcribes (the old structs + +//! `*_constraints` builders stay in-branch as this oracle until the final +//! deletion phase), on [`TRIALS`] random rows — off-trace points, where a +//! weakened or slipped transcription diverges with overwhelming probability: +//! +//! 1. `ProverEvalFolder` output == old `evaluate_prover`; +//! 2. `VerifierEvalFolder` output == old `evaluate_verifier`; +//! 3. `CaptureBuilder` → flatten → interpret == old `evaluate_prover`; +//! +//! plus meta parity vs the old boxed objects (count, `num_base`, and per-idx +//! degree / period / offset / exemptions_period / periodic_exemptions_offset / +//! end_exemptions), and tree-measured degree (`CaptureBuilder::finish`) == +//! declared `meta.degree`. + +use math::field::element::FieldElement; +use stark::constraint_ir::eval_program_base; +use stark::constraints::builder::{ + CaptureBuilder, ConstraintSet, ProverEvalFolder, RootKind, VerifierEvalFolder, + num_base_from_meta, +}; +use stark::constraints::transition::TransitionConstraintEvaluator; +use stark::frame::Frame; +use stark::lookup::PackingShifts; +use stark::table::TableView; +use stark::traits::TransitionEvaluationContext; + +use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; + +type Gl = GoldilocksField; +type Gl3 = GoldilocksExtension; +type Fp3 = FieldElement; + +const TRIALS: usize = 1000; + +/// Deterministic SplitMix64. +struct SplitMix64(u64); +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } +} + +/// The full differential check for one table's `ConstraintSet` against the +/// OLD boxed constraint list (built via the old builder at `idx_start = 0`). +/// +/// `num_cols` is the table's column count; frames are single-step (none of +/// these tables read next-row cells). +fn check_set_vs_old( + label: &str, + set: &CS, + old: &[Box>], + num_cols: usize, +) where + CS: ConstraintSet, +{ + let meta = set.meta(); + let n = meta.len(); + assert_eq!(old.len(), n, "[{label}] constraint count"); + + // --- meta parity vs the old boxed objects --- + let num_base = num_base_from_meta(&meta); + assert_eq!(num_base, n, "[{label}] all-base num_base"); + // Build a lookup from the old objects by constraint_idx. + let mut old_by_idx: Vec>>> = + vec![None; n]; + for c in old.iter() { + let i = c.constraint_idx(); + assert!(i < n, "[{label}] old constraint idx {i} out of range"); + assert!(old_by_idx[i].is_none(), "[{label}] duplicate old idx {i}"); + old_by_idx[i] = Some(c); + } + for (i, m) in meta.iter().enumerate() { + let c = old_by_idx[i].expect("dense old idx"); + assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); + assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); + assert_eq!(m.degree, c.degree(), "[{label}] degree {i}"); + assert_eq!(m.period, c.period(), "[{label}] period {i}"); + assert_eq!(m.offset, c.offset(), "[{label}] offset {i}"); + assert_eq!( + m.exemptions_period, + c.exemptions_period(), + "[{label}] exemptions_period {i}" + ); + assert_eq!( + m.periodic_exemptions_offset, + c.periodic_exemptions_offset(), + "[{label}] periodic_exemptions_offset {i}" + ); + assert_eq!( + m.end_exemptions, + c.end_exemptions(), + "[{label}] end_exemptions {i}" + ); + } + + // --- capture once; tree-measured degree == declared --- + let mut cb = CaptureBuilder::::new(); + set.eval(&mut cb); + let (prog, degrees) = cb.finish(num_base); + assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); + for &(idx, measured) in °rees { + assert_eq!( + measured, meta[idx].degree, + "[{label}] constraint {idx}: tree degree {measured} != declared {}", + meta[idx].degree + ); + } + + let shifts = PackingShifts::::new(); + let vshifts = PackingShifts::::new(); + let no_periodic: Vec = vec![]; + let no_periodic_e: Vec = vec![]; + let no_ch: Vec = vec![]; + let offset_e = Fp3::zero(); + + let mut rng = SplitMix64(0x5EED_0000_0000_0000 ^ label.len() as u64); + for trial in 0..TRIALS { + let row: Vec = (0..num_cols).map(|_| FE::from(rng.next_u64())).collect(); + let row_e: Vec = row.iter().map(|x| x.to_extension()).collect(); + + // --- old prover-side reference: evaluate_prover into base_evals --- + let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); + let ctx = TransitionEvaluationContext::new_prover( + &frame, + &no_periodic, + &no_ch, + &no_ch, + &offset_e, + &shifts, + ); + let mut old_base = vec![FE::zero(); n]; + let mut old_ext_scratch = vec![Fp3::zero(); n]; + for c in old.iter() { + c.evaluate_prover(&ctx, &mut old_base, &mut old_ext_scratch); + } + + // --- old verifier-side reference: evaluate_verifier into ext_evals --- + let frame_e = + Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); + let vctx = TransitionEvaluationContext::::new_verifier( + &frame_e, + &no_periodic_e, + &no_ch, + &no_ch, + &offset_e, + &vshifts, + ); + let mut old_vext = vec![Fp3::zero(); n]; + for c in old.iter() { + c.evaluate_verifier(&vctx, &mut old_vext); + } + + // --- 1. ProverEvalFolder == old evaluate_prover --- + let mut base_out = vec![FE::zero(); n]; + let mut ext_out = vec![Fp3::zero(); n]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); + set.eval(&mut folder); + folder.assert_all_emitted(); + for i in 0..n { + assert_eq!( + base_out[i], old_base[i], + "[{label}] prover folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- 2. VerifierEvalFolder == old evaluate_verifier --- + let mut vext_out = vec![Fp3::zero(); n]; + let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); + set.eval(&mut vfolder); + vfolder.assert_all_emitted(); + for i in 0..n { + assert_eq!( + vext_out[i], old_vext[i], + "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- 3. capture → flatten → interpret == old evaluate_prover --- + for i in 0..n { + assert_eq!( + eval_program_base(&prog, i, &row), + old_base[i], + "[{label}] interpreter mismatch, constraint {i}, trial {trial}" + ); + } + } +} + +// ============================================================================= +// lt.rs +// ============================================================================= + +mod lt { + use super::*; + use crate::tables::lt::{LtConstraints, cols, lt_constraints}; + use stark::constraints::transition::TransitionConstraint; + + #[test] + fn lt_constraint_set_matches_old() { + let (old, next) = lt_constraints(0); + assert_eq!(next, old.len()); + let boxed: Vec<_> = old.into_iter().map(|c| c.boxed()).collect(); + check_set_vs_old("lt", &LtConstraints, &boxed, cols::NUM_COLUMNS); + } +} diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 151b47b07..39df0f9f8 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -17,6 +17,8 @@ pub mod compute_commit_bus_offset_tests; #[cfg(test)] pub mod constraint_emit_tests; #[cfg(test)] +pub mod constraint_set_tests_a; +#[cfg(test)] pub mod constraints_tests; #[cfg(all(test, feature = "disk-spill"))] pub mod count_table_lengths_drift_tests; From 5612427f3778c86633a2cde2f8b314930ac9dcb9 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:10:50 -0300 Subject: [PATCH 12/52] spike(stark): convert MEMW / MEMW_A / MEMW_R tables to single-source ConstraintSet Add MemwConstraints (15), MemwAlignedConstraints (8), MemwRegisterConstraints (3) mirroring the old boxed builders index-for-index; extend constraint_set_tests_b with 1000-row differential + meta-parity checks. --- prover/src/tables/memw.rs | 75 +++++++++++++++++++++- prover/src/tables/memw_aligned.rs | 61 +++++++++++++++++- prover/src/tables/memw_register.rs | 30 +++++++++ prover/src/tests/constraint_set_tests_b.rs | 55 ++++++++++++++++ 4 files changed, 219 insertions(+), 2 deletions(-) diff --git a/prover/src/tables/memw.rs b/prover/src/tables/memw.rs index 2b240747c..96e458e6c 100644 --- a/prover/src/tables/memw.rs +++ b/prover/src/tables/memw.rs @@ -36,8 +36,10 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::IsBitConstraint; +use crate::constraints::templates::{IsBitConstraint, emit_is_bit}; /// Maximum number of rows per MEMW table chunk. /// If operations exceed this, the trace is split into multiple tables. @@ -988,3 +990,74 @@ pub fn constraints() constraints } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// `μ_sum = μ_read + μ_write` as a builder expression (twin of +/// [`compute_mu_sum`]). +fn mu_sum_expr>(b: &B) -> B::Expr { + b.main(0, cols::MU_READ) + b.main(0, cols::MU_WRITE) +} + +/// `w2 = write2 + write4 + write8` as a builder expression (twin of +/// [`compute_w2`]). +fn w2_expr>(b: &B) -> B::Expr { + b.main(0, cols::WRITE2) + b.main(0, cols::WRITE4) + b.main(0, cols::WRITE8) +} + +/// The MEMW table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`constraints`] index-for-index (15 constraints): +/// - idx 0: `IS_BIT<μ_sum>`; +/// - idx 1: `w2 ⇒ μ_sum` (`w2·(1 − μ_sum)`); +/// - idx 2,3: `IS_BIT` on `μ_read`, `μ_write`; +/// - idx 4-10: `IS_BIT` on `carry[0..6]`; +/// - idx 11-13: `IS_BIT` on `write2`, `write4`, `write8`; +/// - idx 14: `IS_BIT` (width sum is a bit). +pub struct MemwConstraints; + +impl ConstraintSet for MemwConstraints { + fn meta(&self) -> Vec { + let mut m = Vec::with_capacity(15); + for i in 0..15 { + m.push(ConstraintMeta::base(i, 2)); + } + m + } + + fn eval>(&self, b: &mut B) { + // idx 0: IS_BIT<μ_sum> = μ_sum * (1 - μ_sum) + let one = b.one(); + let mu_sum = mu_sum_expr(b); + b.emit_base(0, mu_sum.clone() * (one - mu_sum)); + + // idx 1: w2 ⇒ μ_sum = w2 * (1 - μ_sum) + let one = b.one(); + let w2 = w2_expr(b); + let mu_sum = mu_sum_expr(b); + b.emit_base(1, w2 * (one - mu_sum)); + + // idx 2,3: IS_BIT<μ_read>, IS_BIT<μ_write> + emit_is_bit(b, 2, cols::MU_READ, None); + emit_is_bit(b, 3, cols::MU_WRITE, None); + + // idx 4-10: IS_BIT for carry[0..6] + let mut idx = 4; + for &col in &cols::CARRY { + emit_is_bit(b, idx, col, None); + idx += 1; + } + + // idx 11-13: IS_BIT on the width flags + for &col in &[cols::WRITE2, cols::WRITE4, cols::WRITE8] { + emit_is_bit(b, idx, col, None); + idx += 1; + } + + // idx 14: IS_BIT = w2 * (1 - w2) + let one = b.one(); + let w2 = w2_expr(b); + b.emit_base(idx, w2.clone() * (one - w2)); + } +} diff --git a/prover/src/tables/memw_aligned.rs b/prover/src/tables/memw_aligned.rs index 8042d9052..801deb353 100644 --- a/prover/src/tables/memw_aligned.rs +++ b/prover/src/tables/memw_aligned.rs @@ -41,9 +41,11 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + use super::memw::MemwOperation; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::IsBitConstraint; +use crate::constraints::templates::{IsBitConstraint, emit_is_bit}; /// Maximum number of rows per MEMW_A table chunk. pub const MAX_ROWS: usize = super::max_rows::MEMW_A; @@ -738,3 +740,60 @@ pub fn constraints() MemwAlignedConstraint::new(MemwAlignedConstraintKind::WidthSumIsBit, 7).boxed(), ] } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// `μ_sum = μ_read + μ_write` as a builder expression (twin of the inlined +/// `compute`). +fn mu_sum_expr>(b: &B) -> B::Expr { + b.main(0, cols::MU_READ) + b.main(0, cols::MU_WRITE) +} + +/// `w2 = write2 + write4 + write8` as a builder expression. +fn w2_expr>(b: &B) -> B::Expr { + b.main(0, cols::WRITE2) + b.main(0, cols::WRITE4) + b.main(0, cols::WRITE8) +} + +/// The MEMW_A table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`constraints`] index-for-index (8 constraints): +/// - idx 0: `IS_BIT<μ_sum>`; +/// - idx 1: `w2 ⇒ μ_sum` (`w2·(1 − μ_sum)`); +/// - idx 2,3: `IS_BIT` on `μ_read`, `μ_write`; +/// - idx 4-6: `IS_BIT` on `write2`, `write4`, `write8`; +/// - idx 7: `IS_BIT` (width sum is a bit). +pub struct MemwAlignedConstraints; + +impl ConstraintSet for MemwAlignedConstraints { + fn meta(&self) -> Vec { + (0..8).map(|i| ConstraintMeta::base(i, 2)).collect() + } + + fn eval>(&self, b: &mut B) { + // idx 0: IS_BIT<μ_sum> = μ_sum * (1 - μ_sum) + let one = b.one(); + let mu_sum = mu_sum_expr(b); + b.emit_base(0, mu_sum.clone() * (one - mu_sum)); + + // idx 1: w2 ⇒ μ_sum = w2 * (1 - μ_sum) + let one = b.one(); + let w2 = w2_expr(b); + let mu_sum = mu_sum_expr(b); + b.emit_base(1, w2 * (one - mu_sum)); + + // idx 2,3: IS_BIT<μ_read>, IS_BIT<μ_write> + emit_is_bit(b, 2, cols::MU_READ, None); + emit_is_bit(b, 3, cols::MU_WRITE, None); + + // idx 4-6: IS_BIT on the width flags + emit_is_bit(b, 4, cols::WRITE2, None); + emit_is_bit(b, 5, cols::WRITE4, None); + emit_is_bit(b, 6, cols::WRITE8, None); + + // idx 7: IS_BIT = w2 * (1 - w2) + let one = b.one(); + let w2 = w2_expr(b); + b.emit_base(7, w2.clone() * (one - w2)); + } +} diff --git a/prover/src/tables/memw_register.rs b/prover/src/tables/memw_register.rs index 14a696cb9..59ba6d2be 100644 --- a/prover/src/tables/memw_register.rs +++ b/prover/src/tables/memw_register.rs @@ -45,8 +45,11 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + use super::memw::MemwOperation; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; +use crate::constraints::templates::emit_is_bit; // ========================================================================= // Column indices (10 columns) @@ -418,3 +421,30 @@ pub fn constraints() MemwRegisterMuSumIsBit::new(2).boxed(), ] } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// The MEMW_R table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`constraints`] index-for-index (3 constraints): +/// - idx 0,1: `IS_BIT` on `μ_read`, `μ_write`; +/// - idx 2: `IS_BIT<μ_sum>` with `μ_sum = μ_read + μ_write`. +pub struct MemwRegisterConstraints; + +impl ConstraintSet for MemwRegisterConstraints { + fn meta(&self) -> Vec { + (0..3).map(|i| ConstraintMeta::base(i, 2)).collect() + } + + fn eval>(&self, b: &mut B) { + // idx 0,1: IS_BIT<μ_read>, IS_BIT<μ_write> + emit_is_bit(b, 0, cols::MU_READ, None); + emit_is_bit(b, 1, cols::MU_WRITE, None); + + // idx 2: IS_BIT<μ_sum> = μ_sum * (1 - μ_sum), μ_sum = μ_read + μ_write + let one = b.one(); + let mu_sum = b.main(0, cols::MU_READ) + b.main(0, cols::MU_WRITE); + b.emit_base(2, mu_sum.clone() * (one - mu_sum)); + } +} diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index 67a2fa1b8..eb519abe4 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -239,3 +239,58 @@ mod store { check_table("store", &old, &StoreConstraints, cols::NUM_COLUMNS); } } + +// ============================================================================= +// memw.rs +// ============================================================================= + +mod memw { + use super::*; + use crate::tables::memw::{MemwConstraints, cols, constraints}; + + #[test] + fn memw_constraint_set_matches_old() { + let old = constraints(); + check_table("memw", &old, &MemwConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// memw_aligned.rs +// ============================================================================= + +mod memw_aligned { + use super::*; + use crate::tables::memw_aligned::{MemwAlignedConstraints, cols, constraints}; + + #[test] + fn memw_aligned_constraint_set_matches_old() { + let old = constraints(); + check_table( + "memw_aligned", + &old, + &MemwAlignedConstraints, + cols::NUM_COLUMNS, + ); + } +} + +// ============================================================================= +// memw_register.rs +// ============================================================================= + +mod memw_register { + use super::*; + use crate::tables::memw_register::{MemwRegisterConstraints, cols, constraints}; + + #[test] + fn memw_register_constraint_set_matches_old() { + let old = constraints(); + check_table( + "memw_register", + &old, + &MemwRegisterConstraints, + cols::NUM_COLUMNS, + ); + } +} From 178d551a3d3fc9eb3f5347459865d072f7098298 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:13:37 -0300 Subject: [PATCH 13/52] spike(stark): convert BRANCH + COMMIT tables to single-source ConstraintSet Add BranchConstraints (5, carry-bit next-pc pairs + JALR is-bit) and CommitConstraints (8, is-bit + first/end=>mu + two ADD carry pairs) mirroring the old boxed builders index-for-index; extend constraint_set_tests_b with 1000-row differential + meta-parity checks. --- prover/src/tables/branch.rs | 102 +++++++++++++++++++++ prover/src/tables/commit.rs | 66 ++++++++++++- prover/src/tests/constraint_set_tests_b.rs | 34 +++++++ 3 files changed, 201 insertions(+), 1 deletion(-) diff --git a/prover/src/tables/branch.rs b/prover/src/tables/branch.rs index 9443a81a1..bffafaa87 100644 --- a/prover/src/tables/branch.rs +++ b/prover/src/tables/branch.rs @@ -28,6 +28,7 @@ use math::field::element::FieldElement; use math::field::traits::{IsField, IsSubFieldOf}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use stark::constraints::transition::TransitionConstraint; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::table::TableView; @@ -565,6 +566,107 @@ pub fn branch_constraints(constraint_idx_start: usize) -> (Vec (constraints, idx) } +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// `(unmasked_0, unmasked_1)` — the next-pc value repacked into two words, +/// as builder expressions (twin of [`BranchConstraint::compute_next_pc_unmasked`]): +/// +/// ```text +/// unmasked_0 = unmasked_low_byte + next_pc_low_1·2⁸ + next_pc_high_0·2¹⁶ +/// unmasked_1 = next_pc_high_1 + next_pc_high_2·2¹⁶ +/// ``` +fn next_pc_unmasked_expr>( + b: &B, +) -> (B::Expr, B::Expr) { + let shift_8 = b.const_base(SHIFT_8); + let shift_16 = b.const_base(SHIFT_16); + let unmasked_0 = b.main(0, cols::UNMASKED_LOW_BYTE) + + b.main(0, cols::NEXT_PC_LOW_1) * shift_8 + + b.main(0, cols::NEXT_PC_HIGH_0) * shift_16.clone(); + let unmasked_1 = b.main(0, cols::NEXT_PC_HIGH_1) + b.main(0, cols::NEXT_PC_HIGH_2) * shift_16; + (unmasked_0, unmasked_1) +} + +/// `carry_0 = (base_0 + offset_0 − unmasked_0)·2⁻³²` (twin of +/// [`BranchConstraint::compute_carry_0_for`]). +fn carry_0_expr>( + b: &B, + base_col_0: usize, +) -> B::Expr { + let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); + let (unmasked_0, _) = next_pc_unmasked_expr(b); + (b.main(0, base_col_0) + b.main(0, cols::OFFSET_0) - unmasked_0) * inv_2_32 +} + +/// `carry_1 = (base_1 + offset_1 + carry_0 − unmasked_1)·2⁻³²` (twin of +/// [`BranchConstraint::compute_carry_1_for`]). +fn carry_1_expr>( + b: &B, + base_col_0: usize, + base_col_1: usize, +) -> B::Expr { + let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); + let carry_0 = carry_0_expr(b, base_col_0); + let (_, unmasked_1) = next_pc_unmasked_expr(b); + (b.main(0, base_col_1) + b.main(0, cols::OFFSET_1) + carry_0 - unmasked_1) * inv_2_32 +} + +/// The BRANCH table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`branch_constraints`] index-for-index (5 constraints): +/// - idx 0: `(1 − JALR)·carry_0·(1 − carry_0)` on the pc path (degree 3); +/// - idx 1: `(1 − JALR)·carry_1·(1 − carry_1)` on the pc path (degree 3); +/// - idx 2: `JALR·carry_0·(1 − carry_0)` on the register path (degree 3); +/// - idx 3: `JALR·carry_1·(1 − carry_1)` on the register path (degree 3); +/// - idx 4: `JALR·(1 − JALR)` (degree 2). +pub struct BranchConstraints; + +impl ConstraintSet for BranchConstraints { + fn meta(&self) -> Vec { + vec![ + ConstraintMeta::base(0, 3), // PcCarry0IsBit + ConstraintMeta::base(1, 3), // PcCarry1IsBit + ConstraintMeta::base(2, 3), // RegCarry0IsBit + ConstraintMeta::base(3, 3), // RegCarry1IsBit + ConstraintMeta::base(4, 2), // JalrIsBit + ] + } + + fn eval>(&self, b: &mut B) { + // idx 0: (1 - JALR) * carry_0(pc) * (1 - carry_0) + let one = b.one(); + let cond = one - b.main(0, cols::JALR); + let c = carry_0_expr(b, cols::PC_0); + let one = b.one(); + b.emit_base(0, cond * c.clone() * (one - c)); + + // idx 1: (1 - JALR) * carry_1(pc) * (1 - carry_1) + let one = b.one(); + let cond = one - b.main(0, cols::JALR); + let c = carry_1_expr(b, cols::PC_0, cols::PC_1); + let one = b.one(); + b.emit_base(1, cond * c.clone() * (one - c)); + + // idx 2: JALR * carry_0(register) * (1 - carry_0) + let cond = b.main(0, cols::JALR); + let c = carry_0_expr(b, cols::REGISTER_0); + let one = b.one(); + b.emit_base(2, cond * c.clone() * (one - c)); + + // idx 3: JALR * carry_1(register) * (1 - carry_1) + let cond = b.main(0, cols::JALR); + let c = carry_1_expr(b, cols::REGISTER_0, cols::REGISTER_1); + let one = b.one(); + b.emit_base(3, cond * c.clone() * (one - c)); + + // idx 4: JALR * (1 - JALR) + let one = b.one(); + let jalr = b.main(0, cols::JALR); + b.emit_base(4, jalr.clone() * (one - jalr)); + } +} + // ========================================================================= // Helper functions for computing carries (used by trace generator and tests) // ========================================================================= diff --git a/prover/src/tables/commit.rs b/prover/src/tables/commit.rs index c1663711e..2da9a8c45 100644 --- a/prover/src/tables/commit.rs +++ b/prover/src/tables/commit.rs @@ -50,7 +50,11 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; -use crate::constraints::templates::{AddConstraint, AddOperand}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +use crate::constraints::templates::{ + AddConstraint, AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, +}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; @@ -851,3 +855,63 @@ impl TransitionConstraint for CommitConstr self.compute(step) } } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// The COMMIT table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`create_constraints`] index-for-index (8 constraints): +/// - idx 0-2: `IS_BIT` on `first`, `end`, `μ`; +/// - idx 3: `(first + end)·(1 − μ) = 0` (first/end ⇒ μ); +/// - idx 4,5: `ADD` pair `address + 1 = address_incr` (unconditional); +/// - idx 6,7: `ADD` pair `count_decr + 1 = count` (unconditional). +pub struct CommitConstraints; + +impl ConstraintSet for CommitConstraints { + fn meta(&self) -> Vec { + let mut m = vec![ + is_bit_meta(0, false), // first + is_bit_meta(1, false), // end + is_bit_meta(2, false), // μ + ConstraintMeta::base(3, 2), // (first + end)·(1 − μ) + ]; + m.extend(add_pair_meta(4, false)); // idx 4,5: address + 1 = address_incr + m.extend(add_pair_meta(6, false)); // idx 6,7: count_decr + 1 = count + m + } + + fn eval>(&self, b: &mut B) { + // idx 0-2: IS_BIT for first, end, mu + emit_is_bit(b, 0, cols::FIRST, None); + emit_is_bit(b, 1, cols::END, None); + emit_is_bit(b, 2, cols::MU, None); + + // idx 3: (first + end) * (1 - mu) + let one = b.one(); + let first = b.main(0, cols::FIRST); + let end = b.main(0, cols::END); + let mu = b.main(0, cols::MU); + b.emit_base(3, (first + end) * (one - mu)); + + // idx 4,5: ADD template for address + 1 = address_incr (unconditional) + emit_add_pair( + b, + 4, + &[], + &AddOperand::dword(cols::ADDRESS_0), + &AddOperand::constant(1), + &AddOperand::from_dword_hl(cols::ADDRESS_INCR_0), + ); + + // idx 6,7: SUB via ADD: count_decr + 1 = count (unconditional) + emit_add_pair( + b, + 6, + &[], + &AddOperand::from_dword_hl(cols::COUNT_DECR_0), + &AddOperand::constant(1), + &AddOperand::dword(cols::COUNT_0), + ); + } +} diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index eb519abe4..65e396468 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -294,3 +294,37 @@ mod memw_register { ); } } + +// ============================================================================= +// branch.rs +// ============================================================================= + +mod branch { + use super::*; + use crate::tables::branch::{BranchConstraints, branch_constraints, cols}; + use stark::constraints::transition::TransitionConstraint; + + #[test] + fn branch_constraint_set_matches_old() { + // `branch_constraints` returns unboxed `BranchConstraint`s; box them + // for the differential oracle. + let (old_structs, _) = branch_constraints(0); + let old: OldVec = old_structs.into_iter().map(|c| c.boxed()).collect(); + check_table("branch", &old, &BranchConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// commit.rs +// ============================================================================= + +mod commit { + use super::*; + use crate::tables::commit::{CommitConstraints, cols, create_constraints}; + + #[test] + fn commit_constraint_set_matches_old() { + let (old, _) = create_constraints(0); + check_table("commit", &old, &CommitConstraints, cols::NUM_COLUMNS); + } +} From 48391520f018bba6434e0ce3fef37e52a72c3d9f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:15:23 -0300 Subject: [PATCH 14/52] spike(stark): convert KECCAK + KECCAK_RND tables to single-source ConstraintSet Add KeccakConstraints (51: 25 mu-gated ADD carry pairs for state_ptr lanes + top-lane no-overflow) and KeccakRndConstraints (20 mu-gated is-bit on Cxz_right), mirroring the old boxed builders index-for-index; extend constraint_set_tests_b with 1000-row differential + meta-parity checks. --- prover/src/tables/keccak.rs | 70 ++++++++++++++++++++++ prover/src/tables/keccak_rnd.rs | 32 ++++++++++ prover/src/tests/constraint_set_tests_b.rs | 30 ++++++++++ 3 files changed, 132 insertions(+) diff --git a/prover/src/tables/keccak.rs b/prover/src/tables/keccak.rs index 0f305255b..f70fd0f02 100644 --- a/prover/src/tables/keccak.rs +++ b/prover/src/tables/keccak.rs @@ -23,6 +23,8 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; use crate::constraints::templates::{AddConstraint, AddOperand, INV_SHIFT_32}; @@ -561,3 +563,71 @@ pub fn create_constraints( (constraints, idx) } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// The KECCAK core table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`create_constraints`] index-for-index (51 constraints): +/// - idx 0-49: for `lane_idx ∈ 0..25`, the `ADD` carry pair (gated on `μ`) +/// enforcing `state_ptr[lane] = addr + 8·lane_idx` (`addr` DWordBL, +/// `state_ptr` DWordHL); +/// - idx 50: `μ · carry_1 = 0` (top-lane no-overflow), where `carry_1` is the +/// high carry of `addr + 192 = state_ptr[24]`. +pub struct KeccakConstraints; + +impl ConstraintSet for KeccakConstraints { + fn meta(&self) -> Vec { + let mut m = Vec::with_capacity(51); + for lane_idx in 0..25 { + // ADD pair is conditional on μ → degree 3. + m.extend(crate::constraints::templates::add_pair_meta( + lane_idx * 2, + true, + )); + } + m.push(ConstraintMeta::base(50, 2)); // μ · carry_1 + m + } + + fn eval>(&self, b: &mut B) { + use crate::constraints::templates::emit_add_pair; + + // idx 0-49: state_ptr[lane] = addr + 8*lane_idx (gated on μ). + for lane_idx in 0..25 { + let offset = (lane_idx * 8) as i64; + emit_add_pair( + b, + lane_idx * 2, + &[cols::MU], + &AddOperand::from_dword_bl(cols::ADDR), + &AddOperand::constant(offset), + &AddOperand::from_dword_hl(cols::state_ptr(lane_idx, 0)), + ); + } + + // idx 50: μ · carry_1 (top-lane no-overflow). + let c256 = b.const_base(256); + let c65536 = b.const_base(65536); + let c16777216 = b.const_base(16777216); + let addr_lo = b.main(0, cols::addr(0)) + + b.main(0, cols::addr(1)) * c256.clone() + + b.main(0, cols::addr(2)) * c65536.clone() + + b.main(0, cols::addr(3)) * c16777216.clone(); + let addr_hi = b.main(0, cols::addr(4)) + + b.main(0, cols::addr(5)) * c256 + + b.main(0, cols::addr(6)) * c65536.clone() + + b.main(0, cols::addr(7)) * c16777216; + let ptr_lo = + b.main(0, cols::state_ptr(24, 0)) + b.main(0, cols::state_ptr(24, 1)) * c65536.clone(); + let ptr_hi = b.main(0, cols::state_ptr(24, 2)) + b.main(0, cols::state_ptr(24, 3)) * c65536; + + let inv_2_32 = b.const_base(INV_SHIFT_32); + let c192 = b.const_base(192); + let carry_0 = (addr_lo + c192 - ptr_lo) * inv_2_32.clone(); + let carry_1 = (addr_hi + carry_0 - ptr_hi) * inv_2_32; + let mu = b.main(0, cols::MU); + b.emit_base(50, mu * carry_1); + } +} diff --git a/prover/src/tables/keccak_rnd.rs b/prover/src/tables/keccak_rnd.rs index 279b5c152..fa65da3d3 100644 --- a/prover/src/tables/keccak_rnd.rs +++ b/prover/src/tables/keccak_rnd.rs @@ -29,6 +29,7 @@ //! produces a single-bit carry, range-checked via IS_BIT polynomial constraints. use executor::vm::instruction::execution::{KECCAK_RC, KECCAK_RHO}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; @@ -932,3 +933,34 @@ pub fn create_constraints( } (constraints, idx) } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// The KECCAK round table's transition constraints as a single +/// [`ConstraintSet`], mirroring [`create_constraints`] index-for-index (20 +/// constraints): for `x ∈ 0..5`, `hw ∈ 0..4` (idx `x·4 + hw`), the μ-gated +/// `IS_BIT` on `Cxz_right[x][hw]` — `μ · Cxz_right·(1 − Cxz_right)`. +pub struct KeccakRndConstraints; + +impl ConstraintSet for KeccakRndConstraints { + fn meta(&self) -> Vec { + // 20 conditional IS_BIT constraints → degree 3. + (0..20) + .map(|i| crate::constraints::templates::is_bit_meta(i, true)) + .collect() + } + + fn eval>(&self, b: &mut B) { + use crate::constraints::templates::emit_is_bit; + + let mut idx = 0; + for x in 0..5 { + for hw in 0..4 { + emit_is_bit(b, idx, cols::cxz_right_bit(x, hw), Some(cols::MU)); + idx += 1; + } + } + } +} diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index 65e396468..8cdf2fbab 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -328,3 +328,33 @@ mod commit { check_table("commit", &old, &CommitConstraints, cols::NUM_COLUMNS); } } + +// ============================================================================= +// keccak.rs +// ============================================================================= + +mod keccak { + use super::*; + use crate::tables::keccak::{KeccakConstraints, cols, create_constraints}; + + #[test] + fn keccak_constraint_set_matches_old() { + let (old, _) = create_constraints(0); + check_table("keccak", &old, &KeccakConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// keccak_rnd.rs +// ============================================================================= + +mod keccak_rnd { + use super::*; + use crate::tables::keccak_rnd::{KeccakRndConstraints, cols, create_constraints}; + + #[test] + fn keccak_rnd_constraint_set_matches_old() { + let (old, _) = create_constraints(0); + check_table("keccak_rnd", &old, &KeccakRndConstraints, cols::NUM_COLUMNS); + } +} From 7a2053e506099bfd02240f479e314bdae28ad5c7 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:16:55 -0300 Subject: [PATCH 15/52] spike(stark): convert CPU32 table to single-source ConstraintSet Add Cpu32Constraints (32: is-bit flags, ADD/SUB carry pairs, register-zero limbs, sign-extension arithmetic, sign-zero/arg2-exclusive/flag=>mu), unrolling the old multi-kind cpu32_constraints assembly into straight-line emit calls; extend constraint_set_tests_b with 1000-row differential + meta-parity checks. --- prover/src/tables/cpu32.rs | 169 ++++++++++++++++++++- prover/src/tests/constraint_set_tests_b.rs | 15 ++ 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/prover/src/tables/cpu32.rs b/prover/src/tables/cpu32.rs index d7dbd5d6f..7f6c7a845 100644 --- a/prover/src/tables/cpu32.rs +++ b/prover/src/tables/cpu32.rs @@ -28,7 +28,12 @@ use super::types::{ BusId, FE, GoldilocksExtension, GoldilocksField, SHIFT_16, VmTable, alu_op, packed_decode_shrunk, }; -use crate::constraints::templates::{AddConstraint, AddOperand, new_is_bit_constraints}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +use crate::constraints::templates::{ + AddConstraint, AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, + new_is_bit_constraints, +}; // ========================================================================= // Column indices for CPU32 table @@ -843,3 +848,165 @@ pub fn cpu32_constraints( (constraints, idx) } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// The CPU32 table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`cpu32_constraints`] index-for-index (32 constraints): +/// - idx 0-6: `IS_BIT` on `read_register1/2`, `write_register`, `alu`, `add`, +/// `sub`, `μ`; +/// - idx 7,8: `ADD` pair `arg1 + arg2 = res` (gated on `add`); +/// - idx 9,10: `ADD` pair `arg2 + res = arg1` (gated on `sub`); +/// - idx 11-16: `(1 − read)·value` for the six `rv1/rv2` limbs; +/// - idx 17-22: sign-extension arithmetic `Arg1Lo/Hi`, `Arg2Lo/Hi`, `RvdLo/Hi`; +/// - idx 23,24: `(1 − signed)·rv·_sign` (sign zero when unsigned); +/// - idx 25,26: `read_register2·imm[i]` (arg2 exclusivity); +/// - idx 27-31: `(1 − μ)·flag` for `read_register1/2`, `write_register`, +/// `signed`, `res_sign`. +pub struct Cpu32Constraints; + +impl ConstraintSet for Cpu32Constraints { + fn meta(&self) -> Vec { + let mut m = Vec::with_capacity(32); + // idx 0-6: IS_BIT (unconditional, degree 2). + for i in 0..7 { + m.push(is_bit_meta(i, false)); + } + // idx 7,8: ADD pair gated on ADD (conditional, degree 3). + m.extend(add_pair_meta(7, true)); + // idx 9,10: SUB pair gated on SUB (conditional, degree 3). + m.extend(add_pair_meta(9, true)); + // idx 11-16: RegZero (degree 2). + for i in 11..17 { + m.push(ConstraintMeta::base(i, 2)); + } + // idx 17-22: sign-extension arithmetic (linear, degree 1). + for i in 17..23 { + m.push(ConstraintMeta::base(i, 1)); + } + // idx 23-31: sign-zero, arg2-exclusive, flag ⇒ μ (all degree 2). + for i in 23..32 { + m.push(ConstraintMeta::base(i, 2)); + } + m + } + + fn eval>(&self, b: &mut B) { + // idx 0-6: IS_BIT on the flag columns and the multiplicity. + for (i, &col) in [ + cols::READ_REGISTER1, + cols::READ_REGISTER2, + cols::WRITE_REGISTER, + cols::ALU, + cols::ADD, + cols::SUB, + cols::MU, + ] + .iter() + .enumerate() + { + emit_is_bit(b, i, col, None); + } + + // idx 7,8: ADD fast-path arg1 + arg2 = res (cond = ADD). + emit_add_pair( + b, + 7, + &[cols::ADD], + &AddOperand::dword(cols::ARG1_0), + &AddOperand::dword(cols::ARG2_0), + &AddOperand::from_dword_hl(cols::RES_0), + ); + + // idx 9,10: SUB fast-path arg2 + res = arg1 (cond = SUB). + emit_add_pair( + b, + 9, + &[cols::SUB], + &AddOperand::dword(cols::ARG2_0), + &AddOperand::from_dword_hl(cols::RES_0), + &AddOperand::dword(cols::ARG1_0), + ); + + // idx 11-16: unread register limbs are zero: (1 - read)·value. + let mut idx = 11; + for (read_col, value_col) in [ + (cols::READ_REGISTER1, cols::RV1_0), + (cols::READ_REGISTER1, cols::RV1_1), + (cols::READ_REGISTER1, cols::RV1_2), + (cols::READ_REGISTER2, cols::RV2_0), + (cols::READ_REGISTER2, cols::RV2_1), + (cols::READ_REGISTER2, cols::RV2_2), + ] { + let one = b.one(); + let read = b.main(0, read_col); + let value = b.main(0, value_col); + b.emit_base(idx, (one - read) * value); + idx += 1; + } + + // idx 17-22: sign-extension (`ext`) arithmetic for arg1, arg2, rvd. + let shift16 = b.const_base(SHIFT_16); + let hi_fill = b.const_base(HI_FILL); + + // Arg1Lo: arg1_0 - rv1_0 - shift16·rv1_1 + let e = b.main(0, cols::ARG1_0) + - b.main(0, cols::RV1_0) + - shift16.clone() * b.main(0, cols::RV1_1); + b.emit_base(17, e); + // Arg1Hi: arg1_1 - hi_fill·rv1_sign + let e = b.main(0, cols::ARG1_1) - hi_fill.clone() * b.main(0, cols::RV1_SIGN); + b.emit_base(18, e); + // Arg2Lo: arg2_0 - rv2_0 - shift16·rv2_1 - imm_0 + let e = b.main(0, cols::ARG2_0) + - b.main(0, cols::RV2_0) + - shift16.clone() * b.main(0, cols::RV2_1) + - b.main(0, cols::IMM_0); + b.emit_base(19, e); + // Arg2Hi: arg2_1 - hi_fill·rv2_sign - imm_1 + let e = b.main(0, cols::ARG2_1) + - hi_fill.clone() * b.main(0, cols::RV2_SIGN) + - b.main(0, cols::IMM_1); + b.emit_base(20, e); + // RvdLo: rvd_0 - res_0 - shift16·res_1 + let e = b.main(0, cols::RVD_0) - b.main(0, cols::RES_0) - shift16 * b.main(0, cols::RES_1); + b.emit_base(21, e); + // RvdHi: rvd_1 - hi_fill·res_sign + let e = b.main(0, cols::RVD_1) - hi_fill * b.main(0, cols::RES_SIGN); + b.emit_base(22, e); + + // idx 23,24: (1 - signed)·sign — sign bit is zero when unsigned. + for (i, sign_col) in [cols::RV1_SIGN, cols::RV2_SIGN].into_iter().enumerate() { + let one = b.one(); + let signed = b.main(0, cols::SIGNED); + let sign = b.main(0, sign_col); + b.emit_base(23 + i, (one - signed) * sign); + } + + // idx 25,26: read_register2·imm[i] (arg2 multiplex exclusivity). + for (i, imm_col) in [cols::IMM_0, cols::IMM_1].into_iter().enumerate() { + let rr2 = b.main(0, cols::READ_REGISTER2); + let imm = b.main(0, imm_col); + b.emit_base(25 + i, rr2 * imm); + } + + // idx 27-31: (1 - μ)·flag — flag must be 0 on padding rows. + for (i, flag_col) in [ + cols::READ_REGISTER1, + cols::READ_REGISTER2, + cols::WRITE_REGISTER, + cols::SIGNED, + cols::RES_SIGN, + ] + .into_iter() + .enumerate() + { + let one = b.one(); + let mu = b.main(0, cols::MU); + let flag = b.main(0, flag_col); + b.emit_base(27 + i, (one - mu) * flag); + } + } +} diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index 8cdf2fbab..4d11ff0ed 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -358,3 +358,18 @@ mod keccak_rnd { check_table("keccak_rnd", &old, &KeccakRndConstraints, cols::NUM_COLUMNS); } } + +// ============================================================================= +// cpu32.rs +// ============================================================================= + +mod cpu32 { + use super::*; + use crate::tables::cpu32::{Cpu32Constraints, cols, cpu32_constraints}; + + #[test] + fn cpu32_constraint_set_matches_old() { + let (old, _) = cpu32_constraints(0); + check_table("cpu32", &old, &Cpu32Constraints, cols::NUM_COLUMNS); + } +} From 51a07b9271bbca35e7a70d5f841211424c8c6b65 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:17:58 -0300 Subject: [PATCH 16/52] sscs: convert DVRM + SHIFT tables to single-source ConstraintSet --- prover/src/tables/dvrm.rs | 195 +++++++++++++++++++++++++++++++++++++ prover/src/tables/shift.rs | 190 ++++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+) diff --git a/prover/src/tables/dvrm.rs b/prover/src/tables/dvrm.rs index 3da78dff5..7df15ba83 100644 --- a/prover/src/tables/dvrm.rs +++ b/prover/src/tables/dvrm.rs @@ -1310,3 +1310,198 @@ pub fn dvrm_constraints(constraint_idx_start: usize) -> (Vec, us (constraints, idx) } + +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// Non-destructive twin of `DvrmConstraint` / `dvrm_constraints` above, written +// once against the generic `ConstraintBuilder` so one body serves the compiled +// prover folder, the verifier folder and IR capture. The old structs stay for +// now (they are the differential oracle); the final deletion phase removes +// them. Constraint indices 0..19 match `dvrm_constraints(0)` exactly. + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +/// DVRM table constraints as a single-source [`ConstraintSet`]. No column +/// configuration is needed (the DVRM layout is fixed via `cols`). +pub struct DvrmConstraints; + +impl DvrmConstraints { + /// Sign-extended QuadWL word `k` (0..4) of a halfword group, matching + /// [`DvrmConstraint::build_extended_quad`]: + /// `[hw0 + hw1·2^16, hw2 + hw3·2^16, ext, ext]`, where + /// `ext = sign·SIGN_FILL + sign·SIGN_FILL·2^16`. + fn ext_quad>( + b: &B, + hw: [usize; 4], + sign_col: usize, + k: usize, + ) -> B::Expr { + let shift_16 = b.const_base(SHIFT_16); + match k { + 0 => { + let hw0 = b.main(0, hw[0]); + let hw1 = b.main(0, hw[1]); + hw0 + hw1 * shift_16 + } + 1 => { + let hw2 = b.main(0, hw[2]); + let hw3 = b.main(0, hw[3]); + hw2 + hw3 * shift_16 + } + _ => { + // ext = sign * SIGN_FILL + sign * SIGN_FILL * 2^16 + let sign = b.main(0, sign_col); + let sign_fill = b.const_base(SIGN_FILL); + let sign_fill2 = b.const_base(SIGN_FILL); + let shift_16b = b.const_base(SHIFT_16); + sign.clone() * sign_fill + sign * sign_fill2 * shift_16b + } + } + } + + /// Virtual carry[i] for `n = n_sub_r + r`, matching + /// [`DvrmConstraint::compute_carry`] (extended QuadWL, recursive chain). + fn carry>( + b: &B, + i: usize, + ) -> B::Expr { + const N: [usize; 4] = [cols::N_0, cols::N_1, cols::N_2, cols::N_3]; + const NSR: [usize; 4] = [ + cols::N_SUB_R_0, + cols::N_SUB_R_1, + cols::N_SUB_R_2, + cols::N_SUB_R_3, + ]; + const R: [usize; 4] = [cols::R_0, cols::R_1, cols::R_2, cols::R_3]; + + let ext_n = Self::ext_quad(b, N, cols::SIGN_N, i); + let ext_r = Self::ext_quad(b, R, cols::SIGN_R, i); + let ext_nsr = Self::ext_quad(b, NSR, cols::SIGN_N_SUB_R, i); + let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); + + if i == 0 { + // carry[0] = (ext_nsr[0] + ext_r[0] - ext_n[0]) / 2^32 + (ext_nsr + ext_r - ext_n) * inv_2_32 + } else { + // carry[i] = (ext_nsr[i] + ext_r[i] + carry[i-1] - ext_n[i]) / 2^32 + let prev = Self::carry(b, i - 1); + (ext_nsr + ext_r + prev - ext_n) * inv_2_32 + } + } + + /// `r::DWordWL[i]` (i = 0 → lo32, else hi32), matching the `AbsRFormula` + /// arm; used generically for r or d halfword groups. + fn dword_wl>( + b: &B, + lo: usize, + hi: usize, + ) -> B::Expr { + let shift_16 = b.const_base(SHIFT_16); + let a = b.main(0, lo); + let c = b.main(0, hi); + a + c * shift_16 + } +} + +impl ConstraintSet for DvrmConstraints { + fn meta(&self) -> Vec { + // All DVRM constraints are declared degree 2 (see DvrmConstraint::degree). + (0..19).map(|i| ConstraintMeta::base(i, 2)).collect() + } + + fn eval>(&self, b: &mut B) { + // idx 0: SignedIsBit — signed * (1 - signed) + let signed = b.main(0, cols::SIGNED); + let one = b.one(); + b.emit_base(0, signed.clone() * (one - signed)); + + // idx 1: RemainderSignMatchesNumerator — + // (r[0]+r[1]+r[2]+r[3]) * (sign_r - sign_n) + let r0 = b.main(0, cols::R_0); + let r1 = b.main(0, cols::R_1); + let r2 = b.main(0, cols::R_2); + let r3 = b.main(0, cols::R_3); + let sign_r = b.main(0, cols::SIGN_R); + let sign_n = b.main(0, cols::SIGN_N); + let r_sum = r0 + r1 + r2 + r3; + b.emit_base(1, r_sum * (sign_r - sign_n)); + + // idx 2,3: AbsRFormula(0,1) — (1-sign_r) * (abs_r[i] - r::DWordWL[i]) + for (off, (abs_col, lo, hi)) in [ + (cols::ABS_R_0, cols::R_0, cols::R_1), + (cols::ABS_R_1, cols::R_2, cols::R_3), + ] + .into_iter() + .enumerate() + { + let sign_r = b.main(0, cols::SIGN_R); + let one = b.one(); + let abs_r = b.main(0, abs_col); + let r_wl = Self::dword_wl(b, lo, hi); + b.emit_base(2 + off, (one - sign_r) * (abs_r - r_wl)); + } + + // idx 4,5: AbsDFormula(0,1) — (1-sign_d) * (abs_d[i] - d::DWordWL[i]) + for (off, (abs_col, lo, hi)) in [ + (cols::ABS_D_0, cols::D_0, cols::D_1), + (cols::ABS_D_1, cols::D_2, cols::D_3), + ] + .into_iter() + .enumerate() + { + let sign_d = b.main(0, cols::SIGN_D); + let one = b.one(); + let abs_d = b.main(0, abs_col); + let d_wl = Self::dword_wl(b, lo, hi); + b.emit_base(4 + off, (one - sign_d) * (abs_d - d_wl)); + } + + // idx 6: SignQFormula — signed * (1-overflow) - sign_q + let signed = b.main(0, cols::SIGNED); + let overflow = b.main(0, cols::OVERFLOW); + let sign_q = b.main(0, cols::SIGN_Q); + let one = b.one(); + b.emit_base(6, signed * (one - overflow) - sign_q); + + // idx 7..11: CarryIsBit(0..4) — carry[i] * (1 - carry[i]) + for i in 0..4 { + let carry = Self::carry(b, i); + let one = b.one(); + b.emit_base(7 + i, carry.clone() * (one - carry)); + } + + // idx 11: SignNSubRIsBit — sign_n_sub_r * (1 - sign_n_sub_r) + let sign = b.main(0, cols::SIGN_N_SUB_R); + let one = b.one(); + b.emit_base(11, sign.clone() * (one - sign)); + + // idx 12: UnsignedSignN — (1-signed) * sign_n + let signed = b.main(0, cols::SIGNED); + let sign_n = b.main(0, cols::SIGN_N); + let one = b.one(); + b.emit_base(12, (one - signed) * sign_n); + + // idx 13: UnsignedSignR — (1-signed) * sign_r + let signed = b.main(0, cols::SIGNED); + let sign_r = b.main(0, cols::SIGN_R); + let one = b.one(); + b.emit_base(13, (one - signed) * sign_r); + + // idx 14: UnsignedSignD — (1-signed) * sign_d + let signed = b.main(0, cols::SIGNED); + let sign_d = b.main(0, cols::SIGN_D); + let one = b.one(); + b.emit_base(14, (one - signed) * sign_d); + + // idx 15..19: DivByZeroQ(0..4) — div_by_zero * (q[i] - 65535) + let q_cols = [cols::Q_0, cols::Q_1, cols::Q_2, cols::Q_3]; + for (i, &q_col) in q_cols.iter().enumerate() { + let dbz = b.main(0, cols::DIV_BY_ZERO); + let q = b.main(0, q_col); + let fill = b.const_base(SIGN_FILL); + b.emit_base(15 + i, dbz * (q - fill)); + } + } +} diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index 3115784f6..44b25ec68 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -988,6 +988,196 @@ pub fn shift_constraints(constraint_idx_start: usize) -> (Vec, (constraints, idx) } +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// Non-destructive twin of `ShiftConstraint` / `shift_constraints` above, +// written once against the generic `ConstraintBuilder` so one body serves the +// compiled prover folder, the verifier folder and IR capture. The old structs +// stay for now (they are the differential oracle); the final deletion phase +// removes them. Constraint indices 0..19 match `shift_constraints(0)` exactly. + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +/// SHIFT table constraints as a single-source [`ConstraintSet`]. No column +/// configuration is needed (the SHIFT layout is fixed via `cols`). +pub struct ShiftConstraints; + +impl ShiftConstraints { + /// `limb_shift[i]` (i = 0..2 raw, i = 3 virtual + /// `1 - ls_raw[0] - ls_raw[1] - ls_raw[2]`), matching `get_ls`. + fn limb_shift>( + b: &B, + i: usize, + ) -> B::Expr { + if i < 3 { + b.main(0, cols::LIMB_SHIFT_RAW[i]) + } else { + let one = b.one(); + let a = b.main(0, cols::LIMB_SHIFT_RAW[0]); + let c = b.main(0, cols::LIMB_SHIFT_RAW[1]); + let d = b.main(0, cols::LIMB_SHIFT_RAW[2]); + one - a - c - d + } + } + + /// intra_limb_left[i]: X[0] for i=0, X[i]+Y[i-1] for i>0. + fn intra_left>( + b: &B, + i: usize, + ) -> B::Expr { + if i == 0 { + b.main(0, cols::X[0]) + } else { + let x = b.main(0, cols::X[i]); + let y = b.main(0, cols::Y[i - 1]); + x + y + } + } + + /// intra_limb_right[i]: Y[i]+X[i+1]. + fn intra_right>( + b: &B, + i: usize, + ) -> B::Expr { + let y = b.main(0, cols::Y[i]); + let x = b.main(0, cols::X[i + 1]); + y + x + } + + /// The `shifted` virtual column at index `half_idx` (0..4), matching + /// [`ShiftConstraint::compute_shifted_half`]. + fn shifted_half>( + b: &B, + i: usize, + ) -> B::Expr { + // left = μ - direction, right = direction + let mu = b.main(0, cols::MU); + let dir = b.main(0, cols::DIRECTION); + let left = mu - dir; + let right = b.main(0, cols::DIRECTION); + + // extension = 65535 * is_negative + let is_neg = b.main(0, cols::IS_NEGATIVE); + let c65535 = b.const_base(65535); + let extension = is_neg * c65535; + + // left_part = left * Σ_{j=0}^{i} limb_shift[j] * intra_limb_left[i-j] + let mut left_part = b.zero(); + for j in 0..=i { + left_part = left_part + Self::limb_shift(b, j) * Self::intra_left(b, i - j); + } + let left_part = left * left_part; + + // right_shift_part = Σ_{j=0}^{3-i} limb_shift[j] * intra_limb_right[i+j] + let mut right_shift_part = b.zero(); + for j in 0..=(3 - i) { + right_shift_part = + right_shift_part + Self::limb_shift(b, j) * Self::intra_right(b, i + j); + } + + // right_ext_part = extension * Σ_{j=4-i}^{3} limb_shift[j] + let mut ext_sum = b.zero(); + if i < 4 { + for j in (4 - i)..4 { + ext_sum = ext_sum + Self::limb_shift(b, j); + } + } + let right_ext_part = extension * ext_sum; + + let right_part = right * (right_shift_part + right_ext_part); + + left_part + right_part + } +} + +impl ConstraintSet for ShiftConstraints { + fn meta(&self) -> Vec { + let mut m = Vec::with_capacity(NUM_SHIFT_CONSTRAINTS); + m.push(ConstraintMeta::base(0, 2)); // DirectionImpliesMu + for i in 0..4 { + m.push(ConstraintMeta::base(1 + i, 3)); // ZbsOverrideX + } + m.push(ConstraintMeta::base(5, 2)); // ZbsOverrideX4 + for i in 0..4 { + m.push(ConstraintMeta::base(6 + i, 3)); // ZbsOverrideY + } + for i in 0..4 { + m.push(ConstraintMeta::base(10 + i, 2)); // LimbShiftIsBit + } + for i in 0..2 { + m.push(ConstraintMeta::base(14 + i, 3)); // OutputMatchesShifted + } + for i in 0..3 { + m.push(ConstraintMeta::base(16 + i, 2)); // FlagIsBit + } + debug_assert_eq!(m.len(), NUM_SHIFT_CONSTRAINTS); + m + } + + fn eval>(&self, b: &mut B) { + // idx 0: DirectionImpliesMu — direction * (1 - μ) + let dir = b.main(0, cols::DIRECTION); + let mu = b.main(0, cols::MU); + let one = b.one(); + b.emit_base(0, dir * (one - mu)); + + // idx 1..5: ZbsOverrideX(i) — zbs * (X[i] - in[i] * (μ - direction)) + for i in 0..4 { + let zbs = b.main(0, cols::ZBS); + let x_i = b.main(0, cols::X[i]); + let in_i = b.main(0, cols::IN[i]); + let mu = b.main(0, cols::MU); + let dir = b.main(0, cols::DIRECTION); + let left = mu - dir; + b.emit_base(1 + i, zbs * (x_i - in_i * left)); + } + + // idx 5: ZbsOverrideX4 — zbs * X[4] + let zbs = b.main(0, cols::ZBS); + let x4 = b.main(0, cols::X_4); + b.emit_base(5, zbs * x4); + + // idx 6..10: ZbsOverrideY(i) — zbs * (Y[i] - in[i] * direction) + for i in 0..4 { + let zbs = b.main(0, cols::ZBS); + let y_i = b.main(0, cols::Y[i]); + let in_i = b.main(0, cols::IN[i]); + let dir = b.main(0, cols::DIRECTION); + b.emit_base(6 + i, zbs * (y_i - in_i * dir)); + } + + // idx 10..14: LimbShiftIsBit(i) — limb_shift[i] * (1 - limb_shift[i]) + for i in 0..4 { + let ls = Self::limb_shift(b, i); + let one = b.one(); + b.emit_base(10 + i, ls.clone() * (one - ls)); + } + + // idx 14,15: OutputMatchesShifted(i) — + // out[i] - shifted_half[2i] - shifted_half[2i+1] * 2^16 + for i in 0..2 { + let out_col = if i == 0 { cols::OUT_0 } else { cols::OUT_1 }; + let out = b.main(0, out_col); + let half_lo = Self::shifted_half(b, 2 * i); + let half_hi = Self::shifted_half(b, 2 * i + 1); + let shift_16 = b.const_base(SHIFT_16); + b.emit_base(14 + i, out - half_lo - half_hi * shift_16); + } + + // idx 16..19: FlagIsBit — flag * (1 - flag) for direction, signed, word_instr + for (off, flag_col) in [cols::DIRECTION, cols::SIGNED, cols::WORD_INSTR] + .into_iter() + .enumerate() + { + let flag = b.main(0, flag_col); + let one = b.one(); + b.emit_base(16 + off, flag.clone() * (one - flag)); + } + } +} + // ========================================================================= // Bitwise operation collection // ========================================================================= From 48fab6a0f0a6e62d67ae7a83850e0b497272642a Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:17:58 -0300 Subject: [PATCH 17/52] sscs: convert MUL + LOAD tables to single-source ConstraintSet --- prover/src/tables/load.rs | 118 ++++++++++++++++++++++++++++++ prover/src/tables/mul.rs | 149 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) diff --git a/prover/src/tables/load.rs b/prover/src/tables/load.rs index 250d565b2..cf83c6d39 100644 --- a/prover/src/tables/load.rs +++ b/prover/src/tables/load.rs @@ -637,3 +637,121 @@ pub fn constraints() constraints } + +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// Non-destructive twin of `LoadConstraint` / `constraints()` above, written +// once against the generic `ConstraintBuilder` so one body serves the compiled +// prover folder, the verifier folder and IR capture. The old structs stay for +// now (they are the differential oracle); the final deletion phase removes +// them. Constraint indices 0..13 match `constraints()` (idx_start = 0) exactly: +// 0..4: FlagIsBit(SIGNED, READ2, READ4, READ8) 4: WidthSumIsBit +// 5: ReadImpliesMu 6..10: ExtensionHigh(4..8) +// 10..12: ExtensionMid(2..4) 12: ExtensionLow + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +/// LOAD table constraints as a single-source [`ConstraintSet`]. No column +/// configuration is needed (the LOAD layout is fixed via `cols`). +pub struct LoadConstraints; + +impl LoadConstraints { + /// `flag · (1 − flag)` IS_BIT check for a boolean flag column. + fn flag_is_bit>( + b: &B, + col: usize, + ) -> B::Expr { + let flag = b.main(0, col); + let one = b.one(); + flag.clone() * (one - flag) + } + + /// `signed · sign_bit · 255` — the sign-extended byte value. + fn extended>(b: &B) -> B::Expr { + let signed = b.main(0, cols::SIGNED); + let sign_bit = b.main(0, cols::SIGN_BIT); + let ff = b.const_base(255); + signed * sign_bit * ff + } +} + +impl ConstraintSet for LoadConstraints { + fn meta(&self) -> Vec { + vec![ + ConstraintMeta::base(0, 2), // FlagIsBit(SIGNED) + ConstraintMeta::base(1, 2), // FlagIsBit(READ2) + ConstraintMeta::base(2, 2), // FlagIsBit(READ4) + ConstraintMeta::base(3, 2), // FlagIsBit(READ8) + ConstraintMeta::base(4, 2), // WidthSumIsBit + ConstraintMeta::base(5, 2), // ReadImpliesMu + ConstraintMeta::base(6, 3), // ExtensionHigh(4) + ConstraintMeta::base(7, 3), // ExtensionHigh(5) + ConstraintMeta::base(8, 3), // ExtensionHigh(6) + ConstraintMeta::base(9, 3), // ExtensionHigh(7) + ConstraintMeta::base(10, 3), // ExtensionMid(2) + ConstraintMeta::base(11, 3), // ExtensionMid(3) + ConstraintMeta::base(12, 3), // ExtensionLow (res[1]) + ] + } + + fn eval>(&self, b: &mut B) { + // idx 0..4: IS_BIT on the width/sign flags. + for (i, flag_col) in [cols::SIGNED, cols::READ2, cols::READ4, cols::READ8] + .into_iter() + .enumerate() + { + let root = Self::flag_is_bit(b, flag_col); + b.emit_base(i, root); + } + + // idx 4: IS_BIT on the width-selector sum (read2 + read4 + read8). + let read2 = b.main(0, cols::READ2); + let read4 = b.main(0, cols::READ4); + let read8 = b.main(0, cols::READ8); + let sum = read2 + read4 + read8; + let one = b.one(); + b.emit_base(4, sum.clone() * (one - sum)); + + // idx 5: (read2 + read4 + read8) * (1 - μ) + let read2 = b.main(0, cols::READ2); + let read4 = b.main(0, cols::READ4); + let read8 = b.main(0, cols::READ8); + let mu = b.main(0, cols::MU); + let read_sum = read2 + read4 + read8; + let one = b.one(); + b.emit_base(5, read_sum * (one - mu)); + + // idx 6..10: ExtensionHigh(i) for i in 4..8: + // (1 - read8) * (res[i] - signed*sign_bit*255) + for (offset, i) in (4..8).enumerate() { + let read8 = b.main(0, cols::READ8); + let res_i = b.main(0, cols::RES[i]); + let expected = Self::extended(b); + let one = b.one(); + b.emit_base(6 + offset, (one - read8) * (res_i - expected)); + } + + // idx 10,11: ExtensionMid(i) for i in 2..4: + // (1 - read4 - read8) * (res[i] - signed*sign_bit*255) + for (offset, i) in (2..4).enumerate() { + let read4 = b.main(0, cols::READ4); + let read8 = b.main(0, cols::READ8); + let res_i = b.main(0, cols::RES[i]); + let expected = Self::extended(b); + let one = b.one(); + b.emit_base(10 + offset, (one - read4 - read8) * (res_i - expected)); + } + + // idx 12: ExtensionLow: + // (1 - read2 - read4 - read8) * (res[1] - signed*sign_bit*255) + let read2 = b.main(0, cols::READ2); + let read4 = b.main(0, cols::READ4); + let read8 = b.main(0, cols::READ8); + let res_1 = b.main(0, cols::RES[1]); + let expected = Self::extended(b); + let one = b.one(); + b.emit_base(12, (one - read2 - read4 - read8) * (res_1 - expected)); + } +} diff --git a/prover/src/tables/mul.rs b/prover/src/tables/mul.rs index 33679211c..402154910 100644 --- a/prover/src/tables/mul.rs +++ b/prover/src/tables/mul.rs @@ -905,6 +905,155 @@ pub fn mul_constraints(constraint_idx_start: usize) -> (Vec, usiz (constraints, idx) } +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// Non-destructive twin of `MulConstraint` / `mul_constraints` above, written +// once against the generic `ConstraintBuilder` so one body serves the compiled +// prover folder, the verifier folder and IR capture. The old structs stay for +// now (they are the differential oracle); the final deletion phase removes +// them. Constraint indices 0..8 match `mul_constraints(0)` exactly: +// 0: SignedIsBit(LHS_SIGNED) 1: SignedIsBit(RHS_SIGNED) +// 2: LhsSign 3: RhsSign +// 4..8: RawProduct(0..4) + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +/// MUL table constraints as a single-source [`ConstraintSet`]. No column +/// configuration is needed (the MUL layout is fixed via `cols`). +pub struct MulConstraints; + +impl MulConstraints { + /// `x · (1 − x)` IS_BIT check for a sign-flag column. + fn signed_is_bit>( + b: &B, + col: usize, + ) -> B::Expr { + let x = b.main(0, col); + let one = b.one(); + x.clone() * (one - x) + } + + /// `raw_product[i] − Σ_k 2^(16k)·Σ_j lhs_ext[j]·rhs_ext[idx−j]` (idx = 2i+k). + /// Mirrors [`MulConstraint::compute_raw_product_constraint`] exactly. + fn raw_product>( + b: &B, + i: usize, + ) -> B::Expr { + let lhs = [ + b.main(0, cols::LHS_0), + b.main(0, cols::LHS_1), + b.main(0, cols::LHS_2), + b.main(0, cols::LHS_3), + ]; + let rhs = [ + b.main(0, cols::RHS_0), + b.main(0, cols::RHS_1), + b.main(0, cols::RHS_2), + b.main(0, cols::RHS_3), + ]; + let lhs_is_neg = b.main(0, cols::LHS_IS_NEGATIVE); + let rhs_is_neg = b.main(0, cols::RHS_IS_NEGATIVE); + + // Sign-extended values: [0..4] = halfwords, [4..8] = sign_fill * is_neg. + let sign_fill = b.const_base(SIGN_FILL); + let lhs_hi = sign_fill.clone() * lhs_is_neg; + let rhs_hi = sign_fill * rhs_is_neg; + let lhs_ext: [B::Expr; 8] = [ + lhs[0].clone(), + lhs[1].clone(), + lhs[2].clone(), + lhs[3].clone(), + lhs_hi.clone(), + lhs_hi.clone(), + lhs_hi.clone(), + lhs_hi, + ]; + let rhs_ext: [B::Expr; 8] = [ + rhs[0].clone(), + rhs[1].clone(), + rhs[2].clone(), + rhs[3].clone(), + rhs_hi.clone(), + rhs_hi.clone(), + rhs_hi.clone(), + rhs_hi, + ]; + + // Convolution sum (same k/j bounds as the old code). + let shift_16 = b.const_base(SHIFT_16); + let mut sum = b.zero(); + for k in 0..=1usize { + let idx = 2 * i + k; + if idx < 8 { + let mut inner_sum = b.zero(); + for j in 0..=idx { + if j < 8 && (idx - j) < 8 { + inner_sum = inner_sum + lhs_ext[j].clone() * rhs_ext[idx - j].clone(); + } + } + if k == 0 { + sum = sum + inner_sum; + } else { + sum = sum + inner_sum * shift_16.clone(); + } + } + } + + let raw_col = match i { + 0 => cols::RAW_PRODUCT_0, + 1 => cols::RAW_PRODUCT_1, + 2 => cols::RAW_PRODUCT_2, + 3 => cols::RAW_PRODUCT_3, + _ => unreachable!(), + }; + let raw_product = b.main(0, raw_col); + raw_product - sum + } +} + +impl ConstraintSet for MulConstraints { + fn meta(&self) -> Vec { + vec![ + ConstraintMeta::base(0, 2), // SignedIsBit(LHS_SIGNED) + ConstraintMeta::base(1, 2), // SignedIsBit(RHS_SIGNED) + ConstraintMeta::base(2, 2), // LhsSign + ConstraintMeta::base(3, 2), // RhsSign + ConstraintMeta::base(4, 2), // RawProduct(0) + ConstraintMeta::base(5, 2), // RawProduct(1) + ConstraintMeta::base(6, 2), // RawProduct(2) + ConstraintMeta::base(7, 2), // RawProduct(3) + ] + } + + fn eval>(&self, b: &mut B) { + // idx 0,1: IS_BIT range checks on the sign-flag multiplicities. + let is_bit_lhs = Self::signed_is_bit(b, cols::LHS_SIGNED); + b.emit_base(0, is_bit_lhs); + let is_bit_rhs = Self::signed_is_bit(b, cols::RHS_SIGNED); + b.emit_base(1, is_bit_rhs); + + // idx 2: LhsSign: (1 - lhs_signed) * lhs_is_negative + let lhs_signed = b.main(0, cols::LHS_SIGNED); + let lhs_is_neg = b.main(0, cols::LHS_IS_NEGATIVE); + let one = b.one(); + b.emit_base(2, (one - lhs_signed) * lhs_is_neg); + + // idx 3: RhsSign: (1 - rhs_signed) * rhs_is_negative + let rhs_signed = b.main(0, cols::RHS_SIGNED); + let rhs_is_neg = b.main(0, cols::RHS_IS_NEGATIVE); + let one = b.one(); + b.emit_base(3, (one - rhs_signed) * rhs_is_neg); + + // idx 4..8: raw_product convolution for i = 0..4. + for i in 0..4 { + let root = Self::raw_product(b, i); + b.emit_base(4 + i, root); + } + } +} + // ========================================================================= // Helper functions // ========================================================================= From 5e247efdcab317d157b2c9ac49765e760be946cf Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:17:58 -0300 Subject: [PATCH 18/52] sscs: convert ECSM + ECDAS + EC_SCALAR tables + differential tests for all 8 tables --- prover/src/tables/ec_scalar.rs | 54 +++++ prover/src/tables/ecdas.rs | 215 +++++++++++++++++++ prover/src/tables/ecsm.rs | 233 +++++++++++++++++++++ prover/src/tests/constraint_set_tests_a.rs | 144 ++++++++++++- 4 files changed, 639 insertions(+), 7 deletions(-) diff --git a/prover/src/tables/ec_scalar.rs b/prover/src/tables/ec_scalar.rs index dd8d483a2..99d59374a 100644 --- a/prover/src/tables/ec_scalar.rs +++ b/prover/src/tables/ec_scalar.rs @@ -374,3 +374,57 @@ pub fn create_constraints( (constraints, idx) } + +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// Non-destructive twin of `create_constraints` above, written once against the +// generic `ConstraintBuilder`. The old structs/builder stay as the differential +// oracle; the final deletion phase removes them. Constraint indices 0..20 +// match `create_constraints(0)` exactly. + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +/// EC_SCALAR transition constraints as a single-source [`ConstraintSet`] (20 +/// total). No column configuration needed (the layout is fixed via `cols`). +pub struct EcScalarConstraints; + +impl ConstraintSet for EcScalarConstraints { + fn meta(&self) -> Vec { + // 10 unconditional IS_BIT (degree 2) + 10 MulZero (degree 2). + (0..20).map(|i| ConstraintMeta::base(i, 2)).collect() + } + + fn eval>(&self, b: &mut B) { + // idx 0..10: unconditional IS_BIT `x·(1−x)` for + // [mu, limb_bit(0..8), last_limb], in that column order. + let mut bit_cols = vec![cols::MU]; + bit_cols.extend((0..8).map(cols::limb_bit)); + bit_cols.push(cols::LAST_LIMB); + for (i, &col) in bit_cols.iter().enumerate() { + let x = b.main(0, col); + let one = b.one(); + b.emit_base(i, x.clone() * (one - x)); + } + + // idx 10..18: limb_bit(i) · (1 − mu) = 0. + for i in 0..8 { + let a = b.main(0, cols::limb_bit(i)); + let mu = b.main(0, cols::MU); + let one = b.one(); + b.emit_base(10 + i, a * (one - mu)); + } + + // idx 18: last_limb · (1 − mu) = 0. + let last_limb = b.main(0, cols::LAST_LIMB); + let mu = b.main(0, cols::MU); + let one = b.one(); + b.emit_base(18, last_limb * (one - mu)); + + // idx 19: last_limb · offset = 0. + let last_limb = b.main(0, cols::LAST_LIMB); + let offset = b.main(0, cols::OFFSET); + b.emit_base(19, last_limb * offset); + } +} diff --git a/prover/src/tables/ecdas.rs b/prover/src/tables/ecdas.rs index 6d508d363..88321263a 100644 --- a/prover/src/tables/ecdas.rs +++ b/prover/src/tables/ecdas.rs @@ -515,3 +515,218 @@ pub fn create_constraints( (constraints, idx) } + +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// Non-destructive twin of `create_constraints` above, written once against the +// generic `ConstraintBuilder`. The old structs/builder stay as the differential +// oracle; the final deletion phase removes them. Constraint indices 0..200 +// match `create_constraints(0)` exactly: +// 0,1,2 : IS_BIT(MU), IS_BIT(OP), IS_BIT(NEXT_OP) +// 3 : OP · NEXT_OP +// 4 : NEXT_OP · (1 − MU) +// then for (Lambda,C0),(Xr,C1),(Yr,C2): 64 ConvCarry (i=0..64) + 1 ColIsZero. + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +/// ECDAS transition constraints as a single-source [`ConstraintSet`] (200 +/// total). No column configuration needed (the layout is fixed via `cols`). +pub struct EcdasConstraints; + +impl EcdasConstraints { + /// Byte `m` of the base-point order `P` (zero beyond 32 bytes). Twin of + /// [`p_byte`]. + fn p_byte_expr>( + b: &B, + m: usize, + ) -> B::Expr { + if m < 32 { + b.const_base(P_BYTES[m] as u64) + } else { + b.zero() + } + } + + /// Byte `m` of `R` (zero beyond 33 bytes). Twin of [`r_byte`]. + fn r_byte_expr>( + b: &B, + m: usize, + ) -> B::Expr { + if m < 33 { + b.const_base(R_BYTES[m] as u64) + } else { + b.zero() + } + } + + /// `bytes[base + j]` for `j < len`, else zero (the `b` closure in `s_i`). + fn byte_at>( + b: &B, + base: usize, + len: usize, + j: usize, + ) -> B::Expr { + if j < len { + b.main(0, base + j) + } else { + b.zero() + } + } + + /// The r·P − q·P convolution term `Σ_{j=0..=i} (r_byte(j) − q[j])·p_byte(i−j)` + /// (shared structure across all three relations). + fn rq>( + b: &B, + i: usize, + qbase: usize, + ) -> B::Expr { + let mut s = b.zero(); + for j in 0..=i { + let term = (Self::r_byte_expr(b, j) - Self::byte_at(b, qbase, 33, j)) + * Self::p_byte_expr(b, i - j); + s = s + term; + } + s + } + + /// `S_i` for `relation` at limb `i` (twin of [`ConvCarry::s_i`]). + fn s_i>( + b: &B, + relation: Relation, + i: usize, + ) -> B::Expr { + let lam = |j: usize| Self::byte_at(b, cols::LAMBDA, 32, j); + let xg = |j: usize| Self::byte_at(b, cols::XG, 32, j); + let xa = |j: usize| Self::byte_at(b, cols::XA, 32, j); + let ya = |j: usize| Self::byte_at(b, cols::YA, 32, j); + let yg = |j: usize| Self::byte_at(b, cols::YG, 32, j); + let xr = |j: usize| Self::byte_at(b, cols::XR, 32, j); + let yr = |j: usize| Self::byte_at(b, cols::YR, 32, j); + let op = b.main(0, cols::OP); + let one = b.one(); + + match relation { + Relation::Lambda => { + // op·(Σ λ_j(xG-xA)_{i-j} + (yA_i - yG_i)) + let mut op_branch = ya(i) - yg(i); + for j in 0..=i { + op_branch = op_branch + lam(j) * (xg(i - j) - xa(i - j)); + } + // (1-op)·Σ (2 λ_j yA_{i-j} - 3 xA_j xA_{i-j}) + let mut notop_branch = b.zero(); + for j in 0..=i { + let two = b.const_base(2); + let three = b.const_base(3); + notop_branch = + notop_branch + two * lam(j) * ya(i - j) - three * xa(j) * xa(i - j); + } + op.clone() * op_branch + (one - op) * notop_branch + Self::rq(b, i, cols::Q0) + } + Relation::Xr => { + // Σ λ_j λ_{i-j} − xA_i − xG_i − xR_i − (1-op)(xA_i − xG_i) + rq + let mut s = b.zero(); + for j in 0..=i { + s = s + lam(j) * lam(i - j); + } + s - xa(i) - xg(i) - xr(i) - (one - op) * (xa(i) - xg(i)) + Self::rq(b, i, cols::Q1) + } + Relation::Yr => { + // Σ λ_j(xA-xR)_{i-j} − yA_i − yR_i + rq + let mut s = b.zero(); + for j in 0..=i { + s = s + lam(j) * (xa(i - j) - xr(i - j)); + } + s - ya(i) - yr(i) + Self::rq(b, i, cols::Q2) + } + } + } + + /// `256·c_i − c_{i-1} − S_i` (twin of [`ConvCarry::evaluate`]). + fn conv_carry>( + b: &B, + relation: Relation, + i: usize, + ) -> B::Expr { + let c_base = match relation { + Relation::Lambda => cols::C0, + Relation::Xr => cols::C1, + Relation::Yr => cols::C2, + }; + let c_i = b.main(0, c_base + i); + let c_prev = if i == 0 { + b.zero() + } else { + b.main(0, c_base + i - 1) + }; + let two_pow_8 = b.const_base(256); + two_pow_8 * c_i - c_prev - Self::s_i(b, relation, i) + } +} + +impl ConstraintSet for EcdasConstraints { + fn meta(&self) -> Vec { + let mut m = Vec::with_capacity(200); + // idx 0,1,2: IS_BIT(MU/OP/NEXT_OP), degree 2. + for i in 0..3 { + m.push(ConstraintMeta::base(i, 2)); + } + // idx 3: OP·NEXT_OP, idx 4: NEXT_OP·(1−MU) — degree 2. + m.push(ConstraintMeta::base(3, 2)); + m.push(ConstraintMeta::base(4, 2)); + // Per relation: 64 ConvCarry + 1 ColIsZero. + let mut idx = 5; + for relation in [Relation::Lambda, Relation::Xr, Relation::Yr] { + let conv_degree = match relation { + Relation::Lambda => 3, // op · (λ · Δx) + Relation::Xr | Relation::Yr => 2, + }; + for _ in 0..64 { + m.push(ConstraintMeta::base(idx, conv_degree)); + idx += 1; + } + m.push(ConstraintMeta::base(idx, 1)); // ColIsZero c_63 + idx += 1; + } + debug_assert_eq!(m.len(), 200); + m + } + + fn eval>(&self, b: &mut B) { + // idx 0,1,2: unconditional IS_BIT `x·(1−x)` on [MU, OP, NEXT_OP]. + for (i, col) in [cols::MU, cols::OP, cols::NEXT_OP].into_iter().enumerate() { + let x = b.main(0, col); + let one = b.one(); + b.emit_base(i, x.clone() * (one - x)); + } + + // idx 3: OP · NEXT_OP = 0. + let op = b.main(0, cols::OP); + let next_op = b.main(0, cols::NEXT_OP); + b.emit_base(3, op * next_op); + + // idx 4: NEXT_OP · (1 − MU) = 0. + let next_op = b.main(0, cols::NEXT_OP); + let mu = b.main(0, cols::MU); + let one = b.one(); + b.emit_base(4, next_op * (one - mu)); + + // Per relation: 64 ConvCarry (i=0..64) + 1 ColIsZero(c_63). + let mut idx = 5; + for (relation, c_base) in [ + (Relation::Lambda, cols::C0), + (Relation::Xr, cols::C1), + (Relation::Yr, cols::C2), + ] { + for i in 0..64 { + let root = Self::conv_carry(b, relation, i); + b.emit_base(idx, root); + idx += 1; + } + let c_last = b.main(0, c_base + 63); + b.emit_base(idx, c_last); + idx += 1; + } + } +} diff --git a/prover/src/tables/ecsm.rs b/prover/src/tables/ecsm.rs index f8ec0859d..a9ec75a44 100644 --- a/prover/src/tables/ecsm.rs +++ b/prover/src/tables/ecsm.rs @@ -937,3 +937,236 @@ pub fn create_constraints( (constraints, idx) } + +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// Non-destructive twin of `create_constraints` above, written once against the +// generic `ConstraintBuilder`. The old structs/builder stay as the differential +// oracle; the final deletion phase removes them. Constraint indices 0..148 +// match `create_constraints(0)` exactly: +// 0 : IS_BIT(MU) +// 1..65 : ConvCarry(X2, 0..64) +// 65 : ColIsZero(c0(63)) +// 66..130 : ConvCarry(Yg, 0..64) +// 130 : ColIsZero(c1(63)) +// 131 : IS_BIT(q1(32)) +// 132..139 : CarryBit(KLtN, 0..7) +// 139 : OverflowRequired(KLtN) +// 140..147 : CarryBit(XrLtP, 0..7) +// 147 : OverflowRequired(XrLtP) + +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; + +/// ECSM transition constraints as a single-source [`ConstraintSet`] (148 +/// total). No column configuration needed (the layout is fixed via `cols`). +pub struct EcsmConstraints; + +impl EcsmConstraints { + /// Byte `m` of the base-point order `P` (zero beyond 32 bytes). Twin of + /// [`p_byte`]. + fn p_byte_expr>( + b: &B, + m: usize, + ) -> B::Expr { + if m < 32 { + b.const_base(P_BYTES[m] as u64) + } else { + b.zero() + } + } + + /// `bytes[base + j]` for `j < len`, else zero (the `byte` closure in `s_i`). + fn byte_at>( + b: &B, + base: usize, + len: usize, + j: usize, + ) -> B::Expr { + if j < len { + b.main(0, base + j) + } else { + b.zero() + } + } + + /// `S_i` for `relation` at limb `i` (twin of [`ConvCarry::s_i`]). + fn s_i>( + b: &B, + relation: Relation, + i: usize, + ) -> B::Expr { + let byte = |base: usize, len: usize, j: usize| Self::byte_at(b, base, len, j); + let mut s = b.zero(); + match relation { + Relation::X2 => { + // Σ xG_j·xG_{i-j} − x2_i − Σ q0_j·P_{i-j} + for j in 0..=i { + s = s + byte(cols::XG, 32, j) * byte(cols::XG, 32, i - j); + s = s - byte(cols::Q0, 32, j) * Self::p_byte_expr(b, i - j); + } + s = s - byte(cols::X2, 32, i); + } + Relation::Yg => { + // Σ (yG_j·yG_{i-j} + P_j·P_{i-j} − x2_j·xG_{i-j} − q1_j·P_{i-j}) − b_i + for j in 0..=i { + s = s + byte(cols::YG, 32, j) * byte(cols::YG, 32, i - j); + s = s + Self::p_byte_expr(b, j) * Self::p_byte_expr(b, i - j); + s = s - byte(cols::X2, 32, j) * byte(cols::XG, 32, i - j); + s = s - byte(cols::Q1, 33, j) * Self::p_byte_expr(b, i - j); + } + if i == 0 { + // Only the curve constant `b` is µ-gated (µ·B); B_i = 0 for i ≥ 1. + let mu = b.main(0, cols::MU); + let curve_b = b.const_base(B); + s = s - mu * curve_b; + } + } + } + s + } + + /// `256·c_i − c_{i-1} − S_i` (twin of [`ConvCarry::evaluate`]). + fn conv_carry>( + b: &B, + relation: Relation, + i: usize, + ) -> B::Expr { + let c_base = match relation { + Relation::X2 => cols::C0, + Relation::Yg => cols::C1, + }; + let c_i = b.main(0, c_base + i); + let c_prev = if i == 0 { + b.zero() + } else { + b.main(0, c_base + i - 1) + }; + let two_pow_8 = b.const_base(256); + two_pow_8 * c_i - c_prev - Self::s_i(b, relation, i) + } + + /// The 8 word-carries of the `kind` addition (twin of [`carry_chain`]). + fn carry_chain>( + b: &B, + kind: OverflowKind, + ) -> [B::Expr; 8] { + let hl = kind.addend_hl_base(); + let bl = kind.sum_bl_base(); + let mut c: [B::Expr; 8] = std::array::from_fn(|_| b.zero()); + let mut prev = b.zero(); + for (i, slot) in c.iter_mut().enumerate() { + // addend1 word i (from halfwords): hl[2i] + 2^16·hl[2i+1] + let shift_16 = b.const_base(1u64 << 16); + let addend1 = b.main(0, hl + 2 * i) + b.main(0, hl + 2 * i + 1) * shift_16; + // sum word i (from bytes): Σ bl[4i+b]·2^{8b} + let mut sum = b.zero(); + for byte in 0..4 { + let shift = b.const_base(1u64 << (8 * byte)); + sum = sum + b.main(0, bl + 4 * i + byte) * shift; + } + let addend0 = b.const_base(kind.const_word(i)); + let inv = b.const_base(INV_SHIFT_32); + let ci = (addend0 + addend1 + prev.clone() - sum) * inv; + *slot = ci.clone(); + prev = ci; + } + c + } +} + +impl ConstraintSet for EcsmConstraints { + fn meta(&self) -> Vec { + let mut m = Vec::with_capacity(148); + m.push(ConstraintMeta::base(0, 2)); // IS_BIT(MU) + let mut idx = 1; + // X2 convolution: 64 carries (deg 2) + closing (deg 1). + for _ in 0..64 { + m.push(ConstraintMeta::base(idx, 2)); + idx += 1; + } + m.push(ConstraintMeta::base(idx, 1)); + idx += 1; + // Yg convolution: 64 carries (deg 2) + closing (deg 1). + for _ in 0..64 { + m.push(ConstraintMeta::base(idx, 2)); + idx += 1; + } + m.push(ConstraintMeta::base(idx, 1)); + idx += 1; + // IS_BIT(q1[32]) (deg 2). + m.push(ConstraintMeta::base(idx, 2)); + idx += 1; + // k < N: 7 carry bits (deg 3) + overflow-required (deg 2). + for _ in 0..7 { + m.push(ConstraintMeta::base(idx, 3)); + idx += 1; + } + m.push(ConstraintMeta::base(idx, 2)); + idx += 1; + // xR < p: 7 carry bits (deg 3) + overflow-required (deg 2). + for _ in 0..7 { + m.push(ConstraintMeta::base(idx, 3)); + idx += 1; + } + m.push(ConstraintMeta::base(idx, 2)); + idx += 1; + debug_assert_eq!(idx, 148); + m + } + + fn eval>(&self, b: &mut B) { + // idx 0: IS_BIT(MU): mu·(1−mu). + let mu = b.main(0, cols::MU); + let one = b.one(); + b.emit_base(0, mu.clone() * (one - mu)); + + let mut idx = 1; + + // X2 convolution: 64 carries + closing c0(63). + for i in 0..64 { + let root = Self::conv_carry(b, Relation::X2, i); + b.emit_base(idx, root); + idx += 1; + } + let c0_last = b.main(0, cols::c0(63)); + b.emit_base(idx, c0_last); + idx += 1; + + // Yg convolution: 64 carries + closing c1(63). + for i in 0..64 { + let root = Self::conv_carry(b, Relation::Yg, i); + b.emit_base(idx, root); + idx += 1; + } + let c1_last = b.main(0, cols::c1(63)); + b.emit_base(idx, c1_last); + idx += 1; + + // idx 131: IS_BIT(q1[32]): x·(1−x). + let q1_32 = b.main(0, cols::q1(32)); + let one = b.one(); + b.emit_base(idx, q1_32.clone() * (one - q1_32)); + idx += 1; + + // k < N and xR < p: 7 carry bits + overflow-required each. + for kind in [OverflowKind::KLtN, OverflowKind::XrLtP] { + let c = Self::carry_chain(b, kind); + for ci in c.iter().take(7) { + // µ · c_i · (1 − c_i) + let mu = b.main(0, cols::MU); + let one = b.one(); + b.emit_base(idx, mu * ci.clone() * (one - ci.clone())); + idx += 1; + } + // µ · (1 − c_7) + let mu = b.main(0, cols::MU); + let one = b.one(); + b.emit_base(idx, mu * (one - c[7].clone())); + idx += 1; + } + + debug_assert_eq!(idx, 148); + } +} diff --git a/prover/src/tests/constraint_set_tests_a.rs b/prover/src/tests/constraint_set_tests_a.rs index 5183a5623..d500fbe10 100644 --- a/prover/src/tests/constraint_set_tests_a.rs +++ b/prover/src/tests/constraint_set_tests_a.rs @@ -101,15 +101,27 @@ fn check_set_vs_old( ); } - // --- capture once; tree-measured degree == declared --- + // --- capture once; tree-measured degree <= declared (== old degree()) --- + // + // The declared `meta.degree` reproduces the OLD struct's `degree()` exactly + // (asserted above at the meta-parity loop), which the engine uses as the + // composition-poly degree bound. For most tables that bound is tight, so + // tree-measured == declared. A few constraints (the ecsm/ecdas convolution + // TAILS — `ConvCarry` at large `i`) legitimately have a lower EXACT degree + // than their uniform declared bound: at limb `i` near the top, every + // remaining product has a zeroed (constant) factor, so the surviving + // expression is degree 1 while the struct declares 2 (resp. 3). The + // soundness-relevant invariant is therefore `measured <= declared` (the + // real degree must never EXCEED the bound the composition polynomial is + // sized for) — an over-declaration is safe, an under-declaration is not. let mut cb = CaptureBuilder::::new(); set.eval(&mut cb); let (prog, degrees) = cb.finish(num_base); assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); for &(idx, measured) in °rees { - assert_eq!( - measured, meta[idx].degree, - "[{label}] constraint {idx}: tree degree {measured} != declared {}", + assert!( + measured <= meta[idx].degree, + "[{label}] constraint {idx}: tree degree {measured} EXCEEDS declared {}", meta[idx].degree ); } @@ -184,10 +196,10 @@ fn check_set_vs_old( } // --- 3. capture → flatten → interpret == old evaluate_prover --- - for i in 0..n { + for (i, expected) in old_base.iter().enumerate() { assert_eq!( - eval_program_base(&prog, i, &row), - old_base[i], + &eval_program_base(&prog, i, &row), + expected, "[{label}] interpreter mismatch, constraint {i}, trial {trial}" ); } @@ -211,3 +223,121 @@ mod lt { check_set_vs_old("lt", &LtConstraints, &boxed, cols::NUM_COLUMNS); } } + +// ============================================================================= +// dvrm.rs +// ============================================================================= + +mod dvrm { + use super::*; + use crate::tables::dvrm::{DvrmConstraints, cols, dvrm_constraints}; + use stark::constraints::transition::TransitionConstraint; + + #[test] + fn dvrm_constraint_set_matches_old() { + let (old, next) = dvrm_constraints(0); + assert_eq!(next, old.len()); + let boxed: Vec<_> = old.into_iter().map(|c| c.boxed()).collect(); + check_set_vs_old("dvrm", &DvrmConstraints, &boxed, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// shift.rs +// ============================================================================= + +mod shift { + use super::*; + use crate::tables::shift::{ShiftConstraints, cols, shift_constraints}; + use stark::constraints::transition::TransitionConstraint; + + #[test] + fn shift_constraint_set_matches_old() { + let (old, next) = shift_constraints(0); + assert_eq!(next, old.len()); + let boxed: Vec<_> = old.into_iter().map(|c| c.boxed()).collect(); + check_set_vs_old("shift", &ShiftConstraints, &boxed, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// mul.rs +// ============================================================================= + +mod mul { + use super::*; + use crate::tables::mul::{MulConstraints, cols, mul_constraints}; + use stark::constraints::transition::TransitionConstraint; + + #[test] + fn mul_constraint_set_matches_old() { + let (old, next) = mul_constraints(0); + assert_eq!(next, old.len()); + let boxed: Vec<_> = old.into_iter().map(|c| c.boxed()).collect(); + check_set_vs_old("mul", &MulConstraints, &boxed, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// load.rs +// ============================================================================= + +mod load { + use super::*; + use crate::tables::load::{LoadConstraints, cols, constraints as load_constraints}; + + #[test] + fn load_constraint_set_matches_old() { + // `constraints()` already returns boxed evaluators (idx_start = 0). + let boxed = load_constraints(); + check_set_vs_old("load", &LoadConstraints, &boxed, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// ecsm.rs +// ============================================================================= + +mod ecsm { + use super::*; + use crate::tables::ecsm::{EcsmConstraints, cols, create_constraints}; + + #[test] + fn ecsm_constraint_set_matches_old() { + let (boxed, next) = create_constraints(0); + assert_eq!(next, boxed.len()); + check_set_vs_old("ecsm", &EcsmConstraints, &boxed, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// ecdas.rs +// ============================================================================= + +mod ecdas { + use super::*; + use crate::tables::ecdas::{EcdasConstraints, cols, create_constraints}; + + #[test] + fn ecdas_constraint_set_matches_old() { + let (boxed, next) = create_constraints(0); + assert_eq!(next, boxed.len()); + check_set_vs_old("ecdas", &EcdasConstraints, &boxed, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// ec_scalar.rs +// ============================================================================= + +mod ec_scalar { + use super::*; + use crate::tables::ec_scalar::{EcScalarConstraints, cols, create_constraints}; + + #[test] + fn ec_scalar_constraint_set_matches_old() { + let (boxed, next) = create_constraints(0); + assert_eq!(next, boxed.len()); + check_set_vs_old("ec_scalar", &EcScalarConstraints, &boxed, cols::NUM_COLUMNS); + } +} From 1bb9ff3148de2d9d79bc9e8dea47228b1b763833 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:18:28 -0300 Subject: [PATCH 19/52] spike(stark): use iterator comparisons in constraint_set_tests_b (clippy) --- prover/src/tests/constraint_set_tests_b.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index 4d11ff0ed..8be0da6b2 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -170,9 +170,9 @@ fn check_table>(label: &str, old: &OldVec, set: &CS, let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); set.eval(&mut folder); folder.assert_all_emitted(); - for i in 0..n { + for (i, (got, want)) in base_out.iter().zip(old_base.iter()).enumerate() { assert_eq!( - base_out[i], old_base[i], + got, want, "[{label}] prover folder mismatch, constraint {i}, trial {trial}" ); } @@ -192,18 +192,18 @@ fn check_table>(label: &str, old: &OldVec, set: &CS, let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); set.eval(&mut vfolder); vfolder.assert_all_emitted(); - for i in 0..n { + for (i, (got, want)) in vext_out.iter().zip(old_ext_v.iter()).enumerate() { assert_eq!( - vext_out[i], old_ext_v[i], + got, want, "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" ); } // --- 3. capture → flatten → interpret == old evaluate_prover (base) --- - for i in 0..n { + for (i, want) in old_base.iter().enumerate() { assert_eq!( - eval_program_base(&prog, i, &row), - old_base[i], + &eval_program_base(&prog, i, &row), + want, "[{label}] interpreter mismatch, constraint {i}, trial {trial}" ); } From f52a0280d7110f36ad92296f78b474c461a33571 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 00:48:52 -0300 Subject: [PATCH 20/52] stark: delete the bit_flags and simple_periodic_cols example AIRs These two examples were the sole users of sub-row/virtual-column reads and periodic columns, both abandoned features. The framework machinery (Op::Periodic, periodic zerofier paths) stays; it is removed together with the engine machinery in a later phase. --- crypto/stark/src/examples/bit_flags.rs | 203 ------------------ crypto/stark/src/examples/mod.rs | 2 - .../src/examples/simple_periodic_cols.rs | 194 ----------------- crypto/stark/src/tests/air_tests.rs | 104 --------- 4 files changed, 503 deletions(-) delete mode 100644 crypto/stark/src/examples/bit_flags.rs delete mode 100644 crypto/stark/src/examples/simple_periodic_cols.rs diff --git a/crypto/stark/src/examples/bit_flags.rs b/crypto/stark/src/examples/bit_flags.rs deleted file mode 100644 index 9b83ba6d3..000000000 --- a/crypto/stark/src/examples/bit_flags.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::{ - constraints::{boundary::BoundaryConstraints, transition::TransitionConstraintEvaluator}, - context::AirContext, - proof::options::ProofOptions, - trace::TraceTable, - traits::{AIR, TransitionEvaluationContext}, -}; -use math::field::{element::FieldElement, goldilocks::GoldilocksField}; - -type StarkField = GoldilocksField; -type Felt = FieldElement; - -#[derive(Clone)] -pub struct BitConstraint; -impl BitConstraint { - fn new() -> Self { - Self - } -} - -impl TransitionConstraintEvaluator for BitConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn exemptions_period(&self) -> Option { - Some(16) - } - - fn periodic_exemptions_offset(&self) -> Option { - Some(15) - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let step = frame.get_evaluation_step(0); - - let prefix_flag = step.get_main_evaluation_element(0, 0); - let next_prefix_flag = step.get_main_evaluation_element(1, 0); - - let two = Felt::from(2); - let one = Felt::one(); - let bit_flag = prefix_flag - two * next_prefix_flag; - - let bit_constraint = bit_flag * (bit_flag - one); - - transition_evaluations[self.constraint_idx()] = bit_constraint; - } -} - -#[derive(Clone)] -pub struct ZeroFlagConstraint; -impl ZeroFlagConstraint { - fn new() -> Self { - Self - } -} - -impl TransitionConstraintEvaluator for ZeroFlagConstraint { - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 1 - } - - fn period(&self) -> usize { - 16 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let step = frame.get_evaluation_step(0); - let zero_flag = step.get_main_evaluation_element(15, 0); - - transition_evaluations[self.constraint_idx()] = *zero_flag; - } -} - -pub struct BitFlagsAIR { - context: AirContext, - constraints: Vec>>, -} - -impl AIR for BitFlagsAIR { - type Field = StarkField; - type FieldExtension = StarkField; - type PublicInputs = (); - - fn step_size(&self) -> usize { - 16 - } - - fn new(proof_options: &ProofOptions) -> Self { - let bit_constraint = Box::new(BitConstraint::new()); - let flag_constraint = Box::new(ZeroFlagConstraint::new()); - let constraints: Vec< - Box>, - > = vec![bit_constraint, flag_constraint]; - - let num_transition_constraints = constraints.len(); - - let context = AirContext { - proof_options: proof_options.clone(), - trace_columns: 2, - transition_offsets: vec![0], - num_transition_constraints, - }; - - Self { - context, - constraints, - } - } - - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.constraints - } - - fn boundary_constraints( - &self, - _pub_inputs: &Self::PublicInputs, - _rap_challenges: &[FieldElement], - _bus_public_inputs: Option<&crate::lookup::BusPublicInputs>, - _trace_length: usize, - ) -> BoundaryConstraints { - BoundaryConstraints::from_constraints(vec![]) - } - - fn context(&self) -> &AirContext { - &self.context - } - - fn composition_poly_degree_bound(&self, trace_length: usize) -> usize { - trace_length * 2 - } - - fn trace_layout(&self) -> (usize, usize) { - (1, 0) - } -} - -pub fn bit_prefix_flag_trace(num_steps: usize) -> TraceTable { - debug_assert!(num_steps.is_power_of_two()); - let step: Vec = [ - 1031u64, 515, 257, 128, 64, 32, 16, 8, 4, 2, 1, 0, 0, 0, 0, 0, - ] - .iter() - .map(|t| Felt::from(*t)) - .collect(); - - let mut data: Vec = std::iter::repeat_n(step, num_steps).flatten().collect(); - data[0] = Felt::from(1030); - - let mut dummy_column = (0..16).map(Felt::from).collect(); - dummy_column = std::iter::repeat_n(dummy_column, num_steps) - .flatten() - .collect(); - TraceTable::from_columns_main(vec![data, dummy_column], 16) -} diff --git a/crypto/stark/src/examples/mod.rs b/crypto/stark/src/examples/mod.rs index 524de4a1d..770540e83 100644 --- a/crypto/stark/src/examples/mod.rs +++ b/crypto/stark/src/examples/mod.rs @@ -1,4 +1,3 @@ -pub mod bit_flags; pub mod dummy_air; pub mod fibonacci_2_cols_shifted; pub mod fibonacci_2_columns; @@ -10,4 +9,3 @@ pub mod read_only_memory; pub mod read_only_memory_logup; pub mod simple_addition; pub mod simple_fibonacci; -pub mod simple_periodic_cols; diff --git a/crypto/stark/src/examples/simple_periodic_cols.rs b/crypto/stark/src/examples/simple_periodic_cols.rs deleted file mode 100644 index 70f5da3b4..000000000 --- a/crypto/stark/src/examples/simple_periodic_cols.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::marker::PhantomData; - -use crate::{ - constraints::{ - boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, - }, - context::AirContext, - proof::options::ProofOptions, - trace::TraceTable, - traits::{AIR, TransitionEvaluationContext}, -}; -use math::field::{element::FieldElement, traits::IsFFTField}; - -pub struct PeriodicConstraint { - phantom: PhantomData, -} -impl PeriodicConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} -impl Default for PeriodicConstraint { - fn default() -> Self { - Self::new() - } -} - -impl TransitionConstraintEvaluator for PeriodicConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - 2 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - let third_step = frame.get_evaluation_step(2); - - let a0 = first_step.get_main_evaluation_element(0, 0); - let a1 = second_step.get_main_evaluation_element(0, 0); - let a2 = third_step.get_main_evaluation_element(0, 0); - - let s = &periodic_values[0]; - - transition_evaluations[self.constraint_idx()] = s * (a2 - a1 - a0); - } -} - -/// A sequence that uses periodic columns. It has two columns -/// - C1: at each step adds the last two values or does -/// nothing depending on C2. -/// - C2: it is a binary column that cycles around [0, 1] -/// -/// C1 | C2 -/// 1 | 0 Boundary col1 = 1 -/// 1 | 1 Boundary col1 = 1 -/// 1 | 0 Does nothing -/// 2 | 1 Adds 1 + 1 -/// 2 | 0 Does nothing -/// 4 | 1 Adds 2 + 2 -/// 4 | 0 ... -/// 8 | 1 -pub struct SimplePeriodicAIR -where - F: IsFFTField, -{ - context: AirContext, - transition_constraints: Vec>>, -} - -#[derive(Clone, Debug)] -pub struct SimplePeriodicPublicInputs -where - F: IsFFTField, -{ - pub a0: FieldElement, - pub a1: FieldElement, -} - -impl AIR for SimplePeriodicAIR -where - F: IsFFTField + Send + Sync + 'static, -{ - type Field = F; - type FieldExtension = F; - type PublicInputs = SimplePeriodicPublicInputs; - - fn step_size(&self) -> usize { - 1 - } - - fn new(proof_options: &ProofOptions) -> Self { - let transition_constraints: Vec< - Box>, - > = vec![Box::new(PeriodicConstraint::new())]; - - let context = AirContext { - proof_options: proof_options.clone(), - trace_columns: 1, - transition_offsets: vec![0, 1, 2], - num_transition_constraints: transition_constraints.len(), - }; - - Self { - context, - transition_constraints, - } - } - - fn composition_poly_degree_bound(&self, trace_length: usize) -> usize { - trace_length - } - - fn boundary_constraints( - &self, - pub_inputs: &Self::PublicInputs, - _rap_challenges: &[FieldElement], - _bus_public_inputs: Option<&crate::lookup::BusPublicInputs>, - trace_length: usize, - ) -> BoundaryConstraints { - let a0 = BoundaryConstraint::new_simple_main(0, pub_inputs.a0.clone()); - let a1 = BoundaryConstraint::new_simple_main(trace_length - 1, pub_inputs.a1.clone()); - - BoundaryConstraints::from_constraints(vec![a0, a1]) - } - - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.transition_constraints - } - - fn get_periodic_column_values(&self) -> Vec>> { - vec![vec![FieldElement::zero(), FieldElement::one()]] - } - - fn context(&self) -> &AirContext { - &self.context - } - - fn trace_layout(&self) -> (usize, usize) { - (1, 0) - } -} - -pub fn simple_periodic_trace(trace_length: usize) -> TraceTable { - let mut ret: Vec> = vec![]; - - ret.push(FieldElement::one()); - ret.push(FieldElement::one()); - ret.push(FieldElement::one()); - - let mut accum = FieldElement::from(2); - while ret.len() < trace_length - 1 { - ret.push(accum.clone()); - ret.push(accum.clone()); - accum = &accum + &accum; - } - ret.push(accum); - - TraceTable::from_columns_main(vec![ret], 1) -} diff --git a/crypto/stark/src/tests/air_tests.rs b/crypto/stark/src/tests/air_tests.rs index 8e20f303e..d18e18c7d 100644 --- a/crypto/stark/src/tests/air_tests.rs +++ b/crypto/stark/src/tests/air_tests.rs @@ -9,7 +9,6 @@ use math::field::{ use crate::traits::AIR; use crate::{ examples::{ - bit_flags::{self, BitFlagsAIR}, dummy_air::{self, DummyAIR}, fibonacci_2_cols_shifted::{self, Fibonacci2ColsShifted}, fibonacci_2_columns::{self, Fibonacci2ColsAIR}, @@ -18,7 +17,6 @@ use crate::{ quadratic_air::{self, QuadraticAIR, QuadraticPublicInputs}, read_only_memory::{ReadOnlyPublicInputs, ReadOnlyRAP, sort_rap_trace}, simple_fibonacci::{self, FibonacciAIR, FibonacciPublicInputs}, - simple_periodic_cols::{self, SimplePeriodicAIR, SimplePeriodicPublicInputs}, // simple_periodic_cols::{self, SimplePeriodicAIR, SimplePeriodicPublicInputs}, }, proof::options::ProofOptions, prover::{IsStarkProver, Prover}, @@ -60,61 +58,6 @@ fn test_prove_fib() { )); } -#[test_log::test] -fn test_prove_simple_periodic_8() { - let mut trace = simple_periodic_cols::simple_periodic_trace::(8); - - let proof_options = ProofOptions::default_test_options(); - - let pub_inputs = SimplePeriodicPublicInputs { - a0: Felt::one(), - a1: Felt::from(8), - }; - - let air = SimplePeriodicAIR::::new(&proof_options); - - let proof = Prover::prove( - &air, - &mut trace, - &pub_inputs, - &mut DefaultTranscript::::new(&[]), - ) - .unwrap(); - assert!(Verifier::verify( - &proof, - &air, - &mut DefaultTranscript::::new(&[]), - )); -} - -#[test_log::test] -fn test_prove_simple_periodic_32() { - let mut trace = simple_periodic_cols::simple_periodic_trace::(32); - - let proof_options = ProofOptions::default_test_options(); - - let pub_inputs = SimplePeriodicPublicInputs { - a0: Felt::one(), - a1: Felt::from(32768), - }; - - let air = SimplePeriodicAIR::::new(&proof_options); - - let proof = Prover::prove( - &air, - &mut trace, - &pub_inputs, - &mut DefaultTranscript::::new(&[]), - ) - .unwrap(); - - assert!(Verifier::verify( - &proof, - &air, - &mut DefaultTranscript::::new(&[]), - )); -} - #[test_log::test] fn test_prove_fib_2_cols() { let mut trace = fibonacci_2_columns::compute_trace([Felt::from(1), Felt::from(1)], 16); @@ -246,23 +189,6 @@ fn test_prove_dummy() { )); } -#[test_log::test] -fn test_prove_bit_flags() { - let mut trace = bit_flags::bit_prefix_flag_trace(32); - let proof_options = ProofOptions::default_test_options(); - - let air = BitFlagsAIR::new(&proof_options); - - let proof = - Prover::prove(&air, &mut trace, &(), &mut DefaultTranscript::::new(&[])).unwrap(); - - assert!(Verifier::verify( - &proof, - &air, - &mut DefaultTranscript::::new(&[]), - )); -} - #[test_log::test] fn test_prove_read_only_memory() { let address_col = vec![ @@ -523,36 +449,6 @@ fn test_multi_prove_2_tables_small_field() { )); } -#[test_log::test] -fn test_multi_prove_different_airs() { - let mut trace_1 = dummy_air::dummy_trace(16); - let mut trace_2 = bit_flags::bit_prefix_flag_trace(32); - let proof_options = ProofOptions::default_test_options(); - - let air_1 = DummyAIR::new(&proof_options); - let air_2 = BitFlagsAIR::new(&proof_options); - - let air_trace_pairs: Vec<( - &dyn AIR, - &mut _, - &_, - )> = vec![(&air_1, &mut trace_1, &()), (&air_2, &mut trace_2, &())]; - - let multi_proof = - multi_prove_ram(air_trace_pairs, &mut DefaultTranscript::::new(&[])).unwrap(); - - let airs: Vec< - &dyn AIR, - > = vec![&air_1, &air_2]; - - assert!(Verifier::multi_verify( - &airs, - &multi_proof, - &mut DefaultTranscript::::new(&[]), - &FieldElement::zero(), - )); -} - // Type aliases for multi-column Fibonacci tests type GoldilocksExt = Degree3GoldilocksExtensionField; type GoldilocksFE = FieldElement; From 88adbfa64c4d18aa853fd3a84464b3fc8b5a0996 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 01:00:04 -0300 Subject: [PATCH 21/52] stark: add examples-cli prove/verify binary over the example AIRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cargo example target (requires the test-utils feature) that proves and verifies each example AIR with bincode-serialized proofs, mirroring bin/cli's VM-proof format. Trace sizes and public inputs mirror the existing stark tests, so a proof produced by one version of the constraint system can be checked by another — this binary is the old-verifier side of scripts/cross_verify_examples.sh. Public-input structs gain serde derives (with an explicit FieldElement bound) so the proofs, which embed them, can round-trip. --- crypto/stark/Cargo.toml | 4 + crypto/stark/examples/examples_cli.rs | 707 ++++++++++++++++++ .../src/examples/fibonacci_2_cols_shifted.rs | 3 +- .../src/examples/fibonacci_multi_column.rs | 1 + crypto/stark/src/examples/fibonacci_rap.rs | 3 +- crypto/stark/src/examples/quadratic_air.rs | 3 +- crypto/stark/src/examples/read_only_memory.rs | 3 +- .../src/examples/read_only_memory_logup.rs | 3 +- crypto/stark/src/examples/simple_addition.rs | 3 +- crypto/stark/src/examples/simple_fibonacci.rs | 3 +- 10 files changed, 726 insertions(+), 7 deletions(-) create mode 100644 crypto/stark/examples/examples_cli.rs diff --git a/crypto/stark/Cargo.toml b/crypto/stark/Cargo.toml index 8060b80f3..7787804c7 100644 --- a/crypto/stark/Cargo.toml +++ b/crypto/stark/Cargo.toml @@ -78,6 +78,10 @@ dwarf-debug-info = false # Should we omit the default import path omit-default-module-path = false +[[example]] +name = "examples_cli" +required-features = ["test-utils"] + [[bench]] name = "prover_benchmark" harness = false diff --git a/crypto/stark/examples/examples_cli.rs b/crypto/stark/examples/examples_cli.rs new file mode 100644 index 000000000..7be1d673c --- /dev/null +++ b/crypto/stark/examples/examples_cli.rs @@ -0,0 +1,707 @@ +//! Prove/verify CLI over the stark example AIRs, for cross-version +//! verification of the constraint system (see +//! `scripts/cross_verify_examples.sh`). +//! +//! Usage: +//! examples_cli prove -o +//! examples_cli verify +//! +//! Proofs are bincode-serialized, mirroring `bin/cli`'s VM-proof format. +//! Trace sizes and public inputs mirror the existing stark tests +//! (`tests/air_tests.rs`, `tests/small_trace_tests.rs`, +//! `tests/bus_tests/completeness_tests.rs`) so a proof produced by one +//! version of the constraint system can be checked by another. +//! +//! Exit code 0 = success (prove written / verify accepted); nonzero = failure. + +use std::path::PathBuf; +use std::process::ExitCode; + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use math::field::{ + element::FieldElement, extensions_goldilocks::Degree3GoldilocksExtensionField, + goldilocks::GoldilocksField, +}; + +use stark::examples::{ + dummy_air::{self, DummyAIR}, + fibonacci_2_cols_shifted::{self, Fibonacci2ColsShifted}, + fibonacci_2_columns::{self, Fibonacci2ColsAIR}, + fibonacci_multi_column::{self, FibonacciMultiColumnAIR, FibonacciMultiColumnPublicInputs}, + fibonacci_rap::{FibonacciRAP, FibonacciRAPPublicInputs, fibonacci_rap_trace}, + multi_table_lookup::{ + new_add_air_with_lookup, new_cpu_air_with_lookup, new_mul_air_with_lookup, + }, + quadratic_air::{self, QuadraticAIR, QuadraticPublicInputs}, + read_only_memory::{ReadOnlyPublicInputs, ReadOnlyRAP, sort_rap_trace}, + read_only_memory_logup::{LogReadOnlyPublicInputs, LogReadOnlyRAP, read_only_logup_trace}, + simple_addition::{SimpleAdditionAIR, SimpleAdditionPublicInputs, simple_addition_trace}, + simple_fibonacci::{self, FibonacciAIR, FibonacciPublicInputs}, +}; +use stark::proof::options::ProofOptions; +use stark::proof::stark::{MultiProof, StarkProof}; +use stark::prover::{IsStarkProver, Prover}; +use stark::trace::TraceTable; +use stark::traits::AIR; +use stark::verifier::{IsStarkVerifier, Verifier}; + +type Gl = GoldilocksField; +type Gl3 = Degree3GoldilocksExtensionField; +type Felt = FieldElement; + +const EXAMPLES: &[&str] = &[ + "simple_fibonacci", + "fibonacci_2_columns", + "fibonacci_2_cols_shifted", + "fibonacci_multi_column", + "quadratic_air", + "fibonacci_rap", + "dummy_air", + "simple_addition", + "read_only_memory", + "read_only_memory_logup", + "multi_table_lookup", +]; + +fn ser(proof: &T) -> Result, String> { + bincode::serialize(proof).map_err(|e| format!("failed to serialize proof: {e}")) +} + +fn de(bytes: &[u8]) -> Result { + bincode::deserialize(bytes).map_err(|e| format!("failed to deserialize proof: {e}")) +} + +// ============================================================================= +// simple_fibonacci — mirrors air_tests::test_prove_fib +// ============================================================================= + +fn prove_simple_fibonacci() -> Result, String> { + let mut trace = simple_fibonacci::fibonacci_trace([Felt::from(1), Felt::from(1)], 8); + let pub_inputs = FibonacciPublicInputs { + a0: Felt::one(), + a1: Felt::one(), + }; + let air = FibonacciAIR::::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_simple_fibonacci(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = FibonacciAIR::::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// fibonacci_2_columns — mirrors air_tests::test_prove_fib_2_cols +// ============================================================================= + +fn prove_fibonacci_2_columns() -> Result, String> { + let mut trace = fibonacci_2_columns::compute_trace([Felt::from(1), Felt::from(1)], 16); + let pub_inputs = FibonacciPublicInputs { + a0: Felt::one(), + a1: Felt::one(), + }; + let air = Fibonacci2ColsAIR::::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_fibonacci_2_columns(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = Fibonacci2ColsAIR::::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// fibonacci_2_cols_shifted — mirrors air_tests::test_prove_fib_2_cols_shifted +// ============================================================================= + +fn prove_fibonacci_2_cols_shifted() -> Result, String> { + let mut trace = fibonacci_2_cols_shifted::compute_trace(FieldElement::one(), 16); + let claimed_index = 14; + let claimed_value = trace.main_table.get_row(claimed_index)[0]; + let pub_inputs = fibonacci_2_cols_shifted::PublicInputs { + claimed_value, + claimed_index, + }; + let air = Fibonacci2ColsShifted::::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_fibonacci_2_cols_shifted(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = Fibonacci2ColsShifted::::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// fibonacci_multi_column — mirrors air_tests::test_multi_column_fibonacci_2_cols +// ============================================================================= + +fn multi_column_initial_values() -> Vec<(Felt, Felt)> { + (0..2u64) + .map(|i| (Felt::from(i + 1), Felt::from(i + 2))) + .collect() +} + +fn prove_fibonacci_multi_column() -> Result, String> { + let initial_values = multi_column_initial_values(); + let mut trace = fibonacci_multi_column::compute_trace::(&initial_values, 16); + let pub_inputs = fibonacci_multi_column::create_public_inputs(initial_values); + let air = FibonacciMultiColumnAIR::::with_num_columns( + &ProofOptions::default_test_options(), + 2, + ); + let proof = Prover::::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_fibonacci_multi_column(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = FibonacciMultiColumnAIR::::with_num_columns( + &ProofOptions::default_test_options(), + 2, + ); + Ok(Verifier::::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// quadratic_air — mirrors air_tests::test_prove_quadratic +// ============================================================================= + +fn prove_quadratic_air() -> Result, String> { + let mut trace = quadratic_air::quadratic_trace(Felt::from(3), 32); + let pub_inputs = QuadraticPublicInputs { a0: Felt::from(3) }; + let air = QuadraticAIR::::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_quadratic_air(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = QuadraticAIR::::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// fibonacci_rap — mirrors air_tests::test_prove_rap_fib +// ============================================================================= + +fn prove_fibonacci_rap() -> Result, String> { + let steps = 16; + let mut trace = fibonacci_rap_trace([Felt::from(1), Felt::from(1)], steps); + let pub_inputs = FibonacciRAPPublicInputs { + steps, + a0: Felt::one(), + a1: Felt::one(), + }; + let air = FibonacciRAP::::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_fibonacci_rap(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = FibonacciRAP::::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// dummy_air — mirrors air_tests::test_prove_dummy +// ============================================================================= + +fn prove_dummy_air() -> Result, String> { + let mut trace = dummy_air::dummy_trace(16); + let air = DummyAIR::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &(), + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_dummy_air(bytes: &[u8]) -> Result { + let proof: StarkProof = de(bytes)?; + let air = DummyAIR::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// simple_addition — mirrors small_trace_tests::test_prove_verify_single_row +// ============================================================================= + +fn prove_simple_addition() -> Result, String> { + let mut trace = simple_addition_trace::(1); + let pub_inputs = SimpleAdditionPublicInputs { + a: Felt::from(1u64), + b: Felt::from(2u64), + }; + let air = SimpleAdditionAIR::::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_simple_addition(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = SimpleAdditionAIR::::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// read_only_memory — mirrors air_tests::test_prove_read_only_memory +// ============================================================================= + +fn read_only_memory_columns() -> (Vec, Vec) { + let address_col = vec![ + Felt::from(3), // a0 + Felt::from(2), // a1 + Felt::from(2), // a2 + Felt::from(3), // a3 + Felt::from(4), // a4 + Felt::from(5), // a5 + Felt::from(1), // a6 + Felt::from(3), // a7 + ]; + let value_col = vec![ + Felt::from(10), // v0 + Felt::from(5), // v1 + Felt::from(5), // v2 + Felt::from(10), // v3 + Felt::from(25), // v4 + Felt::from(25), // v5 + Felt::from(7), // v6 + Felt::from(10), // v7 + ]; + (address_col, value_col) +} + +fn prove_read_only_memory() -> Result, String> { + let (address_col, value_col) = read_only_memory_columns(); + let pub_inputs = ReadOnlyPublicInputs { + a0: Felt::from(3), + v0: Felt::from(10), + a_sorted0: Felt::from(1), // a6 + v_sorted0: Felt::from(7), // v6 + }; + let mut trace = sort_rap_trace(address_col, value_col); + let air = ReadOnlyRAP::::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_read_only_memory(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = ReadOnlyRAP::::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// read_only_memory_logup — mirrors air_tests::test_prove_log_read_only_memory +// ============================================================================= + +fn read_only_memory_logup_columns() -> (Vec, Vec) { + let address_col = vec![ + Felt::from(3), // a0 + Felt::from(2), // a1 + Felt::from(2), // a2 + Felt::from(3), // a3 + Felt::from(4), // a4 + Felt::from(5), // a5 + Felt::from(1), // a6 + Felt::from(3), // a7 + ]; + let value_col = vec![ + Felt::from(30), // v0 + Felt::from(20), // v1 + Felt::from(20), // v2 + Felt::from(30), // v3 + Felt::from(40), // v4 + Felt::from(50), // v5 + Felt::from(10), // v6 + Felt::from(30), // v7 + ]; + (address_col, value_col) +} + +fn prove_read_only_memory_logup() -> Result, String> { + let (address_col, value_col) = read_only_memory_logup_columns(); + let pub_inputs = LogReadOnlyPublicInputs { + a0: Felt::from(3), + v0: Felt::from(30), + a_sorted_0: Felt::from(1), + v_sorted_0: Felt::from(10), + m0: Felt::from(1), + }; + let mut trace = read_only_logup_trace(address_col, value_col); + let air = LogReadOnlyRAP::::new(&ProofOptions::default_test_options()); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&proof) +} + +fn verify_read_only_memory_logup(bytes: &[u8]) -> Result { + let proof: StarkProof> = de(bytes)?; + let air = LogReadOnlyRAP::::new(&ProofOptions::default_test_options()); + Ok(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )) +} + +// ============================================================================= +// multi_table_lookup — mirrors bus_tests::completeness_tests::test_multi_table_proof +// ============================================================================= + +fn multi_table_traces() -> ( + TraceTable, + TraceTable, + TraceTable, +) { + // CPU Trace (8 rows): dispatches operations to ADD and MUL tables + let add_column = vec![ + Felt::one(), + Felt::zero(), + Felt::one(), + Felt::zero(), + Felt::one(), + Felt::one(), + Felt::zero(), + Felt::zero(), + ]; + let mul_column = vec![ + Felt::zero(), + Felt::one(), + Felt::zero(), + Felt::one(), + Felt::zero(), + Felt::zero(), + Felt::one(), + Felt::one(), + ]; + let a_column = vec![ + Felt::from(1), + Felt::from(2), + Felt::from(3), + Felt::from(4), + Felt::from(5), + Felt::from(6), + Felt::from(7), + Felt::from(8), + ]; + let b_column = vec![ + Felt::from(10), + Felt::from(20), + Felt::from(30), + Felt::from(40), + Felt::from(50), + Felt::from(60), + Felt::from(70), + Felt::from(80), + ]; + let c_column = vec![ + Felt::from(11), // 1 + 10 + Felt::from(40), // 2 * 20 + Felt::from(33), // 3 + 30 + Felt::from(160), // 4 * 40 + Felt::from(55), // 5 + 50 + Felt::from(66), // 6 + 60 + Felt::from(490), // 7 * 70 + Felt::from(640), // 8 * 80 + ]; + let cpu_trace = TraceTable::from_columns_main( + vec![add_column, mul_column, a_column, b_column, c_column], + 1, + ); + + // ADD Trace (4 rows): receives addition operations + let add_trace = TraceTable::from_columns_main( + vec![ + vec![Felt::from(1), Felt::from(3), Felt::from(5), Felt::from(6)], + vec![ + Felt::from(10), + Felt::from(30), + Felt::from(50), + Felt::from(60), + ], + vec![ + Felt::from(11), + Felt::from(33), + Felt::from(55), + Felt::from(66), + ], + vec![Felt::one(), Felt::one(), Felt::one(), Felt::one()], + ], + 1, + ); + + // MUL Trace (4 rows): receives multiplication operations + let mul_trace = TraceTable::from_columns_main( + vec![ + vec![Felt::from(2), Felt::from(4), Felt::from(7), Felt::from(8)], + vec![ + Felt::from(20), + Felt::from(40), + Felt::from(70), + Felt::from(80), + ], + vec![ + Felt::from(40), + Felt::from(160), + Felt::from(490), + Felt::from(640), + ], + vec![Felt::one(), Felt::one(), Felt::one(), Felt::one()], + ], + 1, + ); + + (cpu_trace, add_trace, mul_trace) +} + +fn prove_multi_table_lookup() -> Result, String> { + let (mut cpu_trace, mut add_trace, mut mul_trace) = multi_table_traces(); + let proof_options = ProofOptions::default_test_options(); + let cpu_air = new_cpu_air_with_lookup(&proof_options); + let add_air = new_add_air_with_lookup(&proof_options); + let mul_air = new_mul_air_with_lookup(&proof_options); + + let air_trace_pairs: Vec<( + &dyn AIR, + _, + _, + )> = vec![ + (&cpu_air, &mut cpu_trace, &()), + (&add_air, &mut add_trace, &()), + (&mul_air, &mut mul_trace, &()), + ]; + + let multi_proof = Prover::::multi_prove( + air_trace_pairs, + &mut DefaultTranscript::::new(&[]), + ) + .map_err(|e| format!("prove failed: {e:?}"))?; + ser(&multi_proof) +} + +fn verify_multi_table_lookup(bytes: &[u8]) -> Result { + let multi_proof: MultiProof = de(bytes)?; + let proof_options = ProofOptions::default_test_options(); + let cpu_air = new_cpu_air_with_lookup(&proof_options); + let add_air = new_add_air_with_lookup(&proof_options); + let mul_air = new_mul_air_with_lookup(&proof_options); + let airs: Vec<&dyn AIR> = + vec![&cpu_air, &add_air, &mul_air]; + Ok(Verifier::multi_verify( + &airs, + &multi_proof, + &mut DefaultTranscript::::new(&[]), + &FieldElement::zero(), + )) +} + +// ============================================================================= +// Dispatch + main +// ============================================================================= + +fn prove_example(name: &str) -> Result, String> { + match name { + "simple_fibonacci" => prove_simple_fibonacci(), + "fibonacci_2_columns" => prove_fibonacci_2_columns(), + "fibonacci_2_cols_shifted" => prove_fibonacci_2_cols_shifted(), + "fibonacci_multi_column" => prove_fibonacci_multi_column(), + "quadratic_air" => prove_quadratic_air(), + "fibonacci_rap" => prove_fibonacci_rap(), + "dummy_air" => prove_dummy_air(), + "simple_addition" => prove_simple_addition(), + "read_only_memory" => prove_read_only_memory(), + "read_only_memory_logup" => prove_read_only_memory_logup(), + "multi_table_lookup" => prove_multi_table_lookup(), + _ => Err(format!( + "unknown example '{name}'; available: {}", + EXAMPLES.join(", ") + )), + } +} + +fn verify_example(name: &str, bytes: &[u8]) -> Result { + match name { + "simple_fibonacci" => verify_simple_fibonacci(bytes), + "fibonacci_2_columns" => verify_fibonacci_2_columns(bytes), + "fibonacci_2_cols_shifted" => verify_fibonacci_2_cols_shifted(bytes), + "fibonacci_multi_column" => verify_fibonacci_multi_column(bytes), + "quadratic_air" => verify_quadratic_air(bytes), + "fibonacci_rap" => verify_fibonacci_rap(bytes), + "dummy_air" => verify_dummy_air(bytes), + "simple_addition" => verify_simple_addition(bytes), + "read_only_memory" => verify_read_only_memory(bytes), + "read_only_memory_logup" => verify_read_only_memory_logup(bytes), + "multi_table_lookup" => verify_multi_table_lookup(bytes), + _ => Err(format!( + "unknown example '{name}'; available: {}", + EXAMPLES.join(", ") + )), + } +} + +fn usage() -> ExitCode { + eprintln!("Usage:"); + eprintln!(" examples_cli prove -o "); + eprintln!(" examples_cli verify "); + eprintln!("Examples: {}", EXAMPLES.join(", ")); + ExitCode::FAILURE +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().collect(); + match args.get(1).map(String::as_str) { + Some("prove") => { + let (Some(name), Some(flag), Some(out)) = (args.get(2), args.get(3), args.get(4)) + else { + return usage(); + }; + if flag != "-o" { + return usage(); + } + let out = PathBuf::from(out); + match prove_example(name) { + Ok(bytes) => { + if let Err(e) = std::fs::write(&out, &bytes) { + eprintln!("failed to write proof to {out:?}: {e}"); + return ExitCode::FAILURE; + } + eprintln!( + "proof for '{name}' written to {out:?} ({} bytes)", + bytes.len() + ); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("{e}"); + ExitCode::FAILURE + } + } + } + Some("verify") => { + let (Some(name), Some(path)) = (args.get(2), args.get(3)) else { + return usage(); + }; + let bytes = match std::fs::read(path) { + Ok(b) => b, + Err(e) => { + eprintln!("failed to read proof file {path}: {e}"); + return ExitCode::FAILURE; + } + }; + match verify_example(name, &bytes) { + Ok(true) => { + eprintln!("verification succeeded for '{name}'"); + ExitCode::SUCCESS + } + Ok(false) => { + eprintln!("verification FAILED for '{name}'"); + ExitCode::FAILURE + } + Err(e) => { + eprintln!("{e}"); + ExitCode::FAILURE + } + } + } + _ => usage(), + } +} diff --git a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs index 76c8ea11f..bb0679907 100644 --- a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs +++ b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs @@ -137,7 +137,8 @@ where } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct PublicInputs where F: IsFFTField, diff --git a/crypto/stark/src/examples/fibonacci_multi_column.rs b/crypto/stark/src/examples/fibonacci_multi_column.rs index ac6069ece..ce8cd2aff 100644 --- a/crypto/stark/src/examples/fibonacci_multi_column.rs +++ b/crypto/stark/src/examples/fibonacci_multi_column.rs @@ -114,6 +114,7 @@ where /// Public inputs for the multi-column Fibonacci AIR. /// Contains the initial values (first two elements) for each column. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct FibonacciMultiColumnPublicInputs { /// Initial values for each column: (a0, a1) pairs pub initial_values: Vec<(FieldElement, FieldElement)>, diff --git a/crypto/stark/src/examples/fibonacci_rap.rs b/crypto/stark/src/examples/fibonacci_rap.rs index 10f1827d2..25d1acefe 100644 --- a/crypto/stark/src/examples/fibonacci_rap.rs +++ b/crypto/stark/src/examples/fibonacci_rap.rs @@ -164,7 +164,8 @@ where transition_constraints: Vec>>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct FibonacciRAPPublicInputs where F: IsFFTField, diff --git a/crypto/stark/src/examples/quadratic_air.rs b/crypto/stark/src/examples/quadratic_air.rs index d49b0050d..9bf4b968c 100644 --- a/crypto/stark/src/examples/quadratic_air.rs +++ b/crypto/stark/src/examples/quadratic_air.rs @@ -81,7 +81,8 @@ where constraints: Vec>>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct QuadraticPublicInputs where F: IsFFTField, diff --git a/crypto/stark/src/examples/read_only_memory.rs b/crypto/stark/src/examples/read_only_memory.rs index 8c3e9efac..ae3861ea6 100644 --- a/crypto/stark/src/examples/read_only_memory.rs +++ b/crypto/stark/src/examples/read_only_memory.rs @@ -232,7 +232,8 @@ where transition_constraints: Vec>>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct ReadOnlyPublicInputs where F: IsFFTField, diff --git a/crypto/stark/src/examples/read_only_memory_logup.rs b/crypto/stark/src/examples/read_only_memory_logup.rs index e4f25c16c..e1ee91da9 100644 --- a/crypto/stark/src/examples/read_only_memory_logup.rs +++ b/crypto/stark/src/examples/read_only_memory_logup.rs @@ -360,7 +360,8 @@ where transition_constraints: Vec>>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct LogReadOnlyPublicInputs where F: IsFFTField + Send + Sync, diff --git a/crypto/stark/src/examples/simple_addition.rs b/crypto/stark/src/examples/simple_addition.rs index 78f938838..2b6557cbb 100644 --- a/crypto/stark/src/examples/simple_addition.rs +++ b/crypto/stark/src/examples/simple_addition.rs @@ -83,7 +83,8 @@ where constraints: Vec>>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct SimpleAdditionPublicInputs where F: IsFFTField, diff --git a/crypto/stark/src/examples/simple_fibonacci.rs b/crypto/stark/src/examples/simple_fibonacci.rs index a39064258..de410a30d 100644 --- a/crypto/stark/src/examples/simple_fibonacci.rs +++ b/crypto/stark/src/examples/simple_fibonacci.rs @@ -82,7 +82,8 @@ where constraints: Vec>>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct FibonacciPublicInputs where F: IsFFTField, From ef628599de9c6544e09a59e0011cb0b1e887ec14 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 01:03:06 -0300 Subject: [PATCH 22/52] stark: run_transition_prover/run_transition_verifier framework helpers Shared plumbing for routing an AIR's compute_transition_prover / compute_transition through a ConstraintSet body via the folders. The verifier-flavored helper also accepts a Prover context (debug trace validation calls compute_transition with a prover frame) by running the prover folder and promoting the Base prefix, mirroring the old boxed path's evaluate_verifier promotion. The engine switch reuses these. --- crypto/stark/src/constraints/builder.rs | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index 305a1a3fc..a371f2670 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -238,6 +238,73 @@ pub trait ConstraintSet: Send + Sync { fn eval>(&self, b: &mut B); } +// ============================================================================= +// Shared AIR plumbing: run a ConstraintSet through the folders +// ============================================================================= + +/// Run a [`ConstraintSet`] through the [`ProverEvalFolder`]: the body of an +/// `AIR::compute_transition_prover` override. `base_evals` must be sized +/// `num_base` (the Base-prefix length of the set's meta, see +/// [`num_base_from_meta`]) and `ext_evals` the total constraint count — +/// the engine's existing contract. +/// +/// Panics if `ctx` is the Verifier variant (the engine only calls the +/// prover path with a prover frame). +pub fn run_transition_prover( + cs: &CS, + ctx: &TransitionEvaluationContext<'_, F, E>, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], +) where + F: IsSubFieldOf, + E: IsField, + CS: ConstraintSet, +{ + let mut folder = ProverEvalFolder::new(ctx, base_evals, ext_evals); + cs.eval(&mut folder); + folder.assert_all_emitted(); +} + +/// Run a [`ConstraintSet`] at a single point, returning all constraint +/// values in the extension field: the body of an `AIR::compute_transition` +/// override. +/// +/// A Verifier context runs the [`VerifierEvalFolder`] (the OOD/recursion +/// path). A Prover context is also accepted — debug trace validation calls +/// this method with a prover frame — by running the [`ProverEvalFolder`] +/// and promoting the Base-prefix results, mirroring the old boxed path's +/// `evaluate_verifier` promotion. +pub fn run_transition_verifier( + cs: &CS, + ctx: &TransitionEvaluationContext<'_, F, E>, + num_base: usize, + num_constraints: usize, +) -> Vec> +where + F: IsSubFieldOf, + E: IsField, + CS: ConstraintSet, +{ + let mut ext_evals = vec![FieldElement::::zero(); num_constraints]; + match ctx { + TransitionEvaluationContext::Verifier { .. } => { + let mut folder = VerifierEvalFolder::new(ctx, &mut ext_evals); + cs.eval(&mut folder); + folder.assert_all_emitted(); + } + TransitionEvaluationContext::Prover { .. } => { + let mut base_evals = vec![FieldElement::::zero(); num_base]; + let mut folder = ProverEvalFolder::new(ctx, &mut base_evals, &mut ext_evals); + cs.eval(&mut folder); + folder.assert_all_emitted(); + for (slot, base) in ext_evals.iter_mut().zip(base_evals) { + *slot = base.to_extension(); + } + } + } + ext_evals +} + // ============================================================================= // Debug-build emit tracking (shared by the folders) // ============================================================================= From abf52c6db622f0924cbfde1ea7dbcf18eaa13d78 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 01:05:32 -0300 Subject: [PATCH 23/52] stark: migrate simple_fibonacci, simple_addition, quadratic_air, dummy_air to ConstraintSet Each example gains a single-body ConstraintSet transcribing its old evaluate_verifier text, and routes compute_transition_prover / compute_transition through the folders via the run_transition_* helpers; num_base_transition_constraints comes from num_base_from_meta. Old TransitionConstraintEvaluator impls stay (zerofier machinery + deletion in a later phase). Meta preserves indexing, degrees, and end exemptions (simple_fibonacci 2, quadratic 1, dummy fib 2 / bit 0, simple_addition 0). --- crypto/stark/src/examples/dummy_air.rs | 59 ++++++++++++++++ crypto/stark/src/examples/quadratic_air.rs | 64 ++++++++++++++++++ crypto/stark/src/examples/simple_addition.rs | 67 +++++++++++++++++++ crypto/stark/src/examples/simple_fibonacci.rs | 65 ++++++++++++++++++ 4 files changed, 255 insertions(+) diff --git a/crypto/stark/src/examples/dummy_air.rs b/crypto/stark/src/examples/dummy_air.rs index 1409f96ba..9b1f453c3 100644 --- a/crypto/stark/src/examples/dummy_air.rs +++ b/crypto/stark/src/examples/dummy_air.rs @@ -3,6 +3,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -130,6 +134,36 @@ where } } +/// Single-body [`ConstraintSet`] for [`DummyAIR`]: the same constraints as +/// `FibConstraint` + `BitConstraint`, written once against the +/// [`ConstraintBuilder`]. +#[derive(Default)] +pub struct DummyConstraints; + +impl ConstraintSet for DummyConstraints { + fn meta(&self) -> Vec { + vec![ + // idx 0: fibonacci on column 1; reads two next rows ⇒ 2 end exemptions. + ConstraintMeta::base(0, 1).with_end_exemptions(2), + // idx 1: IS_BIT on column 0, every row. + ConstraintMeta::base(1, 2), + ] + } + + fn eval>(&self, b: &mut B) { + // a_{i+2} = a_{i+1} + a_i on column 1. + let a0 = b.main(0, 1); + let a1 = b.main(1, 1); + let a2 = b.main(2, 1); + b.emit_base(0, a2 - a1 - a0); + + // bit * (bit - 1) = 0 on column 0. + let bit = b.main(0, 0); + let one = b.one(); + b.emit_base(1, bit.clone() * (bit - one)); + } +} + pub struct DummyAIR { context: AirContext, transition_constraints: Vec>>, @@ -184,6 +218,31 @@ impl AIR for DummyAIR { &self.transition_constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover(&DummyConstraints, evaluation_context, base_evals, ext_evals); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &DummyConstraints, + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&DummyConstraints.meta()) + } + fn context(&self) -> &AirContext { &self.context } diff --git a/crypto/stark/src/examples/quadratic_air.rs b/crypto/stark/src/examples/quadratic_air.rs index 9bf4b968c..c44d711d6 100644 --- a/crypto/stark/src/examples/quadratic_air.rs +++ b/crypto/stark/src/examples/quadratic_air.rs @@ -3,6 +3,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -73,6 +77,36 @@ where } } +/// Single-body [`ConstraintSet`] for [`QuadraticAIR`]: the same constraint +/// as `QuadraticConstraint`, written once against the [`ConstraintBuilder`]. +pub struct QuadraticConstraints { + phantom: PhantomData, +} + +impl Default for QuadraticConstraints { + fn default() -> Self { + Self { + phantom: PhantomData, + } + } +} + +impl ConstraintSet for QuadraticConstraints +where + F: IsFFTField + Send + Sync, +{ + fn meta(&self) -> Vec { + // idx 0: x_{i+1} = x_i²; reads the next row ⇒ 1 end exemption. + vec![ConstraintMeta::base(0, 2).with_end_exemptions(1)] + } + + fn eval>(&self, b: &mut B) { + let x = b.main(0, 0); + let x_squared = b.main(1, 0); + b.emit_base(0, x_squared - x.clone() * x); + } +} + pub struct QuadraticAIR where F: IsFFTField, @@ -138,6 +172,36 @@ where &self.constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &QuadraticConstraints::default(), + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &QuadraticConstraints::default(), + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&QuadraticConstraints::::default().meta()) + } + fn context(&self) -> &AirContext { &self.context } diff --git a/crypto/stark/src/examples/simple_addition.rs b/crypto/stark/src/examples/simple_addition.rs index 2b6557cbb..5cb9df41f 100644 --- a/crypto/stark/src/examples/simple_addition.rs +++ b/crypto/stark/src/examples/simple_addition.rs @@ -6,6 +6,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -75,6 +79,39 @@ where } } +/// Single-body [`ConstraintSet`] for [`SimpleAdditionAIR`]: the same +/// constraint as `AdditionConstraint`, written once against the +/// [`ConstraintBuilder`]. +pub struct SimpleAdditionConstraints { + phantom: PhantomData, +} + +impl Default for SimpleAdditionConstraints { + fn default() -> Self { + Self { + phantom: PhantomData, + } + } +} + +impl ConstraintSet for SimpleAdditionConstraints +where + F: IsFFTField + Send + Sync, +{ + fn meta(&self) -> Vec { + // idx 0: col0 + col1 = col2, applied at every row (no exemptions). + vec![ConstraintMeta::base(0, 1)] + } + + fn eval>(&self, b: &mut B) { + let col0 = b.main(0, 0); + let col1 = b.main(0, 1); + let col2 = b.main(0, 2); + // Constraint: col0 + col1 - col2 = 0 + b.emit_base(0, col0 + col1 - col2); + } +} + pub struct SimpleAdditionAIR where F: IsFFTField, @@ -146,6 +183,36 @@ where &self.constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &SimpleAdditionConstraints::default(), + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &SimpleAdditionConstraints::default(), + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&SimpleAdditionConstraints::::default().meta()) + } + fn context(&self) -> &AirContext { &self.context } diff --git a/crypto/stark/src/examples/simple_fibonacci.rs b/crypto/stark/src/examples/simple_fibonacci.rs index de410a30d..f10dc3179 100644 --- a/crypto/stark/src/examples/simple_fibonacci.rs +++ b/crypto/stark/src/examples/simple_fibonacci.rs @@ -1,6 +1,10 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -74,6 +78,37 @@ where } } +/// Single-body [`ConstraintSet`] for [`FibonacciAIR`]: the same constraint +/// as `FibConstraint`, written once against the [`ConstraintBuilder`]. +pub struct SimpleFibonacciConstraints { + phantom: PhantomData, +} + +impl Default for SimpleFibonacciConstraints { + fn default() -> Self { + Self { + phantom: PhantomData, + } + } +} + +impl ConstraintSet for SimpleFibonacciConstraints +where + F: IsFFTField + Send + Sync, +{ + fn meta(&self) -> Vec { + // idx 0: a_{i+2} = a_{i+1} + a_i; reads two next rows ⇒ 2 end exemptions. + vec![ConstraintMeta::base(0, 1).with_end_exemptions(2)] + } + + fn eval>(&self, b: &mut B) { + let a0 = b.main(0, 0); + let a1 = b.main(1, 0); + let a2 = b.main(2, 0); + b.emit_base(0, a2 - a1 - a0); + } +} + pub struct FibonacciAIR where F: IsFFTField, @@ -129,6 +164,36 @@ where &self.constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &SimpleFibonacciConstraints::default(), + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &SimpleFibonacciConstraints::default(), + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&SimpleFibonacciConstraints::::default().meta()) + } + fn boundary_constraints( &self, pub_inputs: &Self::PublicInputs, From db1254162d0c6a543cfbac9179c7d4df46c880a5 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 01:07:54 -0300 Subject: [PATCH 24/52] stark: migrate the fibonacci_2_cols family + fibonacci_multi_column to ConstraintSet Same recipe: single-body ConstraintSet transcribed from the old evaluate_verifier text, compute_transition_prover / compute_transition routed through the folders, num_base from num_base_from_meta. All three read the next row (fibonacci_multi_column reads two next rows); end exemptions copied (1, 1, and 2 per column respectively). The multi-column set is parameterized by num_columns, one constraint per column with idx == column, exactly as the old per-column structs. --- .../src/examples/fibonacci_2_cols_shifted.rs | 75 +++++++++++++++++++ .../stark/src/examples/fibonacci_2_columns.rs | 75 +++++++++++++++++++ .../src/examples/fibonacci_multi_column.rs | 73 ++++++++++++++++++ 3 files changed, 223 insertions(+) diff --git a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs index bb0679907..94d820bf8 100644 --- a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs +++ b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs @@ -1,6 +1,10 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -159,6 +163,47 @@ where } } +/// Single-body [`ConstraintSet`] for [`Fibonacci2ColsShifted`]: the same +/// constraints as `ShiftedFibTransition1` + `ShiftedFibTransition2`, written +/// once against the [`ConstraintBuilder`]. +pub struct Fibonacci2ColsShiftedConstraints { + phantom: PhantomData, +} + +impl Default for Fibonacci2ColsShiftedConstraints { + fn default() -> Self { + Self { + phantom: PhantomData, + } + } +} + +impl ConstraintSet for Fibonacci2ColsShiftedConstraints +where + F: IsFFTField + Send + Sync, +{ + fn meta(&self) -> Vec { + vec![ + // idx 0: Col0_{i+1} = Col1_i; reads the next row ⇒ 1 end exemption. + ConstraintMeta::base(0, 1).with_end_exemptions(1), + // idx 1: Col1_{i+1} = Col0_i + Col1_i; reads the next row ⇒ 1 end exemption. + ConstraintMeta::base(1, 1).with_end_exemptions(1), + ] + } + + fn eval>(&self, b: &mut B) { + let a0_0 = b.main(0, 0); + let a0_1 = b.main(0, 1); + let a1_0 = b.main(1, 0); + let a1_1 = b.main(1, 1); + + // Col0_{i+1} = Col1_i + b.emit_base(0, a1_0 - a0_1.clone()); + // Col1_{i+1} = Col0_i + Col1_i + b.emit_base(1, a1_1 - a0_0 - a0_1); + } +} + pub struct Fibonacci2ColsShifted where F: IsFFTField, @@ -227,6 +272,36 @@ where &self.transition_constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &Fibonacci2ColsShiftedConstraints::default(), + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &Fibonacci2ColsShiftedConstraints::default(), + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&Fibonacci2ColsShiftedConstraints::::default().meta()) + } + fn context(&self) -> &AirContext { &self.context } diff --git a/crypto/stark/src/examples/fibonacci_2_columns.rs b/crypto/stark/src/examples/fibonacci_2_columns.rs index 7662c8f98..c17397144 100644 --- a/crypto/stark/src/examples/fibonacci_2_columns.rs +++ b/crypto/stark/src/examples/fibonacci_2_columns.rs @@ -4,6 +4,10 @@ use super::simple_fibonacci::FibonacciPublicInputs; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -139,6 +143,47 @@ where } } +/// Single-body [`ConstraintSet`] for [`Fibonacci2ColsAIR`]: the same +/// constraints as `FibTransition1` + `FibTransition2`, written once against +/// the [`ConstraintBuilder`]. +pub struct Fibonacci2ColsConstraints { + phantom: PhantomData, +} + +impl Default for Fibonacci2ColsConstraints { + fn default() -> Self { + Self { + phantom: PhantomData, + } + } +} + +impl ConstraintSet for Fibonacci2ColsConstraints +where + F: IsFFTField + Send + Sync, +{ + fn meta(&self) -> Vec { + vec![ + // idx 0: s_{0, i+1} = s_{0, i} + s_{1, i}; reads the next row ⇒ 1 end exemption. + ConstraintMeta::base(0, 1).with_end_exemptions(1), + // idx 1: s_{1, i+1} = s_{1, i} + s_{0, i+1}; reads the next row ⇒ 1 end exemption. + ConstraintMeta::base(1, 1).with_end_exemptions(1), + ] + } + + fn eval>(&self, b: &mut B) { + let s0_0 = b.main(0, 0); + let s0_1 = b.main(0, 1); + let s1_0 = b.main(1, 0); + let s1_1 = b.main(1, 1); + + // s_{0, i+1} = s_{0, i} + s_{1, i} + b.emit_base(0, s1_0.clone() - s0_0 - s0_1.clone()); + // s_{1, i+1} = s_{1, i} + s_{0, i+1} + b.emit_base(1, s1_1 - s0_1 - s1_0); + } +} + pub struct Fibonacci2ColsAIR where F: IsFFTField, @@ -199,6 +244,36 @@ where &self.constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &Fibonacci2ColsConstraints::default(), + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &Fibonacci2ColsConstraints::default(), + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&Fibonacci2ColsConstraints::::default().meta()) + } + fn context(&self) -> &AirContext { &self.context } diff --git a/crypto/stark/src/examples/fibonacci_multi_column.rs b/crypto/stark/src/examples/fibonacci_multi_column.rs index ce8cd2aff..61426a03e 100644 --- a/crypto/stark/src/examples/fibonacci_multi_column.rs +++ b/crypto/stark/src/examples/fibonacci_multi_column.rs @@ -3,6 +3,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -120,6 +124,37 @@ pub struct FibonacciMultiColumnPublicInputs { pub initial_values: Vec<(FieldElement, FieldElement)>, } +/// Single-body [`ConstraintSet`] for [`FibonacciMultiColumnAIR`]: one +/// Fibonacci constraint per column (the same constraints as +/// [`FibColumnConstraint`]), written once against the [`ConstraintBuilder`]. +pub struct FibonacciMultiColumnConstraints { + pub num_columns: usize, +} + +impl ConstraintSet for FibonacciMultiColumnConstraints +where + F: IsSubFieldOf + IsFFTField + Send + Sync, + E: IsField + Send + Sync, +{ + fn meta(&self) -> Vec { + // idx i: column i's a_{j+2} = a_{j+1} + a_j; reads two next rows ⇒ 2 + // end exemptions. + (0..self.num_columns) + .map(|i| ConstraintMeta::base(i, 1).with_end_exemptions(2)) + .collect() + } + + fn eval>(&self, b: &mut B) { + for col in 0..self.num_columns { + let a0 = b.main(0, col); + let a1 = b.main(1, col); + let a2 = b.main(2, col); + // Constraint: a2 = a1 + a0 => a2 - a1 - a0 = 0 + b.emit_base(col, a2 - a1 - a0); + } + } +} + /// Multi-column Fibonacci AIR. /// Each column contains an independent Fibonacci sequence. pub struct FibonacciMultiColumnAIR @@ -158,6 +193,44 @@ where &self.constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &FibonacciMultiColumnConstraints { + num_columns: self.num_columns, + }, + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &FibonacciMultiColumnConstraints { + num_columns: self.num_columns, + }, + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&ConstraintSet::::meta( + &FibonacciMultiColumnConstraints { + num_columns: self.num_columns, + }, + )) + } + fn boundary_constraints( &self, pub_inputs: &Self::PublicInputs, From 2dff6db8f24bd4a06e2970c8b869575e31dc8b2f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 01:11:00 -0300 Subject: [PATCH 25/52] stark: migrate fibonacci_rap + read_only_memory(_logup) to ConstraintSet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RAP/LogUp trio: continuity and single-value stay Base constraints; the permutation/LogUp constraints read the auxiliary column and the interaction challenges, so they are Ext constraints after the Base prefix (num_base = 1 resp. 2 via num_base_from_meta, overriding the old all-ext default — value-neutral: the engine's F-vs-E accumulation split changes, the composition polynomial does not). Degrees and end exemptions copied from the old structs, including fibonacci_rap's steps=16-hard-coded fib end exemptions and the degree-3 LogUp term. multi_table_lookup has no example-level constraints (all LogUp, framework-generated); noted in the file — it converts with AirWithBuses in the engine-switch phase. --- crypto/stark/src/examples/fibonacci_rap.rs | 75 +++++++++++++++ .../stark/src/examples/multi_table_lookup.rs | 6 ++ crypto/stark/src/examples/read_only_memory.rs | 83 +++++++++++++++++ .../src/examples/read_only_memory_logup.rs | 93 +++++++++++++++++++ 4 files changed, 257 insertions(+) diff --git a/crypto/stark/src/examples/fibonacci_rap.rs b/crypto/stark/src/examples/fibonacci_rap.rs index 25d1acefe..5770d845e 100644 --- a/crypto/stark/src/examples/fibonacci_rap.rs +++ b/crypto/stark/src/examples/fibonacci_rap.rs @@ -3,6 +3,10 @@ use std::{marker::PhantomData, ops::Div}; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -156,6 +160,47 @@ where } } +/// Single-body [`ConstraintSet`] for [`FibonacciRAP`]: the same constraints +/// as `FibConstraint` + `PermutationConstraint`, written once against the +/// [`ConstraintBuilder`]. The permutation constraint reads the auxiliary +/// (RAP) column and the interaction challenge, so it is an `Ext` constraint +/// after the `Base` prefix. +pub struct FibonacciRAPConstraints; + +impl ConstraintSet for FibonacciRAPConstraints +where + F: IsFFTField + Send + Sync, +{ + fn meta(&self) -> Vec { + vec![ + // idx 0: fibonacci; end exemptions hard-coded for the steps = 16 + // integration tests (see `FibConstraint::end_exemptions`). + ConstraintMeta::base(0, 1).with_end_exemptions(3 + 32 - 16 - 1), + // idx 1: permutation; reads the next row ⇒ 1 end exemption. + ConstraintMeta::ext(1, 2).with_end_exemptions(1), + ] + } + + fn eval>(&self, b: &mut B) { + // a_{i+2} = a_{i+1} + a_i on column 0. + let a0 = b.main(0, 0); + let a1 = b.main(1, 0); + let a2 = b.main(2, 0); + b.emit_base(0, a2 - a1 - a0); + + // z_{i+1} * (b_i + gamma) = z_i * (a_i + gamma) + let z_i = b.aux(0, 0); + let z_i_plus_one = b.aux(1, 0); + let gamma = b.challenge(0); + let a_i = b.main(0, 0); + let b_i = b.main(0, 1); + b.emit_ext( + 1, + z_i_plus_one * (b_i + gamma.clone()) - z_i * (a_i + gamma), + ); + } +} + pub struct FibonacciRAP where F: IsFFTField, @@ -278,6 +323,36 @@ where &self.transition_constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &FibonacciRAPConstraints, + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &FibonacciRAPConstraints, + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&ConstraintSet::::meta(&FibonacciRAPConstraints)) + } + fn context(&self) -> &AirContext { &self.context } diff --git a/crypto/stark/src/examples/multi_table_lookup.rs b/crypto/stark/src/examples/multi_table_lookup.rs index 0504d08cb..d02ea13f0 100644 --- a/crypto/stark/src/examples/multi_table_lookup.rs +++ b/crypto/stark/src/examples/multi_table_lookup.rs @@ -1,3 +1,9 @@ +//! NOTE(single-source constraints): this example defines NO example-level +//! transition constraints — every constraint is LogUp, generated by the +//! `AirWithBuses` framework from the bus interactions below. It therefore +//! has no `ConstraintSet` to migrate; it moves to the single-body path +//! together with `AirWithBuses` in the engine-switch phase. + use crate::{ constraints::transition::TransitionConstraintEvaluator, lookup::{ diff --git a/crypto/stark/src/examples/read_only_memory.rs b/crypto/stark/src/examples/read_only_memory.rs index ae3861ea6..3a476cc31 100644 --- a/crypto/stark/src/examples/read_only_memory.rs +++ b/crypto/stark/src/examples/read_only_memory.rs @@ -3,6 +3,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -224,6 +228,55 @@ where } } +/// Single-body [`ConstraintSet`] for [`ReadOnlyRAP`]: the same constraints +/// as `ContinuityConstraint` + `SingleValueConstraint` + +/// `PermutationConstraint`, written once against the [`ConstraintBuilder`]. +/// The permutation constraint reads the auxiliary (RAP) column and the +/// interaction challenges, so it is an `Ext` constraint after the `Base` +/// prefix. +pub struct ReadOnlyRAPConstraints; + +impl ConstraintSet for ReadOnlyRAPConstraints +where + F: IsFFTField + Send + Sync, +{ + fn meta(&self) -> Vec { + // All three read the next row ⇒ 1 end exemption each. + vec![ + ConstraintMeta::base(0, 2).with_end_exemptions(1), // continuity + ConstraintMeta::base(1, 2).with_end_exemptions(1), // single value + ConstraintMeta::ext(2, 2).with_end_exemptions(1), // permutation + ] + } + + fn eval>(&self, b: &mut B) { + let a_sorted_0 = b.main(0, 2); + let a_sorted_1 = b.main(1, 2); + let v_sorted_0 = b.main(0, 3); + let v_sorted_1 = b.main(1, 3); + let one = b.one(); + let addr_diff = a_sorted_1 - a_sorted_0; + + // (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address + b.emit_base(0, addr_diff.clone() * (addr_diff.clone() - one.clone())); + // (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 + b.emit_base(1, (v_sorted_1 - v_sorted_0) * (addr_diff - one)); + + // (z - (a'_{i+1} + α * v'_{i+1})) * p_{i+1} = (z - (a_{i+1} + α * v_{i+1})) * p_i + let p0 = b.aux(0, 0); + let p1 = b.aux(1, 0); + let z = b.challenge(0); + let alpha = b.challenge(1); + let a1 = b.main(1, 0); + let v1 = b.main(1, 1); + let a_sorted_1 = b.main(1, 2); + let v_sorted_1 = b.main(1, 3); + let sorted_fp = z.clone() - (a_sorted_1 + v_sorted_1 * alpha.clone()); + let unsorted_fp = z - (a1 + v1 * alpha); + b.emit_ext(2, sorted_fp * p1 - unsorted_fp * p0); + } +} + pub struct ReadOnlyRAP where F: IsFFTField, @@ -369,6 +422,36 @@ where &self.transition_constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &ReadOnlyRAPConstraints, + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &ReadOnlyRAPConstraints, + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&ConstraintSet::::meta(&ReadOnlyRAPConstraints)) + } + fn context(&self) -> &AirContext { &self.context } diff --git a/crypto/stark/src/examples/read_only_memory_logup.rs b/crypto/stark/src/examples/read_only_memory_logup.rs index e1ee91da9..72eb35995 100644 --- a/crypto/stark/src/examples/read_only_memory_logup.rs +++ b/crypto/stark/src/examples/read_only_memory_logup.rs @@ -7,6 +7,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -349,6 +353,65 @@ where } } +/// Single-body [`ConstraintSet`] for [`LogReadOnlyRAP`]: the same +/// constraints as `ContinuityConstraint` + `SingleValueConstraint` + +/// `PermutationConstraint`, written once against the [`ConstraintBuilder`]. +/// The LogUp permutation constraint reads the auxiliary column and the +/// interaction challenges, so it is an `Ext` constraint after the `Base` +/// prefix. +pub struct LogReadOnlyRAPConstraints; + +impl ConstraintSet for LogReadOnlyRAPConstraints +where + F: IsFFTField + IsSubFieldOf + Send + Sync, + E: IsField + Send + Sync, +{ + fn meta(&self) -> Vec { + // All three read the next row ⇒ 1 end exemption each. + vec![ + ConstraintMeta::base(0, 2).with_end_exemptions(1), // continuity + ConstraintMeta::base(1, 2).with_end_exemptions(1), // single value + ConstraintMeta::ext(2, 3).with_end_exemptions(1), // LogUp permutation + ] + } + + fn eval>(&self, b: &mut B) { + let a_sorted_0 = b.main(0, 2); + let a_sorted_1 = b.main(1, 2); + let v_sorted_0 = b.main(0, 3); + let v_sorted_1 = b.main(1, 3); + let one = b.one(); + let addr_diff = a_sorted_1 - a_sorted_0; + + // (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address + b.emit_base(0, addr_diff.clone() * (addr_diff.clone() - one.clone())); + // (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 + b.emit_base(1, (v_sorted_1 - v_sorted_0) * (addr_diff - one)); + + // We are using the following LogUp equation: + // s1 = s0 + m / sorted_term - 1/unsorted_term. + // Since constraints must be expressed without division, we multiply + // each term by sorted_term * unsorted_term. + let s0 = b.aux(0, 0); + let s1 = b.aux(1, 0); + let z = b.challenge(0); + let alpha = b.challenge(1); + let a1 = b.main(1, 0); + let v1 = b.main(1, 1); + let a_sorted_1 = b.main(1, 2); + let v_sorted_1 = b.main(1, 3); + let m = b.main(1, 4); + let unsorted_term = -(a1 + v1 * alpha.clone()) + z.clone(); + let sorted_term = -(a_sorted_1 + v_sorted_1 * alpha) + z; + b.emit_ext( + 2, + s0 * unsorted_term.clone() * sorted_term.clone() + m * unsorted_term.clone() + - sorted_term.clone() + - s1 * unsorted_term * sorted_term, + ); + } +} + /// AIR for a continuous read-only memory using the LogUp Lookup Argument. /// To accompany the understanding of this code you can see corresponding post in blog.lambdaclass.com. pub struct LogReadOnlyRAP @@ -509,6 +572,36 @@ where &self.transition_constraints } + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + run_transition_prover( + &LogReadOnlyRAPConstraints, + evaluation_context, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + evaluation_context: &TransitionEvaluationContext, + ) -> Vec> { + run_transition_verifier( + &LogReadOnlyRAPConstraints, + evaluation_context, + self.num_base_transition_constraints(), + self.num_transition_constraints(), + ) + } + + fn num_base_transition_constraints(&self) -> usize { + num_base_from_meta(&ConstraintSet::::meta(&LogReadOnlyRAPConstraints)) + } + fn context(&self) -> &AirContext { &self.context } From f2d34efd0143aa614568305b49de3312c8ab282f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 01:11:40 -0300 Subject: [PATCH 26/52] stark: move period=1 end-exemption tests onto the ConstraintMeta zerofier fns The two period=1 cases now exercise zerofier::end_exemptions_roots directly with a ConstraintMeta. The nonzero-offset case exists purely to exercise the period != 1 zerofier shape, which no production constraint uses; it stays on the old trait path untouched and dies with that machinery in the final deletion phase (noted inline). --- crypto/stark/src/tests/transition_tests.rs | 29 +++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/crypto/stark/src/tests/transition_tests.rs b/crypto/stark/src/tests/transition_tests.rs index 17bfaa6cc..81b207d3d 100644 --- a/crypto/stark/src/tests/transition_tests.rs +++ b/crypto/stark/src/tests/transition_tests.rs @@ -1,4 +1,6 @@ +use crate::constraints::builder::ConstraintMeta; use crate::constraints::transition::TransitionConstraintEvaluator; +use crate::constraints::zerofier; use crate::traits::TransitionEvaluationContext; use math::field::element::FieldElement; use math::field::goldilocks::GoldilocksField; @@ -6,7 +8,10 @@ use math::field::traits::IsFFTField; use std::marker::PhantomData; /// Dummy evaluator that only exposes the trait knobs we need (`period`, `offset`, -/// `end_exemptions`) to exercise `end_exemptions_roots`. +/// `end_exemptions`) to exercise the OLD trait-default `end_exemptions_roots`. +/// +/// Kept solely for the period ≠ 1 case below; it dies with the trait machinery +/// in the final deletion phase. struct DummyConstraint { period: usize, offset: usize, @@ -38,19 +43,18 @@ fn end_exemptions_roots_default_offset_matches_last_rows() { let trace_length = 8usize; let g = GoldilocksField::get_primitive_root_of_unity(trace_length.trailing_zeros() as u64).unwrap(); - let c = DummyConstraint:: { - period: 1, - offset: 0, - end_exemptions: 2, - phantom: PhantomData, - }; + let meta = ConstraintMeta::base(0, 1).with_end_exemptions(2); - let roots = c.end_exemptions_roots(&g, trace_length); + let roots = zerofier::end_exemptions_roots(&meta, &g, trace_length); // Constraint applies on rows 0..8; last two rows are 6 and 7. assert_eq!(roots, vec![g.pow(7u64), g.pow(6u64)]); } +// NOTE(single-source constraints): this case exists PURELY to exercise the +// period ≠ 1 zerofier shape, which no production constraint uses. It stays on +// the OLD trait path untouched and dies together with the period machinery in +// the final deletion phase. #[test] fn end_exemptions_roots_nonzero_offset_walks_the_offset_domain() { let trace_length = 8usize; @@ -74,12 +78,7 @@ fn end_exemptions_roots_zero_exemptions_is_empty() { let trace_length = 8usize; let g = GoldilocksField::get_primitive_root_of_unity(trace_length.trailing_zeros() as u64).unwrap(); - let c = DummyConstraint:: { - period: 1, - offset: 0, - end_exemptions: 0, - phantom: PhantomData, - }; + let meta = ConstraintMeta::base(0, 1); - assert!(c.end_exemptions_roots(&g, trace_length).is_empty()); + assert!(zerofier::end_exemptions_roots::(&meta, &g, trace_length).is_empty()); } From 2499b2a821f8b9aa595e5c9342dec5c6012221d7 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 08:50:16 -0300 Subject: [PATCH 27/52] stark: cross-version verification harness for the example AIRs + run evidence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/cross_verify_examples.sh builds examples_cli at two refs (bench_abba-style isolated worktree) and, per example AIR, checks prove NEW -> verify OLD and prove OLD -> verify NEW. The verifier recomputes OOD constraint evaluations from its own definitions, so any constraint (re)ordering, num_base drift, indexing or semantic change in the migration fails loudly — no proof determinism needed. cross_verify_examples.log: run with OLD=88adbfa6 (pre-migration examples-cli) and NEW=f2d34efd (all examples on ConstraintSet): all 11 examples pass in both directions. --- cross_verify_examples.log | 31 +++++++++ scripts/cross_verify_examples.sh | 116 +++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 cross_verify_examples.log create mode 100755 scripts/cross_verify_examples.sh diff --git a/cross_verify_examples.log b/cross_verify_examples.log new file mode 100644 index 000000000..3968d69c2 --- /dev/null +++ b/cross_verify_examples.log @@ -0,0 +1,31 @@ +==> Refs + OLD 88adbfa6 -> 88adbfa64c + NEW f2d34efd -> f2d34efd01 +Preparing worktree (detached HEAD 88adbfa6) +==> Building examples_cli @ 88adbfa64c -> cli_old +==> Building examples_cli @ f2d34efd01 -> cli_new +==> Cross-verifying 11 examples, both directions +PASS prove-NEW-verify-OLD : simple_fibonacci +PASS prove-OLD-verify-NEW : simple_fibonacci +PASS prove-NEW-verify-OLD : fibonacci_2_columns +PASS prove-OLD-verify-NEW : fibonacci_2_columns +PASS prove-NEW-verify-OLD : fibonacci_2_cols_shifted +PASS prove-OLD-verify-NEW : fibonacci_2_cols_shifted +PASS prove-NEW-verify-OLD : fibonacci_multi_column +PASS prove-OLD-verify-NEW : fibonacci_multi_column +PASS prove-NEW-verify-OLD : quadratic_air +PASS prove-OLD-verify-NEW : quadratic_air +PASS prove-NEW-verify-OLD : fibonacci_rap +PASS prove-OLD-verify-NEW : fibonacci_rap +PASS prove-NEW-verify-OLD : dummy_air +PASS prove-OLD-verify-NEW : dummy_air +PASS prove-NEW-verify-OLD : simple_addition +PASS prove-OLD-verify-NEW : simple_addition +PASS prove-NEW-verify-OLD : read_only_memory +PASS prove-OLD-verify-NEW : read_only_memory +PASS prove-NEW-verify-OLD : read_only_memory_logup +PASS prove-OLD-verify-NEW : read_only_memory_logup +PASS prove-NEW-verify-OLD : multi_table_lookup +PASS prove-OLD-verify-NEW : multi_table_lookup + +==> RESULT: all 11 examples cross-verify in both directions. diff --git a/scripts/cross_verify_examples.sh b/scripts/cross_verify_examples.sh new file mode 100755 index 000000000..f2ff56600 --- /dev/null +++ b/scripts/cross_verify_examples.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# +# cross_verify_examples.sh — cross-version verification of the example AIRs. +# +# WHY: the single-source constraints migration must preserve the constraint +# system EXACTLY — order, indices, num_base split, degrees, zerofier shape. +# Prove/verify within one version cannot see a self-consistent drift (a +# version that reorders constraints still accepts its own proofs). Verifying +# each side's proofs with the OTHER side's verifier does: the verifier +# recomputes the OOD constraint evaluations from ITS OWN constraint +# definitions against the other side's commitments, so any semantic +# difference fails loudly. Needs no proof determinism. +# +# WHAT IT DOES: +# 1. Builds the stark `examples_cli` example binary at REF_OLD and REF_NEW +# (isolated worktree, same pattern as scripts/bench_abba.sh). +# 2. Per example AIR: prove NEW -> verify OLD, and prove OLD -> verify NEW. +# 3. Prints a per-example, per-direction PASS/FAIL table; exits nonzero if +# any direction fails. A failing direction is a REAL migration finding — +# diagnose and fix the migration, never the old side. +# +# USAGE: +# scripts/cross_verify_examples.sh REF_OLD REF_NEW +# REF_OLD ref or SHA with the pre-migration constraint system +# REF_NEW ref or SHA with the migrated constraint system +# Env: WORK work/output dir (default /tmp/cross_verify_examples) +# WT build worktree (default /tmp/cross_verify_wt) + +set -euo pipefail + +if [ $# -ne 2 ]; then + echo "usage: cross_verify_examples.sh REF_OLD REF_NEW" >&2 + exit 2 +fi +REF_OLD="$1" +REF_NEW="$2" + +EXAMPLES=( + simple_fibonacci + fibonacci_2_columns + fibonacci_2_cols_shifted + fibonacci_multi_column + quadratic_air + fibonacci_rap + dummy_air + simple_addition + read_only_memory + read_only_memory_logup + multi_table_lookup +) + +WORK="${WORK:-/tmp/cross_verify_examples}" +WT="${WT:-/tmp/cross_verify_wt}" + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +SHA_OLD="$(git rev-parse "$REF_OLD")" +SHA_NEW="$(git rev-parse "$REF_NEW")" +echo "==> Refs" +echo " OLD $REF_OLD -> ${SHA_OLD:0:10}" +echo " NEW $REF_NEW -> ${SHA_NEW:0:10}" + +mkdir -p "$WORK" + +# --- 1. Build both examples_cli binaries in an isolated worktree --- +cleanup() { git worktree remove --force "$WT" 2>/dev/null || true; } +trap cleanup EXIT +git worktree remove --force "$WT" 2>/dev/null || true +git worktree add --detach "$WT" "$SHA_OLD" >/dev/null +build_cli() { # $1=sha $2=out (shared target dir -> 2nd build is incremental) + echo "==> Building examples_cli @ ${1:0:10} -> $2" + git -C "$WT" checkout --quiet -f "$1" + if ! (cd "$WT" && cargo build --release -p stark --features test-utils \ + --example examples_cli >"$WORK/build_$2.log" 2>&1); then + echo "ERROR: cargo build failed for $2 (@ ${1:0:10}). Tail of $WORK/build_$2.log:" >&2 + tail -40 "$WORK/build_$2.log" >&2 + exit 1 + fi + cp "$WT/target/release/examples/examples_cli" "$WORK/$2" +} +build_cli "$SHA_OLD" cli_old +build_cli "$SHA_NEW" cli_new +cleanup +trap - EXIT + +# --- 2. Cross-verify every example in both directions --- +fail=0 +check() { # $1=prover bin $2=verifier bin $3=example $4=direction label + local proof="$WORK/$3.$4.bin" + if ! "$WORK/$1" prove "$3" -o "$proof" >"$WORK/$3.$4.prove.log" 2>&1; then + echo "FAIL $4 : $3 (PROVE errored; see $WORK/$3.$4.prove.log)" + fail=1 + return + fi + if "$WORK/$2" verify "$3" "$proof" >"$WORK/$3.$4.verify.log" 2>&1; then + echo "PASS $4 : $3" + else + echo "FAIL $4 : $3 (VERIFY rejected; see $WORK/$3.$4.verify.log)" + fail=1 + fi +} + +echo "==> Cross-verifying ${#EXAMPLES[@]} examples, both directions" +for ex in "${EXAMPLES[@]}"; do + check cli_new cli_old "$ex" "prove-NEW-verify-OLD" + check cli_old cli_new "$ex" "prove-OLD-verify-NEW" +done + +echo +if [ "$fail" = "0" ]; then + echo "==> RESULT: all ${#EXAMPLES[@]} examples cross-verify in both directions." +else + echo "==> RESULT: FAILURES above — the migration drifted from the old constraint system." +fi +exit "$fail" From 936745dbf12dbf8c514dd6d2f55db9c7aa824384 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 09:58:55 -0300 Subject: [PATCH 28/52] stark: single-source LogUp constraints (LogUpLayout + emit_logup_constraints + logup_meta) Generate the LogUp transition constraints from the interaction config through the generic ConstraintBuilder, so one body serves the compiled prover folder, the verifier folder and IR capture. LogUpLayout captures what AirWithBuses::new computes (committed pairs, absorbed interactions, term/acc column indices); emit_logup_constraints emits the batched-term and accumulated constraints (1- and 2-absorbed branches, aux(1,.) next-row reads); logup_meta reproduces the boxed structs' degree/zerofier answers (all RootKind::Ext, default shape). The fingerprint/multiplicity/packing capture helpers are ported from the spike branch's IrBuilder-shaped helpers to the generic B: ConstraintBuilder API (operator style, base operand LEFT for mixed base x ext ops). Differential test (logup_single_source_tests) compares the OLD boxed LogUp structs vs the new emit fns via ProverEvalFolder, VerifierEvalFolder and capture->interpret on 1000 random two-step frames, for 1-absorbed, 2-absorbed, absorbed-only, and every Packing variant. --- crypto/stark/src/lookup.rs | 788 +++++++++++++++++++++++++++++++++++++ 1 file changed, 788 insertions(+) diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 5174bf66c..27ca3b488 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -1708,6 +1708,443 @@ fn compute_fingerprint_from_step, B: IsField>( z - &linear_combination } +// ============================================================================= +// LogUp single-source constraints (ConstraintBuilder front-end) +// ============================================================================= +// +// The LogUp transition constraints are generated from the interaction config +// (a [`LogUpLayout`]) through the generic [`ConstraintBuilder`], so ONE body +// serves the compiled prover folder, the verifier folder and IR capture. These +// are the single-source twins of the boxed `LookupBatchedTermConstraint` / +// `LookupAccumulatedConstraint` structs (which stay for now as the differential +// oracle; the engine switch deletes them). +// +// All LogUp constraints use the default zerofier shape (every row, no +// exemptions) — the structs override none of period/offset/exemptions — so +// [`logup_meta`] emits plain [`RootKind::Ext`] entries. +// +// Honesty note (matches the runtime body): `BusValue::Linear`'s data-dependent +// "skip the multiply when the row value is zero" optimization is NOT reproduced +// here — the constraint body is row-agnostic and always emits the multiply. +// This is value-preserving (adding `0·α` is a no-op) and only costs a few extra +// base×ext muls per row. + +use crate::constraints::builder::{ConstraintBuilder, ConstraintMeta}; + +/// Config describing an [`AirWithBuses`] table's LogUp layout, exactly as +/// computed by [`AirWithBuses::new`] from the interaction list (via +/// [`split_interactions`]). This is the plain-data replacement for the +/// per-constraint `LookupBatchedTermConstraint` / `LookupAccumulatedConstraint` +/// objects: [`emit_logup_constraints`] reads it to generate every LogUp +/// constraint, and [`logup_meta`] reads it for the metadata. +#[derive(Clone)] +pub struct LogUpLayout { + /// All interactions, in the order they were registered. The first + /// `2 * num_committed_pairs` are the committed (batched) pairs; the last + /// 1–2 are absorbed into the accumulated constraint. + pub interactions: Vec, + /// Number of committed batched pairs (each gets one aux term column). + pub num_committed_pairs: usize, + /// Number of committed term columns (`= num_committed_pairs`). + pub num_term_columns: usize, + /// Index of the accumulated column (`= num_term_columns`). + pub acc_column_idx: usize, +} + +impl LogUpLayout { + /// Derive the LogUp layout from an interaction list, mirroring the split + /// [`AirWithBuses::new`] performs. + pub fn from_interactions(interactions: Vec) -> Self { + let num_interactions = interactions.len(); + let (num_committed_pairs, _absorbed_count) = split_interactions(num_interactions); + let num_term_columns = num_committed_pairs; + Self { + interactions, + num_committed_pairs, + num_term_columns, + acc_column_idx: num_term_columns, + } + } + + /// The absorbed interactions (last 1–2), folded into the accumulated + /// constraint. Empty when there are no interactions. + fn absorbed(&self) -> &[BusInteraction] { + let n = self.interactions.len(); + if n == 0 { + return &[]; + } + let (_, absorbed_count) = split_interactions(n); + &self.interactions[n - absorbed_count..] + } + + /// Number of LogUp transition constraints this layout produces: + /// one per committed pair (batched term) plus one accumulated constraint + /// when there is at least one interaction. + pub fn num_constraints(&self) -> usize { + if self.interactions.is_empty() { + 0 + } else { + self.num_committed_pairs + 1 + } + } +} + +/// Capture a [`Multiplicity`] as a base-field expression, mirroring +/// [`Multiplicity::evaluate_with`] (via [`compute_multiplicity_from_step`]). +fn emit_multiplicity(b: &B, multiplicity: &Multiplicity, offset: usize) -> B::Expr +where + F: IsField, + E: IsField, + B: ConstraintBuilder, +{ + match multiplicity { + Multiplicity::One => b.one(), + Multiplicity::Column(col) => b.main(offset, *col), + Multiplicity::Sum(a, c) => b.main(offset, *a) + b.main(offset, *c), + Multiplicity::Negated(col) => b.one() - b.main(offset, *col), + Multiplicity::Diff(a, c) => b.main(offset, *a) - b.main(offset, *c), + Multiplicity::Sum3(a, c, d) => b.main(offset, *a) + b.main(offset, *c) + b.main(offset, *d), + Multiplicity::Linear(terms) => emit_linear_terms(b, terms, offset), + } +} + +/// Capture a slice of [`LinearTerm`]s as a base-field sum, mirroring the +/// `Multiplicity::Linear` arm of [`Multiplicity::evaluate_with`] (`Σ terms`, +/// starting from zero). +fn emit_linear_terms(b: &B, terms: &[LinearTerm], offset: usize) -> B::Expr +where + F: IsField, + E: IsField, + B: ConstraintBuilder, +{ + let mut result = b.const_base(0); + for term in terms { + match *term { + LinearTerm::Column { + coefficient, + column, + } => { + result = result + b.main(offset, column) * b.const_signed(coefficient); + } + LinearTerm::ColumnUnsigned { + coefficient, + column, + } => { + result = result + b.main(offset, column) * b.const_base(coefficient); + } + LinearTerm::Constant(value) => { + result = result + b.const_signed(value); + } + } + } + result +} + +/// Capture a [`Packing`]'s fingerprint contribution as a sum of extension +/// terms, mirroring [`Packing::accumulate_fingerprint_with`]. Terms are pushed +/// to `acc` (`col_expr * alpha_power`, base operand LEFT); returns the number +/// of alpha powers consumed (`= packing.num_bus_elements()`). Field addition is +/// associative and commutative, so this row-agnostic accumulation is +/// value-identical to the runtime body regardless of grouping. +fn emit_packing_fingerprint( + b: &B, + packing: Packing, + start_col: usize, + offset: usize, + alpha_offset: usize, + acc: &mut Vec, +) -> usize +where + F: IsField, + E: IsField, + B: ConstraintBuilder, +{ + let col = |c: usize| b.main(offset, c); + let alpha = |i: usize| b.alpha_pow(alpha_offset + i); + let shift_8 = || b.const_base(SHIFT_8); + let shift_16 = || b.const_base(SHIFT_16); + let shift_24 = || b.const_base(SHIFT_8 * SHIFT_16); + + match packing { + Packing::Direct => { + acc.push(col(start_col) * alpha(0)); + 1 + } + Packing::Word2L => { + let combined = col(start_col) + col(start_col + 1) * shift_16(); + acc.push(combined * alpha(0)); + 1 + } + Packing::Word4L => { + let combined = col(start_col) + + col(start_col + 1) * shift_8() + + col(start_col + 2) * shift_16() + + col(start_col + 3) * shift_24(); + acc.push(combined * alpha(0)); + 1 + } + Packing::DWordWL => { + acc.push(col(start_col) * alpha(0)); + acc.push(col(start_col + 1) * alpha(1)); + 2 + } + Packing::DWordHHW => { + acc.push(col(start_col) * alpha(0)); + let w = col(start_col + 1) + col(start_col + 2) * shift_16(); + acc.push(w * alpha(1)); + 2 + } + Packing::DWordWHH => { + let w = col(start_col) + col(start_col + 1) * shift_16(); + acc.push(w * alpha(0)); + acc.push(col(start_col + 2) * alpha(1)); + 2 + } + Packing::DWordHL => { + let w0 = col(start_col) + col(start_col + 1) * shift_16(); + acc.push(w0 * alpha(0)); + let w1 = col(start_col + 2) + col(start_col + 3) * shift_16(); + acc.push(w1 * alpha(1)); + 2 + } + Packing::DWordBL => { + let w0 = col(start_col) + + col(start_col + 1) * shift_8() + + col(start_col + 2) * shift_16() + + col(start_col + 3) * shift_24(); + acc.push(w0 * alpha(0)); + let w1 = col(start_col + 4) + + col(start_col + 5) * shift_8() + + col(start_col + 6) * shift_16() + + col(start_col + 7) * shift_24(); + acc.push(w1 * alpha(1)); + 2 + } + Packing::QuadHL => { + for i in 0..4 { + let c = start_col + i * 2; + let w = col(c) + col(c + 1) * shift_16(); + acc.push(w * alpha(i)); + } + 4 + } + Packing::QuadWL => { + for i in 0..4 { + acc.push(col(start_col + i) * alpha(i)); + } + 4 + } + } +} + +/// Capture a [`BusValue`]'s fingerprint contribution into `acc`, mirroring +/// [`BusValue::accumulate_fingerprint_from_step`]. Returns the number of alpha +/// powers consumed. +fn emit_busvalue_fingerprint( + b: &B, + bv: &BusValue, + offset: usize, + alpha_offset: usize, + acc: &mut Vec, +) -> usize +where + F: IsField, + E: IsField, + B: ConstraintBuilder, +{ + match bv { + BusValue::Packed { + start_column, + packing, + } => emit_packing_fingerprint::( + b, + *packing, + *start_column, + offset, + alpha_offset, + acc, + ), + BusValue::Linear(terms) => { + // Value-preserving: always emit the multiply (see the module note). + let result = emit_linear_terms(b, terms, offset); + acc.push(result * b.alpha_pow(alpha_offset)); + 1 + } + } +} + +/// Capture an interaction's fingerprint as an extension expression, mirroring +/// [`compute_fingerprint_from_step`]: `z - (bus_id + α·v[0] + α²·v[1] + ...)`. +/// +/// `α⁰ = 1`: the bus-id term needs no multiply and is added as a base constant. +fn emit_fingerprint(b: &B, interaction: &BusInteraction, offset: usize) -> B::ExprE +where + F: IsField, + E: IsField, + B: ConstraintBuilder, +{ + let z = b.challenge(0); + let bus = b.const_base(interaction.bus_id); + // Collect the α·value terms, then fold. Field addition is associative and + // commutative, so the grouping does not change the value. + let mut terms: Vec = Vec::new(); + let mut alpha_idx = 1; + for bv in &interaction.values { + alpha_idx += emit_busvalue_fingerprint::(b, bv, offset, alpha_idx, &mut terms); + } + // lc = bus_id + Σ terms (base + ext = ext, base operand LEFT). + let mut iter = terms.into_iter(); + let lc = match iter.next() { + Some(first) => { + let mut lc = bus + first; + for t in iter { + lc = lc + t; + } + lc + } + // No values: fingerprint is z - bus_id. `bus` is base, `z` is ext, and + // the tower only implements base − ext (base operand LEFT), so write + // z − bus as −(bus − z). + None => return -(bus - z), + }; + z - lc +} + +/// Emit the batched-term constraint for committed pair `pair_idx`, mirroring +/// `LookupBatchedTermConstraint::capture`: +/// `c · fp_a · fp_b − sign_a·m_a·fp_b − sign_b·m_b·fp_a` (degree 3). +fn emit_logup_batched_term(b: &mut B, layout: &LogUpLayout, pair_idx: usize, idx: usize) +where + F: IsField, + E: IsField, + B: ConstraintBuilder, +{ + let interaction_a = &layout.interactions[pair_idx * 2]; + let interaction_b = &layout.interactions[pair_idx * 2 + 1]; + let term_column_idx = pair_idx; + + let c = b.aux(0, term_column_idx); + let m_a = emit_multiplicity::(b, &interaction_a.multiplicity, 0); + let m_b = emit_multiplicity::(b, &interaction_b.multiplicity, 0); + let fp_a = emit_fingerprint::(b, interaction_a, 0); + let fp_b = emit_fingerprint::(b, interaction_b, 0); + + // is_sender is a compile-time bool, resolved as add vs neg instead of an + // ext×ext sign multiply (same optimization as the runtime body). m·fp is + // base×ext = ext (base operand LEFT). + let term_a = m_a * fp_b.clone(); + let term_a = if interaction_a.is_sender { + term_a + } else { + -term_a + }; + let term_b = m_b * fp_a.clone(); + let term_b = if interaction_b.is_sender { + term_b + } else { + -term_b + }; + + // c · fp_a · fp_b: c is aux (ext), so this is ext throughout. + let main = c * fp_a * fp_b; + b.emit_ext(idx, main - term_a - term_b); +} + +/// Emit the accumulated constraint (with 1–2 absorbed interactions), mirroring +/// `LookupAccumulatedConstraint::capture`. `acc_curr` reads row 0; `acc_next`, +/// the committed-term sum and the absorbed fingerprints/multiplicities all read +/// the NEXT row (offset 1). +/// +/// - 1 absorbed: `(acc_next − acc_curr − Σterms + L/N)·f − sign·m` (degree 2) +/// - 2 absorbed: `(…)·f₁·f₂ − sign₁·m₁·f₂ − sign₂·m₂·f₁` (degree 3) +fn emit_logup_accumulated(b: &mut B, layout: &LogUpLayout, idx: usize) +where + F: IsField, + E: IsField, + B: ConstraintBuilder, +{ + let acc_curr = b.aux(0, layout.acc_column_idx); + let acc_next = b.aux(1, layout.acc_column_idx); + + // delta = acc_next − acc_curr − Σ committed_terms(next) + L/N + let mut delta = acc_next - acc_curr; + for i in 0..layout.num_term_columns { + delta = delta - b.aux(1, i); + } + delta = delta + b.table_offset(); + + let absorbed = layout.absorbed(); + let root = match absorbed.len() { + 1 => { + // delta · f − sign · m + let m = emit_multiplicity::(b, &absorbed[0].multiplicity, 1); + let f = emit_fingerprint::(b, &absorbed[0], 1); + let mt = if absorbed[0].is_sender { m } else { -m }; + // delta · f is ext; `mt` is base. The tower only implements base − + // ext (base operand LEFT), so write `delta·f − mt` as `−(mt − delta·f)`. + -(mt - delta * f) + } + 2 => { + // delta · f1 · f2 − sign1·m1·f2 − sign2·m2·f1 + let m1 = emit_multiplicity::(b, &absorbed[0].multiplicity, 1); + let m2 = emit_multiplicity::(b, &absorbed[1].multiplicity, 1); + let f1 = emit_fingerprint::(b, &absorbed[0], 1); + let f2 = emit_fingerprint::(b, &absorbed[1], 1); + + let term1 = m1 * f2.clone(); + let term1 = if absorbed[0].is_sender { term1 } else { -term1 }; + let term2 = m2 * f1.clone(); + let term2 = if absorbed[1].is_sender { term2 } else { -term2 }; + delta * f1 * f2 - term1 - term2 + } + _ => unreachable!("absorbed must contain 1 or 2 interactions"), + }; + + b.emit_ext(idx, root); +} + +/// Emit every LogUp transition constraint for `layout` through the builder, +/// starting at absolute constraint index `idx_base` (the table's base-constraint +/// count). Committed batched terms come first (one per committed pair), then the +/// single accumulated constraint. Emits nothing when there are no interactions. +pub fn emit_logup_constraints(b: &mut B, layout: &LogUpLayout, idx_base: usize) +where + F: IsField, + E: IsField, + B: ConstraintBuilder, +{ + if layout.interactions.is_empty() { + return; + } + let mut idx = idx_base; + for pair_idx in 0..layout.num_committed_pairs { + emit_logup_batched_term::(b, layout, pair_idx, idx); + idx += 1; + } + emit_logup_accumulated::(b, layout, idx); +} + +/// The idx-ordered [`ConstraintMeta`] for `layout`'s LogUp constraints, starting +/// at `idx_start`. Reproduces the boxed structs' answers exactly: batched terms +/// are degree 3; the accumulated constraint is degree `1 + absorbed.len()` +/// (2 for one absorbed, 3 for two). All are [`RootKind::Ext`] with the default +/// zerofier shape (period 1, offset 0, no exemptions) — the structs override +/// none of those. +pub fn logup_meta(layout: &LogUpLayout, idx_start: usize) -> Vec { + let mut meta = Vec::with_capacity(layout.num_constraints()); + if layout.interactions.is_empty() { + return meta; + } + let mut idx = idx_start; + for _ in 0..layout.num_committed_pairs { + meta.push(ConstraintMeta::ext(idx, 3)); // c · fp_a · fp_b + idx += 1; + } + let absorbed_len = layout.absorbed().len(); + meta.push(ConstraintMeta::ext(idx, 1 + absorbed_len)); + meta +} + /// Constraint for a batched pair of interactions sharing one aux column. /// /// Verifies: `c = m_a/fp_a + m_b/fp_b` where signs are baked into m_a, m_b. @@ -2004,3 +2441,354 @@ where } } } + +#[cfg(test)] +mod logup_single_source_tests { + //! Differential tests for the single-source LogUp constraint bodies + //! ([`emit_logup_constraints`]) against the OLD boxed constraint structs + //! (`LookupBatchedTermConstraint` / `LookupAccumulatedConstraint`) that stay + //! in-branch as the transcription oracle until the final deletion phase. + //! + //! For every layout we compare, on 1000 random two-step frames (off-trace + //! points where a weakened or slipped transcription diverges with + //! overwhelming probability): + //! 1. [`ProverEvalFolder`] output == old `evaluate_prover` (ext slots); + //! 2. [`VerifierEvalFolder`] output == old `evaluate_verifier`; + //! 3. capture → flatten → interpret == old `evaluate_verifier`. + //! + //! Coverage: the accumulated constraint's 1-absorbed AND 2-absorbed branches + //! (the latter reads `aux(1, ·)` next-row cells), the batched-term + //! constraint, and every [`Packing`] variant's fingerprint contribution. + use super::*; + use crate::constraint_ir::{eval_program, eval_program_verifier}; + use crate::constraints::builder::{ + CaptureBuilder, ProverEvalFolder, RootKind, VerifierEvalFolder, num_base_from_meta, + }; + use crate::frame::Frame; + use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext3; + use math::field::goldilocks::GoldilocksField as Gl; + + type Fp = FieldElement; + type Fp3 = FieldElement; + + const TRIALS: usize = 1000; + + /// A tiny deterministic SplitMix64 PRNG (no `rand` dependency). + struct SplitMix64 { + state: u64, + } + impl SplitMix64 { + fn new(seed: u64) -> Self { + Self { state: seed } + } + fn next_u64(&mut self) -> u64 { + self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + } + + /// Build the OLD boxed constraints for a layout, index-for-index with + /// [`emit_logup_constraints`]: committed batched terms first (idx `idx_base` + /// onward), then the accumulated constraint. + fn old_boxed( + layout: &LogUpLayout, + idx_base: usize, + ) -> Vec>> { + let mut out: Vec>> = Vec::new(); + let mut idx = idx_base; + for pair_idx in 0..layout.num_committed_pairs { + out.push(Box::new(LookupBatchedTermConstraint::new( + layout.interactions[pair_idx * 2].clone(), + layout.interactions[pair_idx * 2 + 1].clone(), + pair_idx, + idx, + ))); + idx += 1; + } + if !layout.interactions.is_empty() { + out.push(Box::new(LookupAccumulatedConstraint::new( + idx, + layout.num_term_columns, + layout.absorbed().to_vec(), + ))); + } + out + } + + /// Number of aux columns the layout uses: committed term columns + the + /// accumulated column. + fn num_aux_cols(layout: &LogUpLayout) -> usize { + if layout.interactions.is_empty() { + 0 + } else { + layout.num_term_columns + 1 + } + } + + fn rand_fp3(rng: &mut SplitMix64) -> Fp3 { + FieldElement::::new([ + Fp::from(rng.next_u64()), + Fp::from(rng.next_u64()), + Fp::from(rng.next_u64()), + ]) + } + + /// The full three-way differential check for one layout, on `TRIALS` random + /// two-step frames. + fn check_layout(label: &str, layout: &LogUpLayout, num_main_cols: usize) { + let n_base = 0usize; // LogUp constraints are all extension-rooted. + let old = old_boxed(layout, n_base); + let n = old.len(); + assert_eq!(n, layout.num_constraints(), "[{label}] constraint count"); + + // Meta parity vs the old boxed objects. + let meta = logup_meta(layout, n_base); + assert_eq!(meta.len(), n, "[{label}] meta count"); + let num_base = num_base_from_meta(&meta); + assert_eq!(num_base, 0, "[{label}] LogUp meta is all-ext"); + for (i, m) in meta.iter().enumerate() { + let c = old.iter().find(|c| c.constraint_idx() == i).expect("dense"); + assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); + assert_eq!(m.kind, RootKind::Ext, "[{label}] meta kind {i}"); + assert_eq!(m.degree, c.degree(), "[{label}] degree {i}"); + assert_eq!(m.period, c.period(), "[{label}] period {i}"); + assert_eq!(m.offset, c.offset(), "[{label}] offset {i}"); + assert_eq!( + m.end_exemptions, + c.end_exemptions(), + "[{label}] end_exempt {i}" + ); + } + + // Capture once; tree-measured degree <= declared. + let mut cb = CaptureBuilder::::new(); + emit_logup_constraints(&mut cb, layout, n_base); + let (prog, degrees) = cb.finish(num_base); + assert!(prog.complete, "[{label}] capture must be complete"); + assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); + for &(idx, measured) in °rees { + assert!( + measured <= meta[idx].degree, + "[{label}] constraint {idx}: tree degree {measured} exceeds declared {}", + meta[idx].degree + ); + } + + let n_aux = num_aux_cols(layout); + let shifts = PackingShifts::::new(); + let vshifts = PackingShifts::::new(); + let no_periodic: Vec = vec![]; + let no_periodic_e: Vec = vec![]; + + for trial in 0..TRIALS { + let mut rng = SplitMix64::new(0xC0FF_EE00_u64 ^ (label.len() as u64) ^ trial as u64); + + // Random two-step prover frame. + let mk_step = |rng: &mut SplitMix64| { + let main: Vec = (0..num_main_cols) + .map(|_| Fp::from(rng.next_u64())) + .collect(); + let aux: Vec = (0..n_aux).map(|_| rand_fp3(rng)).collect(); + TableView::new(vec![main], vec![aux]) + }; + let frame = Frame::::new(vec![mk_step(&mut rng), mk_step(&mut rng)]); + let rap_challenges = vec![rand_fp3(&mut rng), rand_fp3(&mut rng)]; // [z, alpha] + let alpha_powers: Vec = (0..12).map(|_| rand_fp3(&mut rng)).collect(); + let table_offset = rand_fp3(&mut rng); + + let prover_ctx = TransitionEvaluationContext::new_prover( + &frame, + &no_periodic, + &rap_challenges, + &alpha_powers, + &table_offset, + &shifts, + ); + + // --- old prover-side reference: evaluate_prover into ext slots --- + let mut old_base = vec![Fp::zero(); n_base]; + let mut old_ext = vec![Fp3::zero(); n]; + for c in old.iter() { + c.evaluate_prover(&prover_ctx, &mut old_base, &mut old_ext); + } + + // --- 1. ProverEvalFolder == old evaluate_prover --- + let mut base_out = vec![Fp::zero(); n_base]; + let mut ext_out = vec![Fp3::zero(); n]; + let mut folder = ProverEvalFolder::new(&prover_ctx, &mut base_out, &mut ext_out); + emit_logup_constraints(&mut folder, layout, n_base); + folder.assert_all_emitted(); + for i in 0..n { + assert_eq!( + ext_out[i], old_ext[i], + "[{label}] prover folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- 3. capture → interpret == old evaluate_prover --- + let mut ir_base = vec![Fp::zero(); n_base]; + let mut ir_ext = vec![Fp3::zero(); n]; + eval_program(&prog, &prover_ctx, &mut ir_base, &mut ir_ext); + for i in 0..n { + assert_eq!( + ir_ext[i], old_ext[i], + "[{label}] interpreter mismatch, constraint {i}, trial {trial}" + ); + } + + // --- verifier-side: embed the same frame into the extension --- + let embed_step = |step: &TableView| -> TableView { + let main: Vec = (0..num_main_cols) + .map(|c| { + step.get_main_evaluation_element(0, c) + .clone() + .to_extension() + }) + .collect(); + let aux: Vec = (0..n_aux) + .map(|c| step.get_aux_evaluation_element(0, c).clone()) + .collect(); + TableView::new(vec![main], vec![aux]) + }; + let vframe: Frame = Frame::new(vec![ + embed_step(frame.get_evaluation_step(0)), + embed_step(frame.get_evaluation_step(1)), + ]); + let vctx = TransitionEvaluationContext::::new_verifier( + &vframe, + &no_periodic_e, + &rap_challenges, + &alpha_powers, + &table_offset, + &vshifts, + ); + let mut old_vext = vec![Fp3::zero(); n]; + for c in old.iter() { + c.evaluate_verifier(&vctx, &mut old_vext); + } + + // --- 2. VerifierEvalFolder == old evaluate_verifier --- + let mut vext_out = vec![Fp3::zero(); n]; + let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); + emit_logup_constraints(&mut vfolder, layout, n_base); + vfolder.assert_all_emitted(); + for i in 0..n { + assert_eq!( + vext_out[i], old_vext[i], + "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" + ); + } + + // capture → interpret (verifier) == old evaluate_verifier --- + let mut ir_vext = vec![Fp3::zero(); n]; + eval_program_verifier(&prog, &vctx, &mut ir_vext); + for i in 0..n { + assert_eq!( + ir_vext[i], old_vext[i], + "[{label}] verifier interpreter mismatch, constraint {i}, trial {trial}" + ); + } + } + } + + /// A sender interaction with a `Direct`-packed value at column 1. + fn direct_sender(bus_id: u64) -> BusInteraction { + BusInteraction::sender( + bus_id, + Multiplicity::Column(0), + vec![BusValue::Packed { + start_column: 1, + packing: Packing::Direct, + }], + ) + } + + /// A receiver interaction with a single `column(3)` value. + fn column_receiver(bus_id: u64) -> BusInteraction { + BusInteraction::receiver(bus_id, Multiplicity::Column(2), vec![BusValue::column(3)]) + } + + #[test] + fn logup_one_absorbed_matches_old() { + // 3 interactions → split(3) = (1 committed pair, 1 absorbed): + // idx 0: batched term (interactions 0,1) + // idx 1: accumulated, 1 absorbed (interaction 2), degree 2. + let interactions = vec![direct_sender(7), column_receiver(11), direct_sender(13)]; + let layout = LogUpLayout::from_interactions(interactions); + assert_eq!(layout.num_committed_pairs, 1); + assert_eq!(layout.absorbed().len(), 1, "must exercise 1-absorbed"); + check_layout("one_absorbed", &layout, 8); + } + + #[test] + fn logup_two_absorbed_matches_old() { + // 4 interactions → split(4) = (1 committed pair, 2 absorbed): + // idx 0: batched term (interactions 0,1) + // idx 1: accumulated, 2 absorbed (interactions 2,3), degree 3. + let interactions = vec![ + direct_sender(7), + column_receiver(11), + direct_sender(13), + column_receiver(17), + ]; + let layout = LogUpLayout::from_interactions(interactions); + assert_eq!(layout.num_committed_pairs, 1); + assert_eq!(layout.absorbed().len(), 2, "must exercise 2-absorbed"); + check_layout("two_absorbed", &layout, 8); + } + + #[test] + fn logup_two_interactions_absorbed_only() { + // 2 interactions → split(2) = (0 committed pairs, 2 absorbed): the + // accumulated constraint alone, degree 3, no batched term. + let interactions = vec![direct_sender(7), column_receiver(11)]; + let layout = LogUpLayout::from_interactions(interactions); + assert_eq!(layout.num_committed_pairs, 0); + assert_eq!(layout.num_constraints(), 1); + check_layout("two_absorbed_only", &layout, 8); + } + + #[test] + fn logup_matches_old_for_all_packing_variants() { + // Drive every Packing arm through the fingerprint of a committed pair + // and an absorbed interaction. DWordBL/QuadHL are the widest (8 cols); + // give a generous column budget. + const ALL_PACKINGS: [Packing; 10] = [ + Packing::Direct, + Packing::Word2L, + Packing::Word4L, + Packing::DWordWL, + Packing::DWordHHW, + Packing::DWordWHH, + Packing::DWordHL, + Packing::DWordBL, + Packing::QuadHL, + Packing::QuadWL, + ]; + for packing in ALL_PACKINGS { + // 3 interactions: two committed (pair) + one absorbed, all using the + // packing at column 0. + let mk = |bus: u64, sender: bool| { + let values = vec![BusValue::Packed { + start_column: 0, + packing, + }]; + if sender { + BusInteraction::sender(bus, Multiplicity::One, values) + } else { + BusInteraction::receiver(bus, Multiplicity::One, values) + } + }; + let interactions = vec![mk(3, true), mk(5, false), mk(7, true)]; + let layout = LogUpLayout::from_interactions(interactions); + check_layout( + &format!("packing_{packing:?}"), + &layout, + packing.num_columns(), + ); + } + } +} From 9ae742893ce5128f270fc1326356b49d971b0809 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 10:02:16 -0300 Subject: [PATCH 29/52] prover: single-source CpuConstraints ConstraintSet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CPU table's transition constraints are assembled by create_all_cpu_constraints in prover/src/constraints/cpu.rs (never converted — P1 covered only prover/src/tables/*.rs). Add CpuConstraints: ConstraintSet built from the existing emit_*/*_meta fns in that file, in the same order as the old assembly (39 constraints, all base-field, idx 0..38). Differential test (constraint_set_tests_b::cpu) compares it against the old boxed create_all_cpu_constraints assembly: count / num_base / per-idx degree / zerofier params, plus the 1000-row three-way folder-vs-interpreter differential. --- prover/src/constraints/cpu.rs | 180 ++++++++++++++++++++- prover/src/tests/constraint_set_tests_b.rs | 32 ++++ 2 files changed, 210 insertions(+), 2 deletions(-) diff --git a/prover/src/constraints/cpu.rs b/prover/src/constraints/cpu.rs index f269ce3f1..210cedaec 100644 --- a/prover/src/constraints/cpu.rs +++ b/prover/src/constraints/cpu.rs @@ -724,9 +724,9 @@ pub fn create_all_cpu_constraints() -> ( // exemptions), matching the structs (none override period/offset/ // exemptions). -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; -use super::templates::INV_SHIFT_32; +use super::templates::{INV_SHIFT_32, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta}; /// `col_a · col_b = 0`. Twin of [`ProductZeroConstraint`]. pub fn emit_product_zero>( @@ -968,3 +968,179 @@ pub fn next_pc_add_meta(idx: usize) -> [ConstraintMeta; 2] { ConstraintMeta::base(idx + 1, 3), ] } + +// ========================================================================= +// Single-source constraint set (ConstraintBuilder front-end) +// ========================================================================= + +/// The CPU table's transition constraints as a single [`ConstraintSet`], +/// mirroring [`create_all_cpu_constraints`] index-for-index (39 constraints, +/// all base-field): +/// - idx 0..11: IS_BIT (unconditional) on each of [`BIT_FLAG_COLUMNS`]; +/// - idx 12,13: ADD fast-path carry pair (conditional on `ADD`); +/// - idx 14,15: SUB fast-path carry pair (conditional on `SUB`); +/// - idx 16..21: `word_instr · {MEMORY, BRANCH, ECALL, WRITE_REGISTER, +/// READ_REGISTER1, READ_REGISTER2} = 0`; +/// - idx 22,23: `arg2` multiplex (words 0, 1); +/// - idx 24..27: register zero-forcing (`rv1[0..1]`, `rv2[0..1]`); +/// - idx 28,29: `rvd = cast(res, WL)` (words 0, 1); +/// - idx 30,31: BRANCH ⇒ `rvd = pc + instruction_length` carry pair; +/// - idx 32: `branch_cond`; +/// - idx 33,34: `next_pc = pc + instruction_length` carry pair; +/// - idx 35: `MEMORY · BRANCH = 0`; +/// - idx 36,37: `arg2` exclusivity (`imm_0`, `imm_1`); +/// - idx 38: `IS_BIT(mem_flags)` on non-MEMORY rows. +pub struct CpuConstraints; + +impl ConstraintSet for CpuConstraints { + fn meta(&self) -> Vec { + let mut m = Vec::with_capacity(NUM_CPU_CONSTRAINTS); + // idx 0..11: IS_BIT on each BIT_FLAG_COLUMNS entry (unconditional). + for i in 0..BIT_FLAG_COLUMNS.len() { + m.push(is_bit_meta(i, false)); + } + let mut idx = BIT_FLAG_COLUMNS.len(); + // idx 12,13: ADD pair (conditional on ADD). + m.extend(add_pair_meta(idx, true)); + idx += 2; + // idx 14,15: SUB pair (conditional on SUB). + m.extend(add_pair_meta(idx, true)); + idx += 2; + // idx 16..21: word_instr mutexes + register-read gates. + for _ in 0..6 { + m.push(product_zero_meta(idx)); + idx += 1; + } + // idx 22,23: arg2 multiplex. + m.push(arg2_meta(idx)); + idx += 1; + m.push(arg2_meta(idx)); + idx += 1; + // idx 24..27: register zero-forcing. + for _ in 0..4 { + m.push(reg_not_read_is_zero_meta(idx)); + idx += 1; + } + // idx 28,29: rvd = cast(res, WL). + m.push(rvd_eq_res_meta(idx)); + idx += 1; + m.push(rvd_eq_res_meta(idx)); + idx += 1; + // idx 30,31: branch rvd = pc + len. + m.extend(branch_rvd_meta(idx)); + idx += 2; + // idx 32: branch_cond. + m.push(branch_cond_meta(idx)); + idx += 1; + // idx 33,34: next_pc = pc + len. + m.extend(next_pc_add_meta(idx)); + idx += 2; + // idx 35: MEMORY · BRANCH = 0. + m.push(product_zero_meta(idx)); + idx += 1; + // idx 36,37: arg2 exclusivity. + m.push(arg2_exclusive_meta(idx)); + idx += 1; + m.push(arg2_exclusive_meta(idx)); + idx += 1; + // idx 38: IS_BIT(mem_flags) on non-MEMORY rows. + m.push(mem_flags_bit_meta(idx)); + idx += 1; + debug_assert_eq!(idx, NUM_CPU_CONSTRAINTS); + debug_assert_eq!(m.len(), NUM_CPU_CONSTRAINTS); + m + } + + fn eval>(&self, b: &mut B) { + // idx 0..11: IS_BIT on each BIT_FLAG_COLUMNS entry (unconditional). + for (i, &col) in BIT_FLAG_COLUMNS.iter().enumerate() { + emit_is_bit(b, i, col, None); + } + let mut idx = BIT_FLAG_COLUMNS.len(); + + // idx 12,13: ADD fast-path (cond = ADD), rv1 + arg2 = cast(res, WL). + emit_add_pair( + b, + idx, + &[cols::ADD], + &AddOperand::dword(cols::RV1_0), + &AddOperand::dword(cols::ARG2_0), + &AddOperand::from_dword_hl(cols::RES_0), + ); + idx += 2; + + // idx 14,15: SUB fast-path (cond = SUB), arg2 + res = rv1. + emit_add_pair( + b, + idx, + &[cols::SUB], + &AddOperand::dword(cols::ARG2_0), + &AddOperand::from_dword_hl(cols::RES_0), + &AddOperand::dword(cols::RV1_0), + ); + idx += 2; + + // idx 16..21: word_instr mutexes + register-read gates. + for &col in &[ + cols::MEMORY, + cols::BRANCH, + cols::ECALL, + cols::WRITE_REGISTER, + cols::READ_REGISTER1, + cols::READ_REGISTER2, + ] { + emit_product_zero(b, idx, cols::WORD_INSTR, col); + idx += 1; + } + + // idx 22,23: arg2 multiplex (low, high words). + emit_arg2(b, idx, 0); + idx += 1; + emit_arg2(b, idx, 1); + idx += 1; + + // idx 24..27: register zero-forcing (rv1/rv2 are DWordWL → 2 words each). + for &value_col in &[cols::RV1_0, cols::RV1_1] { + emit_reg_not_read_is_zero(b, idx, cols::READ_REGISTER1, value_col); + idx += 1; + } + for &value_col in &[cols::RV2_0, cols::RV2_1] { + emit_reg_not_read_is_zero(b, idx, cols::READ_REGISTER2, value_col); + idx += 1; + } + + // idx 28,29: ¬MEMORY ∧ ¬BRANCH ⇒ rvd = cast(res, WL). + emit_rvd_eq_res(b, idx, 0); + idx += 1; + emit_rvd_eq_res(b, idx, 1); + idx += 1; + + // idx 30,31: BRANCH ⇒ rvd = pc + instruction_length. + emit_branch_rvd_pair(b, idx); + idx += 2; + + // idx 32: branch_cond. + emit_branch_cond(b, idx); + idx += 1; + + // idx 33,34: next_pc = pc + instruction_length. + emit_next_pc_add_pair(b, idx); + idx += 2; + + // idx 35: MEMORY · BRANCH = 0. + emit_product_zero(b, idx, cols::MEMORY, cols::BRANCH); + idx += 1; + + // idx 36,37: arg2 exclusivity. + for &imm_col in &[cols::IMM_0, cols::IMM_1] { + emit_arg2_exclusive(b, idx, imm_col); + idx += 1; + } + + // idx 38: IS_BIT(mem_flags) on non-MEMORY rows. + emit_mem_flags_bit(b, idx); + idx += 1; + + debug_assert_eq!(idx, NUM_CPU_CONSTRAINTS); + } +} diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index 8be0da6b2..0989601f9 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -373,3 +373,35 @@ mod cpu32 { check_table("cpu32", &old, &Cpu32Constraints, cols::NUM_COLUMNS); } } + +// ============================================================================= +// cpu.rs (constraints/cpu.rs — assembled by create_all_cpu_constraints, never +// a prover/src/tables/*.rs conversion) +// ============================================================================= + +mod cpu { + use super::*; + use crate::constraints::cpu::{ + CpuConstraints, NUM_CPU_CONSTRAINTS, create_all_cpu_constraints, + }; + use crate::tables::cpu::cols; + use stark::constraints::transition::TransitionConstraint; + + #[test] + fn cpu_constraint_set_matches_old() { + // create_all_cpu_constraints returns (is_bit, add, other, next) with + // idx 0..11, 12..15, 16..38 respectively — concatenate in that order so + // the boxed oracle is dense and idx-ordered. + let (is_bit, add, other, next) = create_all_cpu_constraints(); + assert_eq!(next, NUM_CPU_CONSTRAINTS); + let mut old: OldVec = Vec::with_capacity(NUM_CPU_CONSTRAINTS); + for c in is_bit { + old.push(c.boxed()); + } + for c in add { + old.push(c.boxed()); + } + old.extend(other); + check_table("cpu", &old, &CpuConstraints, cols::NUM_COLUMNS); + } +} From 3239eb8af68c356887018c3e593bb95c5726a9a6 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 10:32:31 -0300 Subject: [PATCH 30/52] stark+prover: switch the engine to single-source constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewire AirWithBuses and the AIR trait onto the ConstraintBuilder framework, replacing the per-constraint boxed TransitionConstraintEvaluator objects. AirWithBuses gains a CS: ConstraintSet type param (last generic): the boxed transition_constraints vec is replaced by constraint_set: CS + logup: LogUpLayout + meta: Vec (= cs.meta() base-prefix ++ appended logup_meta) + num_base (num_base_from_meta) + a OnceLock lazy capture cache. new() takes the CS value. compute_transition_prover/compute_transition run ONE folder pass over cs.eval + emit_logup_constraints (LogUp idx offset by num_base); constraint_program() captures lazily (prover/GPU/tests only — the verify path never forces it), the guest-safety rule. AIR trait: compute_transition_prover/compute_transition become required (boxed defaults deleted); new constraints_meta() -> &[ConstraintMeta]; constraint_program() added (default panics — only capture-capable AIRs override it); transition_constraints() deleted; composition_poly_degree_bound maxes meta degrees; transition_zerofier_evaluations_grouped and the verifier's OOD zerofier denominators read ConstraintMeta via the constraints/zerofier free fns; debug.rs end-exemptions read from meta. CpuConstraints is wired in; every table's create_*_air now passes its XxxConstraints (EmptyConstraints for pure-lookup tables, L2gMemoryConstraints for the epoch-local L2G). VmAir becomes Box so the heterogeneous per-table AirWithBuses<..,CS> are stored behind a trait object; create_*_air return the concrete AirWithBuses so .with_name/.with_preprocessed still chain, boxed at VmAirs assembly. The 11 example AIRs drop their old per-constraint structs and route through the ConstraintSet + constraints_meta(); all test construction sites updated to the new API. cargo test -p stark green (170); cargo test --release -p lambda-vm-prover 509 passed, 5 failed (pre-existing: missing rust guest ELFs, fail on main too). --- crypto/stark/src/constraints/builder.rs | 14 + crypto/stark/src/debug.rs | 11 +- crypto/stark/src/examples/dummy_air.rs | 145 +---- .../src/examples/fibonacci_2_cols_shifted.rs | 152 +---- .../stark/src/examples/fibonacci_2_columns.rs | 151 +---- .../src/examples/fibonacci_multi_column.rs | 121 +--- crypto/stark/src/examples/fibonacci_rap.rs | 159 +---- .../stark/src/examples/multi_table_lookup.rs | 20 +- crypto/stark/src/examples/quadratic_air.rs | 84 +-- crypto/stark/src/examples/read_only_memory.rs | 241 +------- .../src/examples/read_only_memory_logup.rs | 359 +---------- crypto/stark/src/examples/simple_addition.rs | 84 +-- crypto/stark/src/examples/simple_fibonacci.rs | 83 +-- crypto/stark/src/lookup.rs | 222 +++++-- .../src/tests/bus_tests/completeness_tests.rs | 9 +- .../src/tests/bus_tests/multiplicity_tests.rs | 32 +- .../src/tests/bus_tests/packing_tests.rs | 9 +- .../src/tests/bus_tests/soundness_tests.rs | 95 ++- .../src/tests/prove_verify_roundtrip_tests.rs | 17 +- crypto/stark/src/traits.rs | 77 +-- crypto/stark/src/verifier.rs | 10 +- prover/src/continuation.rs | 37 +- prover/src/lib.rs | 239 +++++--- prover/src/test_utils.rs | 563 +++++++----------- prover/src/tests/bitwise_bus_tests.rs | 14 +- prover/src/tests/bitwise_tests.rs | 16 +- prover/src/tests/branch_bus_tests.rs | 14 +- prover/src/tests/dvrm_tests.rs | 4 +- prover/src/tests/local_to_global_bus_tests.rs | 37 +- prover/src/tests/lt_bus_tests.rs | 14 +- prover/src/tests/lt_tests.rs | 8 +- prover/src/tests/mul_tests.rs | 4 +- prover/src/tests/prove_elfs_tests.rs | 106 ++-- scripts/cross_verify_vm.sh | 138 +++++ 34 files changed, 1074 insertions(+), 2215 deletions(-) create mode 100755 scripts/cross_verify_vm.sh diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index a371f2670..9c43a12ce 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -238,6 +238,20 @@ pub trait ConstraintSet: Send + Sync { fn eval>(&self, b: &mut B); } +/// A [`ConstraintSet`] with no transition constraints — for tables whose +/// soundness rests entirely on their bus (LogUp) interactions (e.g. BITWISE, +/// PAGE, REGISTER, the continuation GLOBAL_MEMORY / global L2G sub-tables). +/// The framework still appends the LogUp constraints; this contributes nothing +/// before them. +pub struct EmptyConstraints; + +impl ConstraintSet for EmptyConstraints { + fn meta(&self) -> Vec { + Vec::new() + } + fn eval>(&self, _b: &mut B) {} +} + // ============================================================================= // Shared AIR plumbing: run a ConstraintSet through the folders // ============================================================================= diff --git a/crypto/stark/src/debug.rs b/crypto/stark/src/debug.rs index bf1a454a7..d97d0e8d3 100644 --- a/crypto/stark/src/debug.rs +++ b/crypto/stark/src/debug.rs @@ -89,12 +89,11 @@ pub fn validate_trace< }); // --------- VALIDATE TRANSITION CONSTRAINTS ----------- - let n_transition_constraints = air.context().num_transition_constraints; - let exemption_steps: Vec = - std::iter::repeat_n(lde_trace.num_steps(), n_transition_constraints) - .zip(air.transition_constraints()) - .map(|(trace_steps, constraint)| trace_steps - constraint.end_exemptions()) - .collect(); + let exemption_steps: Vec = air + .constraints_meta() + .iter() + .map(|m| lde_trace.num_steps() - m.end_exemptions) + .collect(); let logup_alpha_powers: Vec> = if rap_challenges.len() > LOGUP_CHALLENGE_ALPHA { diff --git a/crypto/stark/src/examples/dummy_air.rs b/crypto/stark/src/examples/dummy_air.rs index 9b1f453c3..e898ade9f 100644 --- a/crypto/stark/src/examples/dummy_air.rs +++ b/crypto/stark/src/examples/dummy_air.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, @@ -7,7 +5,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -18,124 +15,8 @@ use math::field::{element::FieldElement, goldilocks::GoldilocksField, traits::Is type StarkField = GoldilocksField; -#[derive(Clone)] -struct FibConstraint { - phantom: PhantomData, -} -impl FibConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for FibConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - 2 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - let third_step = frame.get_evaluation_step(2); - - let a0 = first_step.get_main_evaluation_element(0, 1); - let a1 = second_step.get_main_evaluation_element(0, 1); - let a2 = third_step.get_main_evaluation_element(0, 1); - - let res = a2 - a1 - a0; - - transition_evaluations[self.constraint_idx()] = res; - } -} - -#[derive(Clone)] -struct BitConstraint { - phantom: PhantomData, -} -impl BitConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for BitConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - - let bit = first_step.get_main_evaluation_element(0, 0); - - let res = bit * (bit - FieldElement::::one()); - - transition_evaluations[self.constraint_idx()] = res; - } -} - -/// Single-body [`ConstraintSet`] for [`DummyAIR`]: the same constraints as -/// `FibConstraint` + `BitConstraint`, written once against the +/// Single-body [`ConstraintSet`] for [`DummyAIR`]: a fibonacci recurrence on +/// column 1 and an IS_BIT on column 0, written once against the /// [`ConstraintBuilder`]. #[derive(Default)] pub struct DummyConstraints; @@ -166,7 +47,7 @@ impl ConstraintSet for DummyConstraints { pub struct DummyAIR { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, } impl AIR for DummyAIR { @@ -179,24 +60,16 @@ impl AIR for DummyAIR { } fn new(proof_options: &ProofOptions) -> Self { - let transition_constraints: Vec< - Box>, - > = vec![ - Box::new(FibConstraint::new()), - Box::new(BitConstraint::new()), - ]; + let meta = DummyConstraints.meta(); let context = AirContext { proof_options: proof_options.clone(), trace_columns: 2, transition_offsets: vec![0, 1, 2], - num_transition_constraints: 2, + num_transition_constraints: meta.len(), }; - Self { - context, - transition_constraints, - } + Self { context, meta } } fn boundary_constraints( @@ -212,10 +85,8 @@ impl AIR for DummyAIR { BoundaryConstraints::from_constraints(vec![a0, a1]) } - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.transition_constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs index 94d820bf8..a1b88a586 100644 --- a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs +++ b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs @@ -5,7 +5,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -17,130 +16,6 @@ use math::{ traits::AsBytes, }; use std::marker::PhantomData; - -#[derive(Clone)] -struct ShiftedFibTransition1 { - phantom: PhantomData, -} - -impl ShiftedFibTransition1 { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for ShiftedFibTransition1 -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_row = frame.get_evaluation_step(0); - let second_row = frame.get_evaluation_step(1); - - let a0_1 = first_row.get_main_evaluation_element(0, 1); - let a1_0 = second_row.get_main_evaluation_element(0, 0); - - let res = a1_0 - a0_1; - - transition_evaluations[self.constraint_idx()] = res; - } -} - -#[derive(Clone)] -struct ShiftedFibTransition2 { - phantom: PhantomData, -} - -impl ShiftedFibTransition2 { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for ShiftedFibTransition2 -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 1 - } - - fn end_exemptions(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_row = frame.get_evaluation_step(0); - let second_row = frame.get_evaluation_step(1); - - let a0_0 = first_row.get_main_evaluation_element(0, 0); - let a0_1 = first_row.get_main_evaluation_element(0, 1); - let a1_1 = second_row.get_main_evaluation_element(0, 1); - - let res = a1_1 - a0_0 - a0_1; - - transition_evaluations[self.constraint_idx()] = res; - } -} - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct PublicInputs @@ -163,9 +38,9 @@ where } } -/// Single-body [`ConstraintSet`] for [`Fibonacci2ColsShifted`]: the same -/// constraints as `ShiftedFibTransition1` + `ShiftedFibTransition2`, written -/// once against the [`ConstraintBuilder`]. +/// Single-body [`ConstraintSet`] for [`Fibonacci2ColsShifted`]: the two +/// shifted-Fibonacci recurrences, written once against the +/// [`ConstraintBuilder`]. pub struct Fibonacci2ColsShiftedConstraints { phantom: PhantomData, } @@ -209,7 +84,8 @@ where F: IsFFTField, { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } /// The AIR for to a 2 column trace, where each column is a Fibonacci sequence and the @@ -229,23 +105,19 @@ where } fn new(proof_options: &ProofOptions) -> Self { - let transition_constraints: Vec< - Box>, - > = vec![ - Box::new(ShiftedFibTransition1::new()), - Box::new(ShiftedFibTransition2::new()), - ]; + let meta = Fibonacci2ColsShiftedConstraints::::default().meta(); let context = AirContext { proof_options: proof_options.clone(), transition_offsets: vec![0, 1], - num_transition_constraints: 2, + num_transition_constraints: meta.len(), trace_columns: 2, }; Self { context, - transition_constraints, + meta, + phantom: PhantomData, } } @@ -266,10 +138,8 @@ where BoundaryConstraints::from_constraints(vec![initial_condition, claimed_value_constraint]) } - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.transition_constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/examples/fibonacci_2_columns.rs b/crypto/stark/src/examples/fibonacci_2_columns.rs index c17397144..0e568c1f4 100644 --- a/crypto/stark/src/examples/fibonacci_2_columns.rs +++ b/crypto/stark/src/examples/fibonacci_2_columns.rs @@ -8,7 +8,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -17,135 +16,8 @@ use crate::{ }; use math::field::{element::FieldElement, traits::IsFFTField}; -#[derive(Clone)] -struct FibTransition1 { - phantom: PhantomData, -} - -impl FibTransition1 { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for FibTransition1 -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - // s_{0, i+1} = s_{0, i} + s_{1, i} - let s0_0 = first_step.get_main_evaluation_element(0, 0); - let s0_1 = first_step.get_main_evaluation_element(0, 1); - let s1_0 = second_step.get_main_evaluation_element(0, 0); - - let res = s1_0 - s0_0 - s0_1; - - transition_evaluations[self.constraint_idx()] = res; - } -} - -#[derive(Clone)] -struct FibTransition2 { - phantom: PhantomData, -} - -impl FibTransition2 { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for FibTransition2 -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 1 - } - - fn end_exemptions(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - // s_{1, i+1} = s_{1, i} + s_{0, i+1} - let s0_1 = first_step.get_main_evaluation_element(0, 1); - let s1_0 = second_step.get_main_evaluation_element(0, 0); - let s1_1 = second_step.get_main_evaluation_element(0, 1); - - let res = s1_1 - s0_1 - s1_0; - - transition_evaluations[self.constraint_idx()] = res; - } -} - -/// Single-body [`ConstraintSet`] for [`Fibonacci2ColsAIR`]: the same -/// constraints as `FibTransition1` + `FibTransition2`, written once against -/// the [`ConstraintBuilder`]. +/// Single-body [`ConstraintSet`] for [`Fibonacci2ColsAIR`]: the two row-major +/// Fibonacci recurrences, written once against the [`ConstraintBuilder`]. pub struct Fibonacci2ColsConstraints { phantom: PhantomData, } @@ -189,7 +61,8 @@ where F: IsFFTField, { context: AirContext, - constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } /// The AIR for to a 2 column trace, where the columns form a Fibonacci sequence when @@ -207,23 +80,19 @@ where } fn new(proof_options: &ProofOptions) -> Self { - let constraints: Vec< - Box>, - > = vec![ - Box::new(FibTransition1::new()), - Box::new(FibTransition2::new()), - ]; + let meta = Fibonacci2ColsConstraints::::default().meta(); let context = AirContext { proof_options: proof_options.clone(), transition_offsets: vec![0, 1], - num_transition_constraints: constraints.len(), + num_transition_constraints: meta.len(), trace_columns: 2, }; Self { context, - constraints, + meta, + phantom: PhantomData, } } @@ -240,8 +109,8 @@ where BoundaryConstraints::from_constraints(vec![a0, a1]) } - fn transition_constraints(&self) -> &Vec>> { - &self.constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/examples/fibonacci_multi_column.rs b/crypto/stark/src/examples/fibonacci_multi_column.rs index 61426a03e..d5824b3a4 100644 --- a/crypto/stark/src/examples/fibonacci_multi_column.rs +++ b/crypto/stark/src/examples/fibonacci_multi_column.rs @@ -7,7 +7,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -19,102 +18,6 @@ use math::field::{ traits::{IsFFTField, IsField, IsSubFieldOf}, }; -/// Transition constraint for a single Fibonacci column. -/// Enforces: col[i+2] = col[i+1] + col[i] -#[derive(Clone)] -pub struct FibColumnConstraint -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - column_idx: usize, - constraint_idx: usize, - phantom_f: PhantomData, - phantom_e: PhantomData, -} - -impl FibColumnConstraint -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - pub fn new(column_idx: usize, constraint_idx: usize) -> Self { - Self { - column_idx, - constraint_idx, - phantom_f: PhantomData, - phantom_e: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for FibColumnConstraint -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn end_exemptions(&self) -> usize { - 2 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values: _, - rap_challenges: _, - .. - } => { - let step_0 = frame.get_evaluation_step(0); - let step_1 = frame.get_evaluation_step(1); - let step_2 = frame.get_evaluation_step(2); - - // Get the values from the column at each step - let a0 = step_0.get_main_evaluation_element(0, self.column_idx); - let a1 = step_1.get_main_evaluation_element(0, self.column_idx); - let a2 = step_2.get_main_evaluation_element(0, self.column_idx); - - // Constraint: a2 = a1 + a0 => a2 - a1 - a0 = 0 - let res = a2 - a1 - a0; - - transition_evaluations[self.constraint_idx] = res.to_extension(); - } - TransitionEvaluationContext::Verifier { - frame, - periodic_values: _, - rap_challenges: _, - .. - } => { - let step_0 = frame.get_evaluation_step(0); - let step_1 = frame.get_evaluation_step(1); - let step_2 = frame.get_evaluation_step(2); - - // Get the values from the column at each step - let a0 = step_0.get_main_evaluation_element(0, self.column_idx); - let a1 = step_1.get_main_evaluation_element(0, self.column_idx); - let a2 = step_2.get_main_evaluation_element(0, self.column_idx); - - // Constraint: a2 = a1 + a0 => a2 - a1 - a0 = 0 - let res = a2 - a1 - a0; - - transition_evaluations[self.constraint_idx] = res; - } - } - } -} - /// Public inputs for the multi-column Fibonacci AIR. /// Contains the initial values (first two elements) for each column. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -125,8 +28,8 @@ pub struct FibonacciMultiColumnPublicInputs { } /// Single-body [`ConstraintSet`] for [`FibonacciMultiColumnAIR`]: one -/// Fibonacci constraint per column (the same constraints as -/// [`FibColumnConstraint`]), written once against the [`ConstraintBuilder`]. +/// Fibonacci constraint per column, written once against the +/// [`ConstraintBuilder`]. pub struct FibonacciMultiColumnConstraints { pub num_columns: usize, } @@ -163,8 +66,9 @@ where E: IsField + Send + Sync, { context: AirContext, - constraints: Vec>>, + meta: Vec, num_columns: usize, + phantom: PhantomData<(F, E)>, } impl AIR for FibonacciMultiColumnAIR @@ -189,8 +93,8 @@ where trace_length } - fn transition_constraints(&self) -> &Vec>> { - &self.constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( @@ -275,25 +179,20 @@ where { /// Creates a new multi-column Fibonacci AIR with the specified number of columns. pub fn with_num_columns(proof_options: &ProofOptions, num_columns: usize) -> Self { - // Create one constraint per column - let constraints: Vec>> = (0..num_columns) - .map(|col_idx| { - Box::new(FibColumnConstraint::new(col_idx, col_idx)) - as Box> - }) - .collect(); + let meta = ConstraintSet::::meta(&FibonacciMultiColumnConstraints { num_columns }); let context = AirContext { proof_options: proof_options.clone(), trace_columns: num_columns, transition_offsets: vec![0, 1, 2], - num_transition_constraints: num_columns, + num_transition_constraints: meta.len(), }; Self { context, - constraints, + meta, num_columns, + phantom: PhantomData, } } } diff --git a/crypto/stark/src/examples/fibonacci_rap.rs b/crypto/stark/src/examples/fibonacci_rap.rs index 5770d845e..e32709caf 100644 --- a/crypto/stark/src/examples/fibonacci_rap.rs +++ b/crypto/stark/src/examples/fibonacci_rap.rs @@ -7,7 +7,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -29,139 +28,8 @@ fn resize_to_next_power_of_two(trace_columns: &mut [Vec { - phantom: PhantomData, -} - -impl FibConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for FibConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - // NOTE: This is hard-coded for the example of steps = 16 in the integration tests. - // If that number changes in the test, this should be changed too or the test will fail. - 3 + 32 - 16 - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - let third_step = frame.get_evaluation_step(2); - - let a0 = first_step.get_main_evaluation_element(0, 0); - let a1 = second_step.get_main_evaluation_element(0, 0); - let a2 = third_step.get_main_evaluation_element(0, 0); - - let res = a2 - a1 - a0; - - transition_evaluations[self.constraint_idx()] = res; - } -} - -#[derive(Clone)] -struct PermutationConstraint { - phantom: PhantomData, -} - -impl PermutationConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for PermutationConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 1 - } - - fn end_exemptions(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - // Auxiliary constraints - let z_i = first_step.get_aux_evaluation_element(0, 0); - let z_i_plus_one = second_step.get_aux_evaluation_element(0, 0); - let gamma = &rap_challenges[0]; - - let a_i = first_step.get_main_evaluation_element(0, 0); - let b_i = first_step.get_main_evaluation_element(0, 1); - - let res = z_i_plus_one * (b_i + gamma) - z_i * (a_i + gamma); - - transition_evaluations[self.constraint_idx()] = res; - } -} - -/// Single-body [`ConstraintSet`] for [`FibonacciRAP`]: the same constraints -/// as `FibConstraint` + `PermutationConstraint`, written once against the +/// Single-body [`ConstraintSet`] for [`FibonacciRAP`]: the Fibonacci +/// recurrence plus the RAP permutation constraint, written once against the /// [`ConstraintBuilder`]. The permutation constraint reads the auxiliary /// (RAP) column and the interaction challenge, so it is an `Ext` constraint /// after the `Base` prefix. @@ -174,7 +42,7 @@ where fn meta(&self) -> Vec { vec![ // idx 0: fibonacci; end exemptions hard-coded for the steps = 16 - // integration tests (see `FibConstraint::end_exemptions`). + // integration tests. ConstraintMeta::base(0, 1).with_end_exemptions(3 + 32 - 16 - 1), // idx 1: permutation; reads the next row ⇒ 1 end exemption. ConstraintMeta::ext(1, 2).with_end_exemptions(1), @@ -206,7 +74,8 @@ where F: IsFFTField, { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -234,23 +103,19 @@ where } fn new(proof_options: &ProofOptions) -> Self { - let transition_constraints: Vec< - Box>, - > = vec![ - Box::new(FibConstraint::new()), - Box::new(PermutationConstraint::new()), - ]; + let meta = ConstraintSet::::meta(&FibonacciRAPConstraints); let context = AirContext { proof_options: proof_options.clone(), trace_columns: 3, transition_offsets: vec![0, 1, 2], - num_transition_constraints: transition_constraints.len(), + num_transition_constraints: meta.len(), }; Self { context, - transition_constraints, + meta, + phantom: PhantomData, } } @@ -317,10 +182,8 @@ where BoundaryConstraints::from_constraints(vec![a0, a1, a0_aux]) } - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.transition_constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/examples/multi_table_lookup.rs b/crypto/stark/src/examples/multi_table_lookup.rs index d02ea13f0..91b0b9999 100644 --- a/crypto/stark/src/examples/multi_table_lookup.rs +++ b/crypto/stark/src/examples/multi_table_lookup.rs @@ -5,7 +5,7 @@ //! together with `AirWithBuses` in the engine-switch phase. use crate::{ - constraints::transition::TransitionConstraintEvaluator, + constraints::builder::EmptyConstraints, lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -33,9 +33,7 @@ impl From for u64 { pub fn new_cpu_air_with_lookup( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Interaction with ADD table (CPU sends to ADD bus) @@ -58,15 +56,13 @@ pub fn new_cpu_air_with_lookup( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } pub fn new_mul_air_with_lookup( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Interaction with CPU table (MUL table receives from MUL bus) @@ -83,15 +79,13 @@ pub fn new_mul_air_with_lookup( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } pub fn new_add_air_with_lookup( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Interaction with CPU table (ADD table receives from ADD bus) @@ -108,6 +102,6 @@ pub fn new_add_air_with_lookup( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } diff --git a/crypto/stark/src/examples/quadratic_air.rs b/crypto/stark/src/examples/quadratic_air.rs index c44d711d6..4818baed1 100644 --- a/crypto/stark/src/examples/quadratic_air.rs +++ b/crypto/stark/src/examples/quadratic_air.rs @@ -7,7 +7,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -16,69 +15,8 @@ use crate::{ }; use math::field::{element::FieldElement, traits::IsFFTField}; -#[derive(Clone)] -struct QuadraticConstraint { - phantom: PhantomData, -} - -impl QuadraticConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for QuadraticConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - let x = first_step.get_main_evaluation_element(0, 0); - let x_squared = second_step.get_main_evaluation_element(0, 0); - - let res = x_squared - x * x; - - transition_evaluations[self.constraint_idx()] = res; - } -} - -/// Single-body [`ConstraintSet`] for [`QuadraticAIR`]: the same constraint -/// as `QuadraticConstraint`, written once against the [`ConstraintBuilder`]. +/// Single-body [`ConstraintSet`] for [`QuadraticAIR`]: `x_{i+1} = x_i²`, +/// written once against the [`ConstraintBuilder`]. pub struct QuadraticConstraints { phantom: PhantomData, } @@ -112,7 +50,8 @@ where F: IsFFTField, { context: AirContext, - constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -137,20 +76,19 @@ where } fn new(proof_options: &ProofOptions) -> Self { - let constraints: Vec< - Box>, - > = vec![Box::new(QuadraticConstraint::new())]; + let meta = QuadraticConstraints::::default().meta(); let context = AirContext { proof_options: proof_options.clone(), trace_columns: 1, transition_offsets: vec![0, 1], - num_transition_constraints: constraints.len(), + num_transition_constraints: meta.len(), }; Self { context, - constraints, + meta, + phantom: PhantomData, } } @@ -166,10 +104,8 @@ where BoundaryConstraints::from_constraints(vec![a0]) } - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/examples/read_only_memory.rs b/crypto/stark/src/examples/read_only_memory.rs index 3a476cc31..dd74a1b37 100644 --- a/crypto/stark/src/examples/read_only_memory.rs +++ b/crypto/stark/src/examples/read_only_memory.rs @@ -7,7 +7,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -21,219 +20,11 @@ use math::{ traits::ByteConversion, }; -/// This condition ensures the continuity in a read-only memory structure, preserving strict ordering. -/// Equation based on Cairo Whitepaper section 9.7.2 -#[derive(Clone)] -struct ContinuityConstraint { - phantom: PhantomData, -} - -impl ContinuityConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for ContinuityConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - // NOTE: We are assuming that the trace has as length a power of 2. - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - let a_sorted_0 = first_step.get_main_evaluation_element(0, 2); - let a_sorted_1 = second_step.get_main_evaluation_element(0, 2); - // (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address - let res = (a_sorted_1 - a_sorted_0) * (a_sorted_1 - a_sorted_0 - FieldElement::::one()); - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res; - } - } -} -/// Transition constraint that ensures that same addresses have same values, making the memory read-only. -/// Equation based on Cairo Whitepaper section 9.7.2 -#[derive(Clone)] -struct SingleValueConstraint { - phantom: PhantomData, -} - -impl SingleValueConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for SingleValueConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 1 - } - - fn end_exemptions(&self) -> usize { - // NOTE: We are assuming that the trace has as length a power of 2. - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - let a_sorted0 = first_step.get_main_evaluation_element(0, 2); - let a_sorted1 = second_step.get_main_evaluation_element(0, 2); - let v_sorted0 = first_step.get_main_evaluation_element(0, 3); - let v_sorted1 = second_step.get_main_evaluation_element(0, 3); - // (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 - let res = (v_sorted1 - v_sorted0) * (a_sorted1 - a_sorted0 - FieldElement::::one()); - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res; - } - } -} -/// Permutation constraint ensures that the values are permuted in the memory. -/// Equation based on Cairo Whitepaper section 9.7.2 -#[derive(Clone)] -struct PermutationConstraint { - phantom: PhantomData, -} - -impl PermutationConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for PermutationConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 2 - } - - fn end_exemptions(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - // Auxiliary constraints - let p0 = first_step.get_aux_evaluation_element(0, 0); - let p1 = second_step.get_aux_evaluation_element(0, 0); - let z = &rap_challenges[0]; - let alpha = &rap_challenges[1]; - let a1 = second_step.get_main_evaluation_element(0, 0); - let v1 = second_step.get_main_evaluation_element(0, 1); - let a_sorted_1 = second_step.get_main_evaluation_element(0, 2); - let v_sorted_1 = second_step.get_main_evaluation_element(0, 3); - // (z - (a'_{i+1} + α * v'_{i+1})) * p_{i+1} = (z - (a_{i+1} + α * v_{i+1})) * p_i - let res = (z - (a_sorted_1 + alpha * v_sorted_1)) * p1 - (z - (a1 + alpha * v1)) * p0; - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res; - } - } -} - -/// Single-body [`ConstraintSet`] for [`ReadOnlyRAP`]: the same constraints -/// as `ContinuityConstraint` + `SingleValueConstraint` + -/// `PermutationConstraint`, written once against the [`ConstraintBuilder`]. -/// The permutation constraint reads the auxiliary (RAP) column and the -/// interaction challenges, so it is an `Ext` constraint after the `Base` -/// prefix. +/// Single-body [`ConstraintSet`] for [`ReadOnlyRAP`]: the continuity, +/// single-value and permutation constraints, written once against the +/// [`ConstraintBuilder`]. The permutation constraint reads the auxiliary +/// (RAP) column and the interaction challenges, so it is an `Ext` constraint +/// after the `Base` prefix. pub struct ReadOnlyRAPConstraints; impl ConstraintSet for ReadOnlyRAPConstraints @@ -282,7 +73,8 @@ where F: IsFFTField, { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -311,24 +103,19 @@ where } fn new(proof_options: &ProofOptions) -> Self { - let transition_constraints: Vec< - Box>, - > = vec![ - Box::new(ContinuityConstraint::new()), - Box::new(SingleValueConstraint::new()), - Box::new(PermutationConstraint::new()), - ]; + let meta = ConstraintSet::::meta(&ReadOnlyRAPConstraints); let context = AirContext { proof_options: proof_options.clone(), trace_columns: 5, transition_offsets: vec![0, 1], - num_transition_constraints: transition_constraints.len(), + num_transition_constraints: meta.len(), }; Self { context, - transition_constraints, + meta, + phantom: PhantomData, } } @@ -416,10 +203,8 @@ where BoundaryConstraints::from_constraints(vec![c1, c2, c3, c4, c_aux1, c_aux2]) } - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.transition_constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/examples/read_only_memory_logup.rs b/crypto/stark/src/examples/read_only_memory_logup.rs index 72eb35995..2ad9a28c3 100644 --- a/crypto/stark/src/examples/read_only_memory_logup.rs +++ b/crypto/stark/src/examples/read_only_memory_logup.rs @@ -11,7 +11,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -28,337 +27,11 @@ use math::{ traits::ByteConversion, }; -/// Transition Constraint that ensures the continuity of the sorted address column of a memory. -#[derive(Clone)] -struct ContinuityConstraint + IsFFTField + Send + Sync, E: IsField + Send + Sync> -{ - phantom_f: PhantomData, - phantom_e: PhantomData, -} - -impl ContinuityConstraint -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - pub fn new() -> Self { - Self { - phantom_f: PhantomData::, - phantom_e: PhantomData::, - } - } -} - -impl TransitionConstraintEvaluator for ContinuityConstraint -where - F: IsFFTField + IsSubFieldOf + Send + Sync, - E: IsField + Send + Sync, -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - // NOTE: We are assuming that the trace has as length a power of 2. - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - // In both evaluation contexts, Prover and Verfier will evaluate the transition polynomial in the same way. - // The only difference is that the Prover's Frame has base field and field extension elements, - // while the Verfier's Frame has only field extension elements. - match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values: _periodic_values, - rap_challenges: _rap_challenges, - .. - } => { - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - let a_sorted_0 = first_step.get_main_evaluation_element(0, 2); - let a_sorted_1 = second_step.get_main_evaluation_element(0, 2); - // (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address - let res = (a_sorted_1 - a_sorted_0) - * (a_sorted_1 - a_sorted_0 - FieldElement::::one()); - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res.to_extension(); - } - } - - TransitionEvaluationContext::Verifier { - frame, - periodic_values: _periodic_values, - rap_challenges: _rap_challenges, - .. - } => { - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - let a_sorted_0 = first_step.get_main_evaluation_element(0, 2); - let a_sorted_1 = second_step.get_main_evaluation_element(0, 2); - // (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address - let res = (a_sorted_1 - a_sorted_0) - * (a_sorted_1 - a_sorted_0 - FieldElement::::one()); - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res; - } - } - } - } -} -/// Transition constraint that ensures that same addresses have same values, making the sorted memory read-only. -#[derive(Clone)] -struct SingleValueConstraint< - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -> { - phantom_f: PhantomData, - phantom_e: PhantomData, -} - -impl SingleValueConstraint -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - pub fn new() -> Self { - Self { - phantom_f: PhantomData::, - phantom_e: PhantomData::, - } - } -} - -impl TransitionConstraintEvaluator for SingleValueConstraint -where - F: IsFFTField + IsSubFieldOf + Send + Sync, - E: IsField + Send + Sync, -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - 1 - } - - fn end_exemptions(&self) -> usize { - // NOTE: We are assuming that the trace has as length a power of 2. - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - // In both evaluation contexts, Prover and Verfier will evaluate the transition polynomial in the same way. - // The only difference is that the Prover's Frame has base field and field extension elements, - // while the Verfier's Frame has only field extension elements. - match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values: _periodic_values, - rap_challenges: _rap_challenges, - .. - } => { - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - let a_sorted_0 = first_step.get_main_evaluation_element(0, 2); - let a_sorted_1 = second_step.get_main_evaluation_element(0, 2); - let v_sorted_0 = first_step.get_main_evaluation_element(0, 3); - let v_sorted_1 = second_step.get_main_evaluation_element(0, 3); - // (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 - let res = (v_sorted_1 - v_sorted_0) - * (a_sorted_1 - a_sorted_0 - FieldElement::::one()); - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res.to_extension(); - } - } - - TransitionEvaluationContext::Verifier { - frame, - periodic_values: _periodic_values, - rap_challenges: _rap_challenges, - .. - } => { - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - let a_sorted_0 = first_step.get_main_evaluation_element(0, 2); - let a_sorted_1 = second_step.get_main_evaluation_element(0, 2); - let v_sorted_0 = first_step.get_main_evaluation_element(0, 3); - let v_sorted_1 = second_step.get_main_evaluation_element(0, 3); - // (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 - let res = (v_sorted_1 - v_sorted_0) - * (a_sorted_1 - a_sorted_0 - FieldElement::::one()); - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res; - } - } - } - } -} -/// Transition constraint that ensures that the sorted columns are a permutation of the original ones. -/// We are using the LogUp construction described in: -/// . -/// See also our post of LogUp argument in blog.lambdaclass.com. -#[derive(Clone)] -struct PermutationConstraint< - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -> { - phantom_f: PhantomData, - phantom_e: PhantomData, -} - -impl PermutationConstraint -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - pub fn new() -> Self { - Self { - phantom_f: PhantomData::, - phantom_e: PhantomData::, - } - } -} - -impl TransitionConstraintEvaluator for PermutationConstraint -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - fn degree(&self) -> usize { - 3 - } - - fn constraint_idx(&self) -> usize { - 2 - } - - fn end_exemptions(&self) -> usize { - 1 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - // In both evaluation contexts, Prover and Verfier will evaluate the transition polynomial in the same way. - // The only difference is that the Prover's Frame has base field and field extension elements, - // while the Verfier's Frame has only field extension elements. - match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values: _periodic_values, - rap_challenges, - .. - } => { - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - // Auxiliary frame elements - let s0 = first_step.get_aux_evaluation_element(0, 0); - let s1 = second_step.get_aux_evaluation_element(0, 0); - - // Challenges - let z = &rap_challenges[0]; - let alpha = &rap_challenges[1]; - - // Main frame elements - let a1 = second_step.get_main_evaluation_element(0, 0); - let v1 = second_step.get_main_evaluation_element(0, 1); - let a_sorted_1 = second_step.get_main_evaluation_element(0, 2); - let v_sorted_1 = second_step.get_main_evaluation_element(0, 3); - let m = second_step.get_main_evaluation_element(0, 4); - - let unsorted_term = -(a1 + v1 * alpha) + z; - let sorted_term = -(a_sorted_1 + v_sorted_1 * alpha) + z; - - // We are using the following LogUp equation: - // s1 = s0 + m / sorted_term - 1/unsorted_term. - // Since constraints must be expressed without division, we multiply each term by sorted_term * unsorted_term: - let res = s0 * &unsorted_term * &sorted_term + m * &unsorted_term - - &sorted_term - - s1 * unsorted_term * sorted_term; - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res; - } - } - - TransitionEvaluationContext::Verifier { - frame, - periodic_values: _periodic_values, - rap_challenges, - .. - } => { - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - - // Auxiliary frame elements - let s0 = first_step.get_aux_evaluation_element(0, 0); - let s1 = second_step.get_aux_evaluation_element(0, 0); - - // Challenges - let z = &rap_challenges[0]; - let alpha = &rap_challenges[1]; - - // Main frame elements - let a1 = second_step.get_main_evaluation_element(0, 0); - let v1 = second_step.get_main_evaluation_element(0, 1); - let a_sorted_1 = second_step.get_main_evaluation_element(0, 2); - let v_sorted_1 = second_step.get_main_evaluation_element(0, 3); - let m = second_step.get_main_evaluation_element(0, 4); - - let unsorted_term = z - (a1 + alpha * v1); - let sorted_term = z - (a_sorted_1 + alpha * v_sorted_1); - - // We are using the following LogUp equation: - // s1 = s0 + m / sorted_term - 1/unsorted_term. - // Since constraints must be expressed without division, we multiply each term by sorted_term * unsorted_term: - let res = s0 * &unsorted_term * &sorted_term + m * &unsorted_term - - &sorted_term - - s1 * unsorted_term * sorted_term; - - // The eval always exists, except if the constraint idx were incorrectly defined. - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx()) { - *eval = res; - } - } - } - } -} - -/// Single-body [`ConstraintSet`] for [`LogReadOnlyRAP`]: the same -/// constraints as `ContinuityConstraint` + `SingleValueConstraint` + -/// `PermutationConstraint`, written once against the [`ConstraintBuilder`]. -/// The LogUp permutation constraint reads the auxiliary column and the -/// interaction challenges, so it is an `Ext` constraint after the `Base` -/// prefix. +/// Single-body [`ConstraintSet`] for [`LogReadOnlyRAP`]: the continuity, +/// single-value and LogUp permutation constraints, written once against the +/// [`ConstraintBuilder`]. The LogUp permutation constraint reads the auxiliary +/// column and the interaction challenges, so it is an `Ext` constraint after +/// the `Base` prefix. pub struct LogReadOnlyRAPConstraints; impl ConstraintSet for LogReadOnlyRAPConstraints @@ -420,7 +93,8 @@ where E: IsField + Send + Sync, { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, + phantom: PhantomData<(F, E)>, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -452,24 +126,19 @@ where } fn new(proof_options: &ProofOptions) -> Self { - let transition_constraints: Vec< - Box>, - > = vec![ - Box::new(ContinuityConstraint::new()), - Box::new(SingleValueConstraint::new()), - Box::new(PermutationConstraint::new()), - ]; + let meta = ConstraintSet::::meta(&LogReadOnlyRAPConstraints); let context = AirContext { proof_options: proof_options.clone(), trace_columns: 6, transition_offsets: vec![0, 1], - num_transition_constraints: transition_constraints.len(), + num_transition_constraints: meta.len(), }; Self { context, - transition_constraints, + meta, + phantom: PhantomData, } } @@ -566,10 +235,8 @@ where BoundaryConstraints::from_constraints(vec![c1, c2, c3, c4, c5, c_aux1, c_aux2]) } - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.transition_constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/examples/simple_addition.rs b/crypto/stark/src/examples/simple_addition.rs index 5cb9df41f..c9eb019ff 100644 --- a/crypto/stark/src/examples/simple_addition.rs +++ b/crypto/stark/src/examples/simple_addition.rs @@ -10,7 +10,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -19,69 +18,8 @@ use crate::{ }; use math::field::{element::FieldElement, traits::IsFFTField}; -/// Transition constraint: col0 + col1 = col2 -/// This constraint is applied at every row (end_exemptions = 0). -#[derive(Clone)] -struct AdditionConstraint { - phantom: PhantomData, -} - -impl AdditionConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for AdditionConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let current_step = frame.get_evaluation_step(0); - - let col0 = current_step.get_main_evaluation_element(0, 0); - let col1 = current_step.get_main_evaluation_element(0, 1); - let col2 = current_step.get_main_evaluation_element(0, 2); - - // Constraint: col0 + col1 - col2 = 0 - let res = col0 + col1 - col2; - - transition_evaluations[self.constraint_idx()] = res; - } -} - -/// Single-body [`ConstraintSet`] for [`SimpleAdditionAIR`]: the same -/// constraint as `AdditionConstraint`, written once against the -/// [`ConstraintBuilder`]. +/// Single-body [`ConstraintSet`] for [`SimpleAdditionAIR`]: `col0 + col1 = col2` +/// (applied at every row), written once against the [`ConstraintBuilder`]. pub struct SimpleAdditionConstraints { phantom: PhantomData, } @@ -117,7 +55,8 @@ where F: IsFFTField, { context: AirContext, - constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -145,20 +84,19 @@ where } fn new(proof_options: &ProofOptions) -> Self { - let constraints: Vec< - Box>, - > = vec![Box::new(AdditionConstraint::new())]; + let meta = SimpleAdditionConstraints::::default().meta(); let context = AirContext { proof_options: proof_options.clone(), trace_columns: 3, // col0, col1, col2 transition_offsets: vec![0], // Only need current step - num_transition_constraints: constraints.len(), + num_transition_constraints: meta.len(), }; Self { context, - constraints, + meta, + phantom: PhantomData, } } @@ -177,10 +115,8 @@ where BoundaryConstraints::from_constraints(vec![a0, a1]) } - fn transition_constraints( - &self, - ) -> &Vec>> { - &self.constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/examples/simple_fibonacci.rs b/crypto/stark/src/examples/simple_fibonacci.rs index f10dc3179..bcb5ff5b6 100644 --- a/crypto/stark/src/examples/simple_fibonacci.rs +++ b/crypto/stark/src/examples/simple_fibonacci.rs @@ -5,7 +5,6 @@ use crate::{ ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, run_transition_prover, run_transition_verifier, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -15,71 +14,8 @@ use crate::{ use math::field::{element::FieldElement, traits::IsFFTField}; use std::marker::PhantomData; -#[derive(Clone)] -struct FibConstraint { - phantom: PhantomData, -} - -impl FibConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for FibConstraint -where - F: IsFFTField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 - } - - fn constraint_idx(&self) -> usize { - 0 - } - - fn end_exemptions(&self) -> usize { - 2 - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - let (frame, _periodic_values, _rap_challenges) = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - periodic_values, - rap_challenges, - .. - } - | TransitionEvaluationContext::Verifier { - frame, - periodic_values, - rap_challenges, - .. - } => (frame, periodic_values, rap_challenges), - }; - - let first_step = frame.get_evaluation_step(0); - let second_step = frame.get_evaluation_step(1); - let third_step = frame.get_evaluation_step(2); - - let a0 = first_step.get_main_evaluation_element(0, 0); - let a1 = second_step.get_main_evaluation_element(0, 0); - let a2 = third_step.get_main_evaluation_element(0, 0); - - let res = a2 - a1 - a0; - - transition_evaluations[self.constraint_idx()] = res; - } -} - -/// Single-body [`ConstraintSet`] for [`FibonacciAIR`]: the same constraint -/// as `FibConstraint`, written once against the [`ConstraintBuilder`]. +/// Single-body [`ConstraintSet`] for [`FibonacciAIR`]: `a_{i+2} = a_{i+1} + a_i`, +/// written once against the [`ConstraintBuilder`]. pub struct SimpleFibonacciConstraints { phantom: PhantomData, } @@ -114,7 +50,8 @@ where F: IsFFTField, { context: AirContext, - constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -140,19 +77,19 @@ where } fn new(proof_options: &ProofOptions) -> Self { - let constraints: Vec>> = - vec![Box::new(FibConstraint::new())]; + let meta = SimpleFibonacciConstraints::::default().meta(); let context = AirContext { proof_options: proof_options.clone(), trace_columns: 1, transition_offsets: vec![0, 1, 2], - num_transition_constraints: constraints.len(), + num_transition_constraints: meta.len(), }; Self { context, - constraints, + meta, + phantom: PhantomData, } } @@ -160,8 +97,8 @@ where trace_length } - fn transition_constraints(&self) -> &Vec>> { - &self.constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } fn compute_transition_prover( diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 27ca3b488..2929c2218 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -5,6 +5,9 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, + builder::{ + ConstraintMeta, ConstraintSet, ProverEvalFolder, VerifierEvalFolder, num_base_from_meta, + }, transition::TransitionConstraintEvaluator, }, context::AirContext, @@ -801,19 +804,35 @@ impl BusValue { // ============================================================================= /// Struct representing an AIR with Lookup. Contains own implementation of boundary constraints and auxiliary trace building +/// +/// `CS` is the table's [`ConstraintSet`]: its single `eval` body emits the +/// table's base-field transition constraints, and the framework appends the +/// LogUp constraints (generated from [`Self::logup`]) after them. One body +/// serves the compiled prover folder, the verifier folder, and IR capture. pub struct AirWithBuses< F: IsFFTField + IsSubFieldOf + IsPrimeField + Send + Sync, E: IsField + Send + Sync, B: BoundaryConstraintBuilder, PI, + CS: ConstraintSet, > { context: AirContext, step_size: usize, trace_layout: (usize, usize), - transition_constraints: Vec>>, - /// Number of domain (base-field) constraints. These come before LogUp constraints - /// in the transition_constraints vec and use the cheaper F×E accumulation path. - num_base_constraints: usize, + /// The table's single-source constraint set (base-field constraints). + constraint_set: CS, + /// The LogUp layout: the framework generates the LogUp (extension) + /// constraints from this and appends them after the `constraint_set` ones. + logup: LogUpLayout, + /// Idx-ordered metadata for all transition constraints: + /// `constraint_set.meta()` (base prefix) followed by `logup_meta` (ext). + meta: Vec, + /// Number of base-field constraints (the `RootKind::Base` prefix length of + /// `meta`) — these use the cheaper F×E accumulation path. + num_base: usize, + /// Lazily captured flat IR of every transition constraint, built once on + /// first request (prover/GPU/tests only — the verify path never forces it). + constraint_program: std::sync::OnceLock>, auxiliary_trace_build_data: AuxiliaryTraceBuildData, boundary_constraint_builder: PhantomData<(B, PI)>, /// Commitment to precomputed columns (if this is a preprocessed table) @@ -832,7 +851,8 @@ impl< E: IsField + Send + Sync + 'static, B: BoundaryConstraintBuilder, PI, -> AirWithBuses + CS: ConstraintSet, +> AirWithBuses { /// Creates an AirWithBuses with LogUp-specific transition constraints. /// If no boundary constraints are needed, use `NullBoundaryConstraintBuilder` as B and () as PI. @@ -850,41 +870,20 @@ impl< auxiliary_trace_build_data: AuxiliaryTraceBuildData, proof_options: &ProofOptions, step_size: usize, - mut transition_constraints: Vec>>, + constraint_set: CS, ) -> Self { - // Domain constraints are passed in first; LogUp constraints are appended below. - // The domain constraints use the F×E accumulation path (3 muls vs 9). - let num_base_constraints = transition_constraints.len(); - + // Base-field (table) constraints come from the constraint set; LogUp + // (extension) constraints are appended by the framework from the layout. let num_interactions = auxiliary_trace_build_data.interactions.len(); + let logup = LogUpLayout::from_interactions(auxiliary_trace_build_data.interactions.clone()); + let num_term_columns = logup.num_term_columns; - // Split interactions: committed pairs get term columns, last 1-2 are absorbed - let (num_committed_pairs, absorbed_count) = split_interactions(num_interactions); - let absorbed = - auxiliary_trace_build_data.interactions[num_interactions - absorbed_count..].to_vec(); - - // Create batched term constraints for committed pairs only - for pair_idx in 0..num_committed_pairs { - let constraint = LookupBatchedTermConstraint::new( - auxiliary_trace_build_data.interactions[pair_idx * 2].clone(), - auxiliary_trace_build_data.interactions[pair_idx * 2 + 1].clone(), - pair_idx, - transition_constraints.len(), - ); - transition_constraints.push(Box::new(constraint)); - } - - let num_term_columns = num_committed_pairs; - - // Add the accumulated constraint with absorbed interactions - if num_interactions > 0 { - let accumulated_constraint = LookupAccumulatedConstraint::new( - transition_constraints.len(), - num_term_columns, - absorbed, - ); - transition_constraints.push(Box::new(accumulated_constraint)); - } + // meta = constraint_set base-prefix meta + appended LogUp ext meta. + let mut meta = constraint_set.meta(); + let num_base = num_base_from_meta(&meta); + // The set is entirely base-field (its meta is a Base prefix). + debug_assert_eq!(num_base, meta.len(), "constraint set meta must be all-base"); + meta.extend(logup_meta(&logup, meta.len())); // Layout: num_committed_pairs term columns + 1 accumulated = ⌈N/2⌉ let num_aux_columns = if num_interactions > 0 { @@ -895,7 +894,7 @@ impl< let trace_layout = (num_main_columns, num_aux_columns); // Compute max bus elements across all interactions for alpha power count - let max_bus_elements = auxiliary_trace_build_data + let max_bus_elements = logup .interactions .iter() .map(|i| i.num_bus_elements()) @@ -907,15 +906,18 @@ impl< proof_options: proof_options.clone(), trace_columns: trace_layout.0 + trace_layout.1, transition_offsets: vec![0, 1], - num_transition_constraints: transition_constraints.len(), + num_transition_constraints: meta.len(), }; Self { context, step_size, trace_layout, - transition_constraints, - num_base_constraints, + constraint_set, + logup, + meta, + num_base, + constraint_program: std::sync::OnceLock::new(), auxiliary_trace_build_data, boundary_constraint_builder: PhantomData, preprocessed_commitment: None, @@ -961,12 +963,13 @@ impl< } } -impl crate::traits::AIR for AirWithBuses +impl crate::traits::AIR for AirWithBuses where F: IsFFTField + IsSubFieldOf + IsPrimeField + Send + Sync, E: IsField + Send + Sync, B: BoundaryConstraintBuilder, PI: Send + Sync, + CS: ConstraintSet, { type Field = F; @@ -1003,12 +1006,7 @@ where } fn composition_poly_degree_bound(&self, trace_length: usize) -> usize { - let max_degree = self - .transition_constraints - .iter() - .map(|c| c.degree()) - .max() - .unwrap_or(1); + let max_degree = self.meta.iter().map(|m| m.degree).max().unwrap_or(1); // The composition polynomial is the constraint QUOTIENT H = Σ βᵢ·Cᵢ/Zᵢ. Its degree is // deg(Cᵢ) − deg(Zᵢ) = (max_degree−1)·N − max_degree + eᵢ, so with the end-exemptions // eᵢ < max_degree (the max-degree LogUp constraints have eᵢ = 0) it fits in @@ -1023,13 +1021,57 @@ where } fn num_base_transition_constraints(&self) -> usize { - self.num_base_constraints + self.num_base } - fn transition_constraints( + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + fn compute_transition_prover( &self, - ) -> &Vec>> { - &self.transition_constraints + ctx: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + // One folder pass runs BOTH the table constraint set and the LogUp + // emission; LogUp constraints are appended after the set's (idx offset + // by the base-constraint count). + run_air_transition_prover( + &self.constraint_set, + &self.logup, + ctx, + base_evals, + ext_evals, + ); + } + + fn compute_transition( + &self, + ctx: &TransitionEvaluationContext, + ) -> Vec> { + run_air_transition_verifier( + &self.constraint_set, + &self.logup, + self.num_base, + self.meta.len(), + ctx, + ) + } + + fn constraint_program( + &self, + ) -> &crate::constraint_ir::ConstraintProgram { + // Lazily captured once (prover/GPU/tests only — the verify path never + // calls this). Runs the table set AND the LogUp emission through one + // CaptureBuilder, matching the folder emission order/indexing exactly. + self.constraint_program.get_or_init(|| { + let mut cb = crate::constraints::builder::CaptureBuilder::::new(); + self.constraint_set.eval(&mut cb); + emit_logup_constraints(&mut cb, &self.logup, self.num_base); + let (prog, _degrees) = cb.finish(self.num_base); + prog + }) } fn build_auxiliary_trace( @@ -1729,7 +1771,7 @@ fn compute_fingerprint_from_step, B: IsField>( // This is value-preserving (adding `0·α` is a no-op) and only costs a few extra // base×ext muls per row. -use crate::constraints::builder::{ConstraintBuilder, ConstraintMeta}; +use crate::constraints::builder::ConstraintBuilder; /// Config describing an [`AirWithBuses`] table's LogUp layout, exactly as /// computed by [`AirWithBuses::new`] from the interaction list (via @@ -2145,6 +2187,72 @@ pub fn logup_meta(layout: &LogUpLayout, idx_start: usize) -> Vec meta } +/// Run an [`AirWithBuses`] table's transition constraints through the +/// [`ProverEvalFolder`] in ONE pass: the constraint set's base-field body +/// followed by the appended LogUp constraints (idx offset by `num_base`, the +/// base-prefix length). `base_evals` is sized `num_base`; `ext_evals` the total +/// constraint count. +fn run_air_transition_prover( + constraint_set: &CS, + logup: &LogUpLayout, + ctx: &TransitionEvaluationContext<'_, F, E>, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], +) where + F: IsSubFieldOf, + E: IsField, + CS: ConstraintSet, +{ + let num_base = base_evals.len(); + let mut folder = ProverEvalFolder::new(ctx, base_evals, ext_evals); + constraint_set.eval(&mut folder); + emit_logup_constraints(&mut folder, logup, num_base); + folder.assert_all_emitted(); +} + +/// Run an [`AirWithBuses`] table's transition constraints at a single point, +/// returning every constraint value in the extension field: the constraint +/// set's base-field body (promoted) followed by the appended LogUp constraints. +/// +/// A Verifier context runs the [`VerifierEvalFolder`] (the OOD/recursion path). +/// A Prover context is also accepted — debug trace validation calls this with a +/// prover frame — by running the [`ProverEvalFolder`] and promoting the +/// base-prefix results, mirroring the old boxed path. +fn run_air_transition_verifier( + constraint_set: &CS, + logup: &LogUpLayout, + num_base: usize, + num_constraints: usize, + ctx: &TransitionEvaluationContext<'_, F, E>, +) -> Vec> +where + F: IsSubFieldOf, + E: IsField, + CS: ConstraintSet, +{ + let mut ext_evals = vec![FieldElement::::zero(); num_constraints]; + match ctx { + TransitionEvaluationContext::Verifier { .. } => { + let mut folder = VerifierEvalFolder::new(ctx, &mut ext_evals); + constraint_set.eval(&mut folder); + emit_logup_constraints(&mut folder, logup, num_base); + folder.assert_all_emitted(); + } + TransitionEvaluationContext::Prover { .. } => { + let mut base_evals = vec![FieldElement::::zero(); num_base]; + let mut folder = ProverEvalFolder::new(ctx, &mut base_evals, &mut ext_evals); + constraint_set.eval(&mut folder); + emit_logup_constraints(&mut folder, logup, num_base); + folder.assert_all_emitted(); + // Promote the base-prefix results into the extension slots. + for (slot, base) in ext_evals.iter_mut().zip(base_evals) { + *slot = base.to_extension(); + } + } + } + ext_evals +} + /// Constraint for a batched pair of interactions sharing one aux column. /// /// Verifies: `c = m_a/fp_a + m_b/fp_b` where signs are baked into m_a, m_b. @@ -2642,14 +2750,10 @@ mod logup_single_source_tests { // --- verifier-side: embed the same frame into the extension --- let embed_step = |step: &TableView| -> TableView { let main: Vec = (0..num_main_cols) - .map(|c| { - step.get_main_evaluation_element(0, c) - .clone() - .to_extension() - }) + .map(|c| step.get_main_evaluation_element(0, c).to_extension()) .collect(); let aux: Vec = (0..n_aux) - .map(|c| step.get_aux_evaluation_element(0, c).clone()) + .map(|c| *step.get_aux_evaluation_element(0, c)) .collect(); TableView::new(vec![main], vec![aux]) }; diff --git a/crypto/stark/src/tests/bus_tests/completeness_tests.rs b/crypto/stark/src/tests/bus_tests/completeness_tests.rs index 83f8ac391..6f4a1655b 100644 --- a/crypto/stark/src/tests/bus_tests/completeness_tests.rs +++ b/crypto/stark/src/tests/bus_tests/completeness_tests.rs @@ -2,6 +2,7 @@ //! //! These tests verify that the prover and verifier work correctly for legitimate use cases. +use crate::constraints::builder::EmptyConstraints; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::element::FieldElement; use math::field::{ @@ -427,12 +428,12 @@ fn test_bus_value_features() { )], }; let proof_options = ProofOptions::default_test_options(); - AirWithBuses::::new( + AirWithBuses::::new( 5, build_data, &proof_options, 1, - vec![], + EmptyConstraints, ) }; @@ -456,12 +457,12 @@ fn test_bus_value_features() { )], }; let proof_options = ProofOptions::default_test_options(); - AirWithBuses::::new( + AirWithBuses::::new( 5, build_data, &proof_options, 1, - vec![], + EmptyConstraints, ) }; diff --git a/crypto/stark/src/tests/bus_tests/multiplicity_tests.rs b/crypto/stark/src/tests/bus_tests/multiplicity_tests.rs index 7e4d632dd..8bf7492e4 100644 --- a/crypto/stark/src/tests/bus_tests/multiplicity_tests.rs +++ b/crypto/stark/src/tests/bus_tests/multiplicity_tests.rs @@ -3,13 +3,13 @@ //! These tests verify that all Multiplicity variants (One, Column, Sum, Negated) //! work correctly for computing bus interaction multiplicities. +use crate::constraints::builder::EmptyConstraints; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::element::FieldElement; use math::field::{ extensions_goldilocks::Degree3GoldilocksExtensionField, goldilocks::GoldilocksField, }; -use crate::constraints::transition::TransitionConstraintEvaluator; use crate::lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -37,8 +37,7 @@ const TEST_BUS: u64 = 0; fn test_multiplicity_one() { fn sender_air( proof_options: &ProofOptions, - ) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Multiplicity::One means every row sends with multiplicity 1 @@ -54,14 +53,13 @@ fn test_multiplicity_one() { auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn receiver_air( proof_options: &ProofOptions, - ) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Receiver also uses Multiplicity::One @@ -77,7 +75,7 @@ fn test_multiplicity_one() { auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } @@ -139,8 +137,7 @@ fn test_multiplicity_one() { fn test_multiplicity_sum() { fn sender_air( proof_options: &ProofOptions, - ) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Multiplicity::Sum(0, 1) means multiplicity = col[0] + col[1] @@ -156,14 +153,13 @@ fn test_multiplicity_sum() { auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn receiver_air( proof_options: &ProofOptions, - ) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Receiver uses Column(2) as multiplicity @@ -179,7 +175,7 @@ fn test_multiplicity_sum() { auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } @@ -249,8 +245,7 @@ fn test_multiplicity_sum() { fn test_multiplicity_negated() { fn sender_air( proof_options: &ProofOptions, - ) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Multiplicity::Negated(0) means multiplicity = 1 - col[0] @@ -267,14 +262,13 @@ fn test_multiplicity_negated() { auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn receiver_air( proof_options: &ProofOptions, - ) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::receiver( TEST_BUS, @@ -287,7 +281,7 @@ fn test_multiplicity_negated() { auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } diff --git a/crypto/stark/src/tests/bus_tests/packing_tests.rs b/crypto/stark/src/tests/bus_tests/packing_tests.rs index ec9f2035a..5f22b2c22 100644 --- a/crypto/stark/src/tests/bus_tests/packing_tests.rs +++ b/crypto/stark/src/tests/bus_tests/packing_tests.rs @@ -1,5 +1,6 @@ //! Unit tests for Packing combine logic. +use crate::constraints::builder::EmptyConstraints; use math::field::element::FieldElement; use math::field::goldilocks::GoldilocksField; @@ -317,12 +318,12 @@ fn test_air_layout_single_interaction() { }; let proof_options = ProofOptions::default_test_options(); - let air = AirWithBuses::::new( + let air = AirWithBuses::::new( 4, build_data, &proof_options, 1, - vec![], + EmptyConstraints, ); // 4 main, 1 aux (0 committed pairs + 1 accumulated with 1 absorbed) @@ -348,12 +349,12 @@ fn test_air_layout_multiple_interactions() { }; let proof_options = ProofOptions::default_test_options(); - let air = AirWithBuses::::new( + let air = AirWithBuses::::new( 5, build_data, &proof_options, 1, - vec![], + EmptyConstraints, ); // 5 main, 1 aux (0 committed pairs + 1 accumulated with 2 absorbed) diff --git a/crypto/stark/src/tests/bus_tests/soundness_tests.rs b/crypto/stark/src/tests/bus_tests/soundness_tests.rs index eb26276b8..652f5e87d 100644 --- a/crypto/stark/src/tests/bus_tests/soundness_tests.rs +++ b/crypto/stark/src/tests/bus_tests/soundness_tests.rs @@ -3,6 +3,7 @@ //! These tests verify that the verifier correctly rejects proofs that violate //! the bus balance invariant. +use crate::constraints::builder::EmptyConstraints; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::element::FieldElement; use math::field::{ @@ -1310,7 +1311,7 @@ fn test_packing_mismatch_direct_vs_word2l() { fn sender_air_direct( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Sender uses Direct: 2 separate elements @@ -1321,12 +1322,18 @@ fn test_packing_mismatch_direct_vs_word2l() { ), ], }; - AirWithBuses::new(3, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 3, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } fn receiver_air_word2l( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Receiver uses Word2L: combines 2 columns into 1 element @@ -1343,7 +1350,7 @@ fn test_packing_mismatch_direct_vs_word2l() { auxiliary_trace_build_data, proof_options, 1, - vec![], + EmptyConstraints, ) } @@ -1415,7 +1422,7 @@ fn test_packing_mismatch_element_count() { fn sender_air_3_direct( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Sender uses 3 Direct elements: produces [col1, col2, col3] @@ -1427,12 +1434,18 @@ fn test_packing_mismatch_element_count() { ), ], }; - AirWithBuses::new(4, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 4, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } fn receiver_air_word2l_direct( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Receiver uses Word2L (combines cols 1,2 into 1 element) + Direct (col 3) @@ -1448,7 +1461,13 @@ fn test_packing_mismatch_element_count() { ), ], }; - AirWithBuses::new(4, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 4, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } let mut sender_trace = TraceTable::from_columns_main( @@ -1517,7 +1536,7 @@ fn test_packing_mismatch_shift_constant() { fn sender_air_word4l( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Word4L: b0 + 2^8*b1 + 2^16*b2 + 2^24*b3 @@ -1528,12 +1547,18 @@ fn test_packing_mismatch_shift_constant() { ), ], }; - AirWithBuses::new(5, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 5, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } fn receiver_air_dwordhl( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // DWordHL: [h0 + 2^16*h1, h2 + 2^16*h3] - different shift pattern! @@ -1544,7 +1569,13 @@ fn test_packing_mismatch_shift_constant() { ), ], }; - AirWithBuses::new(5, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 5, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } // Use small values so the different shift formulas give clearly different results @@ -1618,7 +1649,7 @@ fn test_compound_mismatch_dwordhhw_vs_dwordwhh() { fn sender_air_dwordhhw( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // DWordHHW: [Word, Half, Half] at columns 1, 2, 3 @@ -1629,12 +1660,18 @@ fn test_compound_mismatch_dwordhhw_vs_dwordwhh() { ), ], }; - AirWithBuses::new(4, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 4, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } fn receiver_air_dwordwhh( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // DWordWHH: [Half, Half, Word] at columns 1, 2, 3 @@ -1645,7 +1682,13 @@ fn test_compound_mismatch_dwordhhw_vs_dwordwhh() { ), ], }; - AirWithBuses::new(4, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 4, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } // Trace with values that expose the layout difference @@ -1717,7 +1760,7 @@ fn test_compound_equals_primitive_expansion() { fn sender_air_compound( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // DWordHL (compound): 4 halves at columns 1-4 @@ -1728,12 +1771,18 @@ fn test_compound_equals_primitive_expansion() { ), ], }; - AirWithBuses::new(5, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 5, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } fn receiver_air_primitives( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ // Equivalent: 2× Word2L at columns 1-2 and 3-4 @@ -1744,7 +1793,13 @@ fn test_compound_equals_primitive_expansion() { ), ], }; - AirWithBuses::new(5, auxiliary_trace_build_data, proof_options, 1, vec![]) + AirWithBuses::new( + 5, + auxiliary_trace_build_data, + proof_options, + 1, + EmptyConstraints, + ) } let mut sender_trace = TraceTable::from_columns_main( diff --git a/crypto/stark/src/tests/prove_verify_roundtrip_tests.rs b/crypto/stark/src/tests/prove_verify_roundtrip_tests.rs index 4059ed481..a387df476 100644 --- a/crypto/stark/src/tests/prove_verify_roundtrip_tests.rs +++ b/crypto/stark/src/tests/prove_verify_roundtrip_tests.rs @@ -3,13 +3,13 @@ //! These tests verify that proofs survive serialization/deserialization //! and can be verified independently from the prover. +use crate::constraints::builder::EmptyConstraints; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::element::FieldElement; use math::field::{ extensions_goldilocks::Degree3GoldilocksExtensionField, goldilocks::GoldilocksField, }; -use crate::constraints::transition::TransitionConstraintEvaluator; use crate::lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -184,8 +184,7 @@ fn test_verify_serialized_multi_table_proofs() { fn create_cpu_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![ BusInteraction::sender( @@ -205,14 +204,13 @@ fn create_cpu_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn create_add_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::receiver( BusId::Add, @@ -225,14 +223,13 @@ fn create_add_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn create_mul_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::receiver( BusId::Mul, @@ -245,6 +242,6 @@ fn create_mul_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } diff --git a/crypto/stark/src/traits.rs b/crypto/stark/src/traits.rs index 06465b659..ca9e82758 100644 --- a/crypto/stark/src/traits.rs +++ b/crypto/stark/src/traits.rs @@ -10,7 +10,8 @@ use math::{ }; use crate::{ - constraints::transition::TransitionConstraintEvaluator, + constraint_ir::ConstraintProgram, + constraints::builder::ConstraintMeta, domain::Domain, lookup::{BusPublicInputs, PackingShifts}, }; @@ -220,18 +221,15 @@ pub trait AIR: Send + Sync { /// In the case of the prover, the main evaluation table of the frame takes values in /// `Self::Field`, since they are the evaluations of the main trace at the LDE domain. /// In the case of the verifier, the frame take elements of Self::FieldExtension. + /// + /// Required: implemented via the single-source constraint body (the + /// [`VerifierEvalFolder`](crate::constraints::builder::VerifierEvalFolder) + /// run — this exact monomorphization, compiled into the guest binary, is the + /// recursion-guest constraint-evaluation path; it never captures or hashes). fn compute_transition( &self, evaluation_context: &TransitionEvaluationContext, - ) -> Vec> { - let mut evaluations = - vec![FieldElement::::zero(); self.num_transition_constraints()]; - self.transition_constraints() - .iter() - .for_each(|c| c.evaluate_verifier(evaluation_context, &mut evaluations)); - - evaluations - } + ) -> Vec>; /// Number of constraints that evaluate in the base field F. /// @@ -251,22 +249,32 @@ pub trait AIR: Send + Sync { /// `base_evals` has length `num_base_transition_constraints()`. /// `ext_evals` has length `num_transition_constraints()`; only indices /// `[num_base..]` are written/read for extension constraints. + /// + /// Required: implemented via the single-source constraint body (the + /// [`ProverEvalFolder`](crate::constraints::builder::ProverEvalFolder) run — + /// the CPU prover hot path). fn compute_transition_prover( &self, evaluation_context: &TransitionEvaluationContext, base_evals: &mut [FieldElement], ext_evals: &mut [FieldElement], - ) { - for e in base_evals.iter_mut() { - *e = FieldElement::zero(); - } - let num_base = base_evals.len(); - for e in ext_evals[num_base..].iter_mut() { - *e = FieldElement::zero(); - } - self.transition_constraints() - .iter() - .for_each(|c| c.evaluate_prover(evaluation_context, base_evals, ext_evals)); + ); + + /// The idx-ordered metadata for every transition constraint (kind, declared + /// degree, zerofier shape) — plain data replacing the old per-constraint + /// trait objects. `RootKind::Base` entries form a prefix (its length is + /// `num_base_transition_constraints()`). + fn constraints_meta(&self) -> &[ConstraintMeta]; + + /// The lazily captured flat IR ([`ConstraintProgram`]) of every transition + /// constraint, for the CPU interpreter and the GPU kernel. + /// + /// GUEST-SAFETY: capture hash-conses, so the verify/recursion path must + /// NEVER call this — only the prover, GPU lowering, and tests do. The + /// default panics precisely so any accidental verify-path use is caught; + /// AIRs that support capture override it with a cached (`OnceLock`) build. + fn constraint_program(&self) -> &ConstraintProgram { + unimplemented!("constraint_program is not available for this AIR") } fn boundary_constraints( @@ -311,10 +319,6 @@ pub trait AIR: Send + Sync { result } - fn transition_constraints( - &self, - ) -> &Vec>>; - /// Compute zerofier evaluations as deduplicated groups with index mapping. /// /// Each unique zerofier (keyed by period/offset/exemption parameters) is @@ -324,25 +328,30 @@ pub trait AIR: Send + Sync { &self, domain: &Domain, ) -> ZerofierEvaluations { - let num_constraints = self.num_transition_constraints(); + let meta = self.constraints_meta(); + let num_constraints = meta.len(); let mut constraint_to_group = vec![0usize; num_constraints]; let mut zerofier_groups_map: HashMap = HashMap::new(); let mut groups: Vec>> = Vec::new(); - self.transition_constraints().iter().for_each(|c| { + meta.iter().for_each(|m| { let key = ZerofierGroupKey { - period: c.period(), - offset: c.offset(), - exemptions_period: c.exemptions_period(), - periodic_exemptions_offset: c.periodic_exemptions_offset(), - end_exemptions: c.end_exemptions(), + period: m.period, + offset: m.offset, + exemptions_period: m.exemptions_period, + periodic_exemptions_offset: m.periodic_exemptions_offset, + end_exemptions: m.end_exemptions, }; let group_idx = *zerofier_groups_map.entry(key).or_insert_with(|| { let idx = groups.len(); - groups.push(c.zerofier_evaluations_on_extended_domain(domain)); + groups.push( + crate::constraints::zerofier::zerofier_evaluations_on_extended_domain( + m, domain, + ), + ); idx }); - constraint_to_group[c.constraint_idx()] = group_idx; + constraint_to_group[m.constraint_idx] = group_idx; }); ZerofierEvaluations { diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 03119f617..ba3dfd518 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -210,9 +210,13 @@ pub trait IsStarkVerifier< let mut denominators = vec![FieldElement::::zero(); air.num_transition_constraints()]; - air.transition_constraints().iter().for_each(|c| { - denominators[c.constraint_idx()] = - c.evaluate_zerofier(&challenges.z, &domain.trace_primitive_root, trace_length); + air.constraints_meta().iter().for_each(|m| { + denominators[m.constraint_idx] = crate::constraints::zerofier::evaluate_zerofier( + m, + &challenges.z, + &domain.trace_primitive_root, + trace_length, + ); }); let transition_c_i_evaluations_sum = itertools::izip!( diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index ccdd5a6f9..c601a2611 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -42,6 +42,9 @@ use executor::vm::execution::Executor; use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; use math::field::element::FieldElement; use stark::config::Commitment; +use stark::constraints::builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, EmptyConstraints, +}; use stark::lookup::{AirWithBuses, AuxiliaryTraceBuildData, NullBoundaryConstraintBuilder}; use stark::proof::options::ProofOptions; use stark::proof::stark::MultiProof; @@ -66,11 +69,6 @@ type F = GoldilocksField; type E = GoldilocksExtension; type AirRef<'a> = &'a dyn AIR; -fn empty_constraints() --> Vec>> { - vec![] -} - /// Fresh transcript seeded with the epoch's statement (ELF, public output, table /// layout) and `epoch_label` (its position). The epoch's prove, verify, and /// bus-balance replay all seed via this so their challenges match; the seeding @@ -112,11 +110,18 @@ fn global_transcript(elf_bytes: &[u8], num_epochs: usize) -> DefaultTranscript Vec>> { - use crate::constraints::templates::IsBitConstraint; - use stark::constraints::transition::TransitionConstraint; - vec![IsBitConstraint::unconditional(local_to_global::cols::MU, 0).boxed()] +/// The L2G epoch-local table's single transition constraint: `MU ∈ {0,1}` +/// (`MU·(1−MU) = 0`) at constraint index 0. +struct L2gMemoryConstraints; + +impl ConstraintSet for L2gMemoryConstraints { + fn meta(&self) -> Vec { + // IS_BIT(MU), unconditional → degree 2. + vec![ConstraintMeta::base(0, 2)] + } + fn eval>(&self, b: &mut B) { + crate::constraints::templates::emit_is_bit(b, 0, local_to_global::cols::MU, None); + } } /// Local-to-global AIR on the cross-epoch GlobalMemory bus (used in the global proof). @@ -134,7 +139,7 @@ fn l2g_constraints() fn l2g_global_air( opts: &ProofOptions, epoch_label: u64, -) -> AirWithBuses { +) -> AirWithBuses { AirWithBuses::new( local_to_global::cols::NUM_COLUMNS, AuxiliaryTraceBuildData { @@ -142,7 +147,7 @@ fn l2g_global_air( }, opts, 1, - empty_constraints(), + EmptyConstraints, ) } @@ -155,7 +160,7 @@ fn l2g_global_air( fn l2g_memory_air( opts: &ProofOptions, epoch_label: u64, -) -> AirWithBuses { +) -> AirWithBuses { let interactions = [ local_to_global::memory_bus_interactions(), local_to_global::range_check_interactions(epoch_label), @@ -166,7 +171,7 @@ fn l2g_memory_air( AuxiliaryTraceBuildData { interactions }, opts, 1, - l2g_constraints(), + L2gMemoryConstraints, ) } @@ -181,7 +186,7 @@ fn l2g_memory_air( fn global_memory_air( opts: &ProofOptions, config: &PageConfig, -) -> AirWithBuses { +) -> AirWithBuses { let air = AirWithBuses::new( global_memory::cols::NUM_COLUMNS, AuxiliaryTraceBuildData { @@ -189,7 +194,7 @@ fn global_memory_air( }, opts, 1, - empty_constraints(), + EmptyConstraints, ); let commitment = if config.init_values.is_some() { page::compute_precomputed_commitment(config, opts) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 6bbde8b84..6c7a57218 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -271,73 +271,73 @@ impl VmAirs { /// Build `(air, trace, public_inputs)` triples for [`Prover::multi_prove`]. pub fn air_trace_pairs<'a>(&'a self, traces: &'a mut Traces) -> Vec> { let mut pairs: Vec> = vec![ - (&self.bitwise, &mut traces.bitwise, &()), - (&self.decode, &mut traces.decode, &()), - (&self.commit, &mut traces.commit, &()), - (&self.keccak, &mut traces.keccak, &()), - (&self.keccak_rnd, &mut traces.keccak_rnd, &()), - (&self.keccak_rc, &mut traces.keccak_rc, &()), - (&self.ecsm, &mut traces.ecsm, &()), - (&self.ec_scalar, &mut traces.ec_scalar, &()), - (&self.ecdas, &mut traces.ecdas, &()), - (&self.register, &mut traces.register, &()), + (self.bitwise.as_ref(), &mut traces.bitwise, &()), + (self.decode.as_ref(), &mut traces.decode, &()), + (self.commit.as_ref(), &mut traces.commit, &()), + (self.keccak.as_ref(), &mut traces.keccak, &()), + (self.keccak_rnd.as_ref(), &mut traces.keccak_rnd, &()), + (self.keccak_rc.as_ref(), &mut traces.keccak_rc, &()), + (self.ecsm.as_ref(), &mut traces.ecsm, &()), + (self.ec_scalar.as_ref(), &mut traces.ec_scalar, &()), + (self.ecdas.as_ref(), &mut traces.ecdas, &()), + (self.register.as_ref(), &mut traces.register, &()), ]; if self.include_halt { - pairs.push((&self.halt, &mut traces.halt, &())); + pairs.push((self.halt.as_ref(), &mut traces.halt, &())); } for (air, trace) in self.cpus.iter().zip(traces.cpus.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.lts.iter().zip(traces.lts.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.shifts.iter().zip(traces.shifts.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.memws.iter().zip(traces.memws.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self .memw_aligneds .iter() .zip(traces.memw_aligneds.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.loads.iter().zip(traces.loads.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.muls.iter().zip(traces.muls.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.dvrms.iter().zip(traces.dvrms.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.branches.iter().zip(traces.branches.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.pages.iter().zip(traces.pages.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self .memw_registers .iter() .zip(traces.memw_registers.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.eqs.iter().zip(traces.eqs.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.bytewises.iter().zip(traces.bytewises.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.stores.iter().zip(traces.stores.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } for (air, trace) in self.cpu32s.iter().zip(traces.cpu32s.iter_mut()) { - pairs.push((air, trace, &())); + pairs.push((air.as_ref(), trace, &())); } pairs @@ -346,65 +346,65 @@ impl VmAirs { /// Collect AIR references for [`Verifier::multi_verify`]. pub fn air_refs(&self) -> Vec<&dyn AIR> { let mut refs: Vec<&dyn AIR> = vec![ - &self.bitwise, - &self.decode, - &self.commit, - &self.keccak, - &self.keccak_rnd, - &self.keccak_rc, - &self.ecsm, - &self.ec_scalar, - &self.ecdas, - &self.register, + self.bitwise.as_ref(), + self.decode.as_ref(), + self.commit.as_ref(), + self.keccak.as_ref(), + self.keccak_rnd.as_ref(), + self.keccak_rc.as_ref(), + self.ecsm.as_ref(), + self.ec_scalar.as_ref(), + self.ecdas.as_ref(), + self.register.as_ref(), ]; if self.include_halt { - refs.push(&self.halt); + refs.push(self.halt.as_ref()); } for air in &self.cpus { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.lts { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.shifts { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.memws { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.memw_aligneds { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.loads { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.muls { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.dvrms { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.branches { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.pages { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.memw_registers { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.eqs { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.bytewises { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.stores { - refs.push(air); + refs.push(air.as_ref()); } for air in &self.cpu32s { - refs.push(air); + refs.push(air.as_ref()); } refs @@ -454,68 +454,96 @@ impl VmAirs { register_preprocessed: Option<(Commitment, usize)>, ) -> Self { let cpus: Vec<_> = (0..table_counts.cpu) - .map(|i| create_cpu_air(proof_options).with_name(&format!("CPU[{}]", i))) + .map(|i| { + Box::new(create_cpu_air(proof_options).with_name(&format!("CPU[{}]", i))) as VmAir + }) .collect(); - let bitwise = if minimal_bitwise { - create_bitwise_air(proof_options) + let bitwise: VmAir = if minimal_bitwise { + Box::new(create_bitwise_air(proof_options)) } else { - create_bitwise_air(proof_options).with_preprocessed( + Box::new(create_bitwise_air(proof_options).with_preprocessed( bitwise::preprocessed_commitment(proof_options), bitwise::NUM_PRECOMPUTED_COLS, - ) + )) }; let lts: Vec<_> = (0..table_counts.lt) - .map(|i| create_lt_air(proof_options).with_name(&format!("LT[{}]", i))) + .map(|i| { + Box::new(create_lt_air(proof_options).with_name(&format!("LT[{}]", i))) as VmAir + }) .collect(); let shifts: Vec<_> = (0..table_counts.shift) - .map(|i| create_shift_air(proof_options).with_name(&format!("SHIFT[{}]", i))) + .map(|i| { + Box::new(create_shift_air(proof_options).with_name(&format!("SHIFT[{}]", i))) + as VmAir + }) .collect(); let memws: Vec<_> = (0..table_counts.memw) - .map(|i| create_memw_air(proof_options).with_name(&format!("MEMW[{}]", i))) + .map(|i| { + Box::new(create_memw_air(proof_options).with_name(&format!("MEMW[{}]", i))) as VmAir + }) .collect(); let memw_aligneds: Vec<_> = (0..table_counts.memw_aligned) - .map(|i| create_memw_aligned_air(proof_options).with_name(&format!("MEMW_A[{}]", i))) + .map(|i| { + Box::new( + create_memw_aligned_air(proof_options).with_name(&format!("MEMW_A[{}]", i)), + ) as VmAir + }) .collect(); let loads: Vec<_> = (0..table_counts.load) - .map(|i| create_load_air(proof_options).with_name(&format!("LOAD[{}]", i))) + .map(|i| { + Box::new(create_load_air(proof_options).with_name(&format!("LOAD[{}]", i))) as VmAir + }) .collect(); let decode_root = decode_commitment.unwrap_or_else(|| { decode::commitment_from_elf(elf, proof_options) .expect("Failed to compute decode commitment") }); - let decode = create_decode_air(proof_options) - .with_preprocessed(decode_root, decode::NUM_PRECOMPUTED_COLS); + let decode: VmAir = Box::new( + create_decode_air(proof_options) + .with_preprocessed(decode_root, decode::NUM_PRECOMPUTED_COLS), + ); let muls: Vec<_> = (0..table_counts.mul) - .map(|i| create_mul_air(proof_options).with_name(&format!("MUL[{}]", i))) + .map(|i| { + Box::new(create_mul_air(proof_options).with_name(&format!("MUL[{}]", i))) as VmAir + }) .collect(); let dvrms: Vec<_> = (0..table_counts.dvrm) - .map(|i| create_dvrm_air(proof_options).with_name(&format!("DVRM[{}]", i))) + .map(|i| { + Box::new(create_dvrm_air(proof_options).with_name(&format!("DVRM[{}]", i))) as VmAir + }) .collect(); let branches: Vec<_> = (0..table_counts.branch) - .map(|i| create_branch_air(proof_options).with_name(&format!("BRANCH[{}]", i))) + .map(|i| { + Box::new(create_branch_air(proof_options).with_name(&format!("BRANCH[{}]", i))) + as VmAir + }) .collect(); - let halt = create_halt_air(proof_options); - let commit = create_commit_air(proof_options); - let keccak = create_keccak_air(proof_options); - let keccak_rnd = create_keccak_rnd_air(proof_options); - let keccak_rc = create_keccak_rc_air(proof_options).with_preprocessed( + let halt: VmAir = Box::new(create_halt_air(proof_options)); + let commit: VmAir = Box::new(create_commit_air(proof_options)); + let keccak: VmAir = Box::new(create_keccak_air(proof_options)); + let keccak_rnd: VmAir = Box::new(create_keccak_rnd_air(proof_options)); + let keccak_rc: VmAir = Box::new(create_keccak_rc_air(proof_options).with_preprocessed( tables::keccak_rc::preprocessed_commitment(proof_options), tables::keccak_rc::NUM_PRECOMPUTED_COLS, - ); - let ecsm = create_ecsm_air(proof_options); - let ec_scalar = create_ec_scalar_air(proof_options); - let ecdas = create_ecdas_air(proof_options); - let register = if let Some((commitment, num_preprocessed_cols)) = register_preprocessed { - create_register_air(proof_options).with_preprocessed(commitment, num_preprocessed_cols) - } else { - let register_init = register_init - .map(<[u32]>::to_vec) - .unwrap_or_else(|| register::register_init_from_entry_point(elf.entry_point)); - create_register_air(proof_options).with_preprocessed( - register::preprocessed_commitment(proof_options, ®ister_init), - register::NUM_PREPROCESSED_COLS, - ) - }; + )); + let ecsm: VmAir = Box::new(create_ecsm_air(proof_options)); + let ec_scalar: VmAir = Box::new(create_ec_scalar_air(proof_options)); + let ecdas: VmAir = Box::new(create_ecdas_air(proof_options)); + let register: VmAir = + if let Some((commitment, num_preprocessed_cols)) = register_preprocessed { + Box::new( + create_register_air(proof_options) + .with_preprocessed(commitment, num_preprocessed_cols), + ) + } else { + let register_init = register_init + .map(<[u32]>::to_vec) + .unwrap_or_else(|| register::register_init_from_entry_point(elf.entry_point)); + Box::new(create_register_air(proof_options).with_preprocessed( + register::preprocessed_commitment(proof_options, ®ister_init), + register::NUM_PREPROCESSED_COLS, + )) + }; // Every zero-init page shares one preprocessed commitment: OFFSET is // page-relative and INIT is all-zero, so it depends only on // (blowup, coset) — all fixed here. Compute it once (static const @@ -524,18 +552,20 @@ impl VmAirs { // initialized), so this commitment is always used. let zero_init_commitment = page::zero_init_preprocessed_commitment(proof_options); - let pages: Vec<_> = page_configs + let pages: Vec = page_configs .iter() - .map(|config| { + .map(|config| -> VmAir { let air = create_page_air(proof_options, config.page_base); if config.is_private_input { // Private-input pages: all columns are main trace (not preprocessed). // The verifier doesn't see the init values; correctness is enforced // by the memory bus constraints. - air + Box::new(air) } else if config.init_values.is_none() { // Zero-init pages: the shared commitment computed once above. - air.with_preprocessed(zero_init_commitment, page::NUM_PREPROCESSED_COLS) + Box::new( + air.with_preprocessed(zero_init_commitment, page::NUM_PREPROCESSED_COLS), + ) } else { // ELF data pages: INIT is program-specific, so the commitment is // per-page. Prefer a caller-supplied `(page_base, commitment)` @@ -548,24 +578,39 @@ impl VmAirs { .unwrap_or_else(|| { page::compute_precomputed_commitment(config, proof_options) }); - air.with_preprocessed(commitment, page::NUM_PREPROCESSED_COLS) + Box::new(air.with_preprocessed(commitment, page::NUM_PREPROCESSED_COLS)) } }) .collect(); let memw_registers: Vec<_> = (0..table_counts.memw_register) - .map(|i| create_memw_register_air(proof_options).with_name(&format!("MEMW_R[{}]", i))) + .map(|i| { + Box::new( + create_memw_register_air(proof_options).with_name(&format!("MEMW_R[{}]", i)), + ) as VmAir + }) .collect(); let eqs: Vec<_> = (0..table_counts.eq) - .map(|i| create_eq_air(proof_options).with_name(&format!("EQ[{}]", i))) + .map(|i| { + Box::new(create_eq_air(proof_options).with_name(&format!("EQ[{}]", i))) as VmAir + }) .collect(); let bytewises: Vec<_> = (0..table_counts.bytewise) - .map(|i| create_bytewise_air(proof_options).with_name(&format!("BYTEWISE[{}]", i))) + .map(|i| { + Box::new(create_bytewise_air(proof_options).with_name(&format!("BYTEWISE[{}]", i))) + as VmAir + }) .collect(); let stores: Vec<_> = (0..table_counts.store) - .map(|i| create_store_air(proof_options).with_name(&format!("STORE[{}]", i))) + .map(|i| { + Box::new(create_store_air(proof_options).with_name(&format!("STORE[{}]", i))) + as VmAir + }) .collect(); let cpu32s: Vec<_> = (0..table_counts.cpu32) - .map(|i| create_cpu32_air(proof_options).with_name(&format!("CPU32[{}]", i))) + .map(|i| { + Box::new(create_cpu32_air(proof_options).with_name(&format!("CPU32[{}]", i))) + as VmAir + }) .collect(); #[cfg(feature = "debug-checks")] diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index fd9d9d40c..8ed5fdca2 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -19,7 +19,7 @@ use executor::vm::instruction::decoding::Instruction; use executor::vm::logs::Log; use executor::vm::memory::U64HashMap; use math::field::element::FieldElement; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; +use stark::constraints::builder::{ConstraintSet, EmptyConstraints}; use stark::debug::validate_trace; use stark::domain::Domain; use stark::lookup::{ @@ -33,74 +33,79 @@ use stark::storage_mode::StorageMode; use stark::trace::TraceTable; use stark::traits::AIR; -use crate::constraints::cpu::create_all_cpu_constraints; +use crate::constraints::cpu::CpuConstraints; use crate::tables::bitwise::{ BitwiseOperation, BitwiseOperationType, bus_interactions as bitwise_bus_interactions, cols as bitwise_cols, }; use crate::tables::branch::{ - branch_constraints, bus_interactions as branch_bus_interactions, cols as branch_cols, + BranchConstraints, bus_interactions as branch_bus_interactions, cols as branch_cols, }; use crate::tables::bytewise::{ bus_interactions as bytewise_bus_interactions, cols as bytewise_cols, }; use crate::tables::commit::{ - bus_interactions as commit_bus_interactions, cols as commit_cols, - create_constraints as commit_constraints, + CommitConstraints, bus_interactions as commit_bus_interactions, cols as commit_cols, }; use crate::tables::cpu::{ CpuOperation, bus_interactions as cpu_bus_interactions, cols as cpu_cols, }; use crate::tables::cpu32::{ - bus_interactions as cpu32_bus_interactions, cols as cpu32_cols, cpu32_constraints, + Cpu32Constraints, bus_interactions as cpu32_bus_interactions, cols as cpu32_cols, }; use crate::tables::decode::{bus_interactions as decode_bus_interactions, cols as decode_cols}; use crate::tables::dvrm::{ - bus_interactions as dvrm_bus_interactions, cols as dvrm_cols, dvrm_constraints, + DvrmConstraints, bus_interactions as dvrm_bus_interactions, cols as dvrm_cols, }; use crate::tables::ec_scalar::{ - bus_interactions as ec_scalar_bus_interactions, cols as ec_scalar_cols, + EcScalarConstraints, bus_interactions as ec_scalar_bus_interactions, cols as ec_scalar_cols, }; -use crate::tables::ecdas::{bus_interactions as ecdas_bus_interactions, cols as ecdas_cols}; -use crate::tables::ecsm::{bus_interactions as ecsm_bus_interactions, cols as ecsm_cols}; -use crate::tables::eq::{bus_interactions as eq_bus_interactions, cols as eq_cols, eq_constraints}; +use crate::tables::ecdas::{ + EcdasConstraints, bus_interactions as ecdas_bus_interactions, cols as ecdas_cols, +}; +use crate::tables::ecsm::{ + EcsmConstraints, bus_interactions as ecsm_bus_interactions, cols as ecsm_cols, +}; +use crate::tables::eq::{EqConstraints, bus_interactions as eq_bus_interactions, cols as eq_cols}; use crate::tables::halt::{bus_interactions as halt_bus_interactions, cols as halt_cols}; -use crate::tables::keccak::{bus_interactions as keccak_bus_interactions, cols as keccak_cols}; +use crate::tables::keccak::{ + KeccakConstraints, bus_interactions as keccak_bus_interactions, cols as keccak_cols, +}; use crate::tables::keccak_rc::{ bus_interactions as keccak_rc_bus_interactions, cols as keccak_rc_cols, }; use crate::tables::keccak_rnd::{ - bus_interactions as keccak_rnd_bus_interactions, cols as keccak_rnd_cols, + KeccakRndConstraints, bus_interactions as keccak_rnd_bus_interactions, cols as keccak_rnd_cols, }; use crate::tables::load::{ - bus_interactions as load_bus_interactions, cols as load_cols, constraints as load_constraints, + LoadConstraints, bus_interactions as load_bus_interactions, cols as load_cols, }; use crate::tables::lt::{ - LtOperation, bus_interactions as lt_bus_interactions, cols as lt_cols, lt_constraints, + LtConstraints, LtOperation, bus_interactions as lt_bus_interactions, cols as lt_cols, }; use crate::tables::memw::{ - bus_interactions as memw_bus_interactions, cols as memw_cols, constraints as memw_constraints, + MemwConstraints, bus_interactions as memw_bus_interactions, cols as memw_cols, }; use crate::tables::memw_aligned::{ - bus_interactions as memw_aligned_bus_interactions, cols as memw_aligned_cols, - constraints as memw_aligned_constraints, + MemwAlignedConstraints, bus_interactions as memw_aligned_bus_interactions, + cols as memw_aligned_cols, }; use crate::tables::memw_register::{ - bus_interactions as memw_register_bus_interactions, cols as memw_register_cols, - constraints as memw_register_constraints, + MemwRegisterConstraints, bus_interactions as memw_register_bus_interactions, + cols as memw_register_cols, }; use crate::tables::mul::{ - bus_interactions as mul_bus_interactions, cols as mul_cols, mul_constraints, + MulConstraints, bus_interactions as mul_bus_interactions, cols as mul_cols, }; use crate::tables::page::{bus_interactions as page_bus_interactions, cols as page_cols}; use crate::tables::register::{ bus_interactions as register_bus_interactions, cols as register_cols, }; use crate::tables::shift::{ - bus_interactions as shift_bus_interactions, cols as shift_cols, shift_constraints, + ShiftConstraints, bus_interactions as shift_bus_interactions, cols as shift_cols, }; use crate::tables::store::{ - bus_interactions as store_bus_interactions, cols as store_cols, store_constraints, + StoreConstraints, bus_interactions as store_bus_interactions, cols as store_cols, }; use crate::tables::types::{BusId, GoldilocksExtension, GoldilocksField}; @@ -108,7 +113,16 @@ pub type F = GoldilocksField; pub type E = GoldilocksExtension; pub type FE = FieldElement; -pub type VmAir = AirWithBuses; +/// A boxed VM table AIR. Each table's `AirWithBuses<..., XxxConstraints>` is a +/// distinct concrete type now that the constraint set is a type parameter, so +/// the heterogeneous per-table AIRs are stored behind a trait object. +pub type VmAir = Box>; + +/// The concrete `AirWithBuses` for a table with constraint set `CS`. The +/// `create_*_air` helpers return this so callers can still chain the inherent +/// `.with_name` / `.with_preprocessed` builder methods before boxing into a +/// [`VmAir`]. +pub type ConcreteVmAir = AirWithBuses; type GoldilocksPair<'a, PI> = ( &'a dyn AIR, @@ -139,27 +153,28 @@ where /// With zero bus interactions, `AirWithBuses::new` appends no LogUp constraints /// and allocates no aux columns, so `validate_trace` evaluates exactly the chip's /// transition constraints over a main-only trace. -pub fn busless_air + 'static>( +pub fn busless_air + 'static>( num_columns: usize, - constraints: Vec, + constraint_set: CS, ) -> VmAir { - let transition_constraints = constraints.into_iter().map(|c| c.boxed()).collect(); - AirWithBuses::new( - num_columns, - AuxiliaryTraceBuildData { - interactions: vec![], - }, - &ProofOptions::default_test_options(), - 1, - transition_constraints, + Box::new( + AirWithBuses::<_, _, NullBoundaryConstraintBuilder, (), _>::new( + num_columns, + AuxiliaryTraceBuildData { + interactions: vec![], + }, + &ProofOptions::default_test_options(), + 1, + constraint_set, + ), ) } /// Run `validate_trace` for a bus-less chip AIR over a main-only trace. /// Returns `true` iff every transition constraint holds on every row. pub fn validate_busless(air: &VmAir, trace: &TraceTable) -> bool { - let domain = Domain::new(air, trace.num_rows()); - validate_trace(air, &(), trace, &domain, &[], None) + let domain = Domain::new(air.as_ref(), trace.num_rows()); + validate_trace(air.as_ref(), &(), trace, &domain, &[], None) } /// Number of transition constraints a production builder registers on top of its @@ -171,14 +186,14 @@ pub fn in_chip_constraint_count( num_columns: usize, buses: Vec, ) -> usize { - let bus_only = AirWithBuses::::new( + let bus_only = AirWithBuses::::new( num_columns, AuxiliaryTraceBuildData { interactions: buses, }, &ProofOptions::default_test_options(), 1, - vec![], + EmptyConstraints, ) .num_transition_constraints(); wired @@ -587,229 +602,175 @@ pub fn generate_minimal_bitwise_trace(ops: &[BitwiseOperation]) -> TraceTable VmAir { - // Get all CPU constraints - let (is_bit, add, other, _) = create_all_cpu_constraints(); - - // All CPU constraints - let mut transition_constraints: Vec>> = Vec::new(); - for c in is_bit { - transition_constraints.push(c.boxed()); - } - for c in add { - transition_constraints.push(c.boxed()); - } - for c in other { - transition_constraints.push(c); - } - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: cpu_bus_interactions(), - }; - +/// Build a boxed `AirWithBuses` for a table from its columns, bus interactions, +/// step size, and single-source [`ConstraintSet`]. The framework appends the +/// LogUp constraints from the interactions; `constraint_set` supplies the +/// base-field (table) constraints (or [`EmptyConstraints`] for pure-lookup +/// tables). +fn build_air + 'static>( + num_columns: usize, + interactions: Vec, + proof_options: &ProofOptions, + step_size: usize, + constraint_set: CS, + name: &str, +) -> AirWithBuses { AirWithBuses::new( + num_columns, + AuxiliaryTraceBuildData { interactions }, + proof_options, + step_size, + constraint_set, + ) + .with_name(name) +} + +/// Create CPU AIR with all constraints and bus interactions. +pub fn create_cpu_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( cpu_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + cpu_bus_interactions(), proof_options, 1, - transition_constraints, + CpuConstraints, + "CPU", ) - .with_name("CPU") } /// Create Bitwise AIR with bus interactions. -pub fn create_bitwise_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints: Vec>> = vec![]; - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: bitwise_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_bitwise_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( bitwise_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + bitwise_bus_interactions(), proof_options, 1, - transition_constraints, + EmptyConstraints, + "BITWISE", ) - .with_name("BITWISE") } /// Create LT AIR with constraints and bus interactions. -pub fn create_lt_air(proof_options: &ProofOptions) -> VmAir { - let (constraints, _) = lt_constraints(0); - let transition_constraints: Vec>> = - constraints.into_iter().map(|c| c.boxed()).collect(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: lt_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_lt_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( lt_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + lt_bus_interactions(), proof_options, 1, - transition_constraints, + LtConstraints, + "LT", ) - .with_name("LT") } /// Create SHIFT AIR with constraints and bus interactions. -pub fn create_shift_air(proof_options: &ProofOptions) -> VmAir { - let (constraints, _) = shift_constraints(0); - let transition_constraints: Vec>> = - constraints.into_iter().map(|c| c.boxed()).collect(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: shift_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_shift_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( shift_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + shift_bus_interactions(), proof_options, 1, - transition_constraints, + ShiftConstraints, + "SHIFT", ) - .with_name("SHIFT") } /// Create the EQ AIR. -pub fn create_eq_air(proof_options: &ProofOptions) -> VmAir { - let (transition_constraints, _) = eq_constraints(0); - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: eq_bus_interactions(), - }; - AirWithBuses::new( +pub fn create_eq_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( eq_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + eq_bus_interactions(), proof_options, 1, - transition_constraints, + EqConstraints, + "EQ", ) - .with_name("EQ") } /// Create the BYTEWISE AIR. No polynomial constraints. -pub fn create_bytewise_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints: Vec>> = vec![]; - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: bytewise_bus_interactions(), - }; - AirWithBuses::new( +pub fn create_bytewise_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( bytewise_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + bytewise_bus_interactions(), proof_options, 1, - transition_constraints, + EmptyConstraints, + "BYTEWISE", ) - .with_name("BYTEWISE") } /// Create the STORE AIR. -pub fn create_store_air(proof_options: &ProofOptions) -> VmAir { - let (transition_constraints, _) = store_constraints(0); - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: store_bus_interactions(), - }; - AirWithBuses::new( +pub fn create_store_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( store_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + store_bus_interactions(), proof_options, 1, - transition_constraints, + StoreConstraints, + "STORE", ) - .with_name("STORE") } /// Create the CPU32 AIR. -pub fn create_cpu32_air(proof_options: &ProofOptions) -> VmAir { - let (transition_constraints, _) = cpu32_constraints(0); - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: cpu32_bus_interactions(), - }; - AirWithBuses::new( +pub fn create_cpu32_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( cpu32_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + cpu32_bus_interactions(), proof_options, 1, - transition_constraints, + Cpu32Constraints, + "CPU32", ) - .with_name("CPU32") } /// Create MEMW AIR with constraints and bus interactions. -pub fn create_memw_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints = memw_constraints(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: memw_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_memw_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( memw_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + memw_bus_interactions(), proof_options, 1, - transition_constraints, + MemwConstraints, + "MEMW", ) - .with_name("MEMW") } /// Create MEMW_A (aligned) AIR with constraints and bus interactions. -pub fn create_memw_aligned_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints = memw_aligned_constraints(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: memw_aligned_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_memw_aligned_air( + proof_options: &ProofOptions, +) -> ConcreteVmAir { + build_air( memw_aligned_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + memw_aligned_bus_interactions(), proof_options, 1, - transition_constraints, + MemwAlignedConstraints, + "MEMW_A", ) - .with_name("MEMW_A") } /// Create MEMW_R (register) AIR with constraints and bus interactions. -pub fn create_memw_register_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints = memw_register_constraints(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: memw_register_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_memw_register_air( + proof_options: &ProofOptions, +) -> ConcreteVmAir { + build_air( memw_register_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + memw_register_bus_interactions(), proof_options, 1, - transition_constraints, + MemwRegisterConstraints, + "MEMW_R", ) - .with_name("MEMW_R") } /// Create LOAD AIR with constraints and bus interactions. -pub fn create_load_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints = load_constraints(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: load_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_load_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( load_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + load_bus_interactions(), proof_options, 1, - transition_constraints, + LoadConstraints, + "LOAD", ) - .with_name("LOAD") } /// Create DECODE AIR with bus interactions. @@ -817,69 +778,41 @@ pub fn create_load_air(proof_options: &ProofOptions) -> VmAir { /// The DECODE table has no transition constraints (it's a pure lookup table). /// It receives lookups from the CPU table via the DECODE bus. /// -/// For production use with preprocessed verification, chain with `.with_preprocessed()`: -/// ```ignore -/// let decode_air = create_decode_air(&opts) -/// .with_preprocessed( -/// decode::compute_precomputed_commitment(&instructions, &opts), -/// decode::NUM_PRECOMPUTED_COLS, -/// ); -/// ``` -pub fn create_decode_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints: Vec>> = vec![]; - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: decode_bus_interactions(), - }; - - AirWithBuses::new( +/// For production use with preprocessed verification, chain with `.with_preprocessed()` +/// on the concrete `AirWithBuses` before boxing. +pub fn create_decode_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( decode_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + decode_bus_interactions(), proof_options, 1, - transition_constraints, + EmptyConstraints, + "DECODE", ) - .with_name("DECODE") } /// Create MUL AIR with constraints and bus interactions. -pub fn create_mul_air(proof_options: &ProofOptions) -> VmAir { - let (constraints, _) = mul_constraints(0); - let transition_constraints: Vec>> = - constraints.into_iter().map(|c| c.boxed()).collect(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: mul_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_mul_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( mul_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + mul_bus_interactions(), proof_options, 1, - transition_constraints, + MulConstraints, + "MUL", ) - .with_name("MUL") } /// Create DVRM AIR with constraints and bus interactions. -pub fn create_dvrm_air(proof_options: &ProofOptions) -> VmAir { - let (constraints, _) = dvrm_constraints(0); - let transition_constraints: Vec>> = - constraints.into_iter().map(|c| c.boxed()).collect(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: dvrm_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_dvrm_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( dvrm_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + dvrm_bus_interactions(), proof_options, 1, - transition_constraints, + DvrmConstraints, + "DVRM", ) - .with_name("DVRM") } /// Create BRANCH AIR with constraints and bus interactions. @@ -887,59 +820,39 @@ pub fn create_dvrm_air(proof_options: &ProofOptions) -> VmAir { /// The BRANCH table computes next_pc for branch/jump instructions: /// - For branches (BEQ, BLT, JAL): next_pc = pc + sign_extend(offset) /// - For JALR: next_pc = (register + sign_extend(offset)) & ~1 -pub fn create_branch_air(proof_options: &ProofOptions) -> VmAir { - let (constraints, _) = branch_constraints(0); - let transition_constraints: Vec>> = - constraints.into_iter().map(|c| c.boxed()).collect(); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: branch_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_branch_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( branch_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + branch_bus_interactions(), proof_options, 1, - transition_constraints, + BranchConstraints, + "BRANCH", ) - .with_name("BRANCH") } /// Create HALT AIR with bus interactions (no transition constraints). -pub fn create_halt_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints: Vec>> = vec![]; - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: halt_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_halt_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( halt_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + halt_bus_interactions(), proof_options, 1, - transition_constraints, + EmptyConstraints, + "HALT", ) - .with_name("HALT") } /// Create COMMIT AIR with constraints and bus interactions. -pub fn create_commit_air(proof_options: &ProofOptions) -> VmAir { - let (transition_constraints, _) = commit_constraints(0); - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: commit_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_commit_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( commit_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + commit_bus_interactions(), proof_options, 1, - transition_constraints, + CommitConstraints, + "COMMIT", ) - .with_name("COMMIT") } /// Create PAGE AIR with bus interactions for a specific page. @@ -949,147 +862,103 @@ pub fn create_commit_air(proof_options: &ProofOptions) -> VmAir { /// the base address of this page. /// /// The PAGE table has no transition constraints (it's a pure lookup table). -/// It interacts with: -/// - ARE_BYTES bus: range checks for init/fini values -/// - Memory bus: provides initial and final memory tokens -pub fn create_page_air(proof_options: &ProofOptions, page_base: u64) -> VmAir { - let transition_constraints: Vec>> = vec![]; - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: page_bus_interactions(page_base), - }; - - AirWithBuses::new( +pub fn create_page_air( + proof_options: &ProofOptions, + page_base: u64, +) -> ConcreteVmAir { + build_air( page_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + page_bus_interactions(page_base), proof_options, 1, - transition_constraints, + EmptyConstraints, + &format!("PAGE:0x{:x}", page_base), ) - .with_name(&format!("PAGE:0x{:x}", page_base)) } /// Create REGISTER AIR with bus interactions. /// /// The REGISTER table provides initial and final tokens for register accesses /// on the Memory bus (is_register=1). -pub fn create_register_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints: Vec>> = vec![]; - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: register_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_register_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( register_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + register_bus_interactions(), proof_options, 1, - transition_constraints, + EmptyConstraints, + "REGISTER", ) - .with_name("REGISTER") } /// Create KECCAK core AIR with ADD constraints and bus interactions. -pub fn create_keccak_air(proof_options: &ProofOptions) -> VmAir { - let (constraints, _) = crate::tables::keccak::create_constraints(0); - let transition_constraints: Vec>> = constraints; - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: keccak_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_keccak_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( keccak_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + keccak_bus_interactions(), proof_options, 1, - transition_constraints, + KeccakConstraints, + "KECCAK", ) - .with_name("KECCAK") } /// Create KECCAK_RND AIR with pi constraints and bus interactions. -pub fn create_keccak_rnd_air(proof_options: &ProofOptions) -> VmAir { - let (constraints, _) = crate::tables::keccak_rnd::create_constraints(0); - let transition_constraints: Vec>> = constraints; - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: keccak_rnd_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_keccak_rnd_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( keccak_rnd_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + keccak_rnd_bus_interactions(), proof_options, 1, - transition_constraints, + KeccakRndConstraints, + "KECCAK_RND", ) - .with_name("KECCAK_RND") } /// Create KECCAK_RC AIR with bus interactions (preprocessed table). -pub fn create_keccak_rc_air(proof_options: &ProofOptions) -> VmAir { - let transition_constraints: Vec>> = vec![]; - - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: keccak_rc_bus_interactions(), - }; - - AirWithBuses::new( +pub fn create_keccak_rc_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( keccak_rc_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + keccak_rc_bus_interactions(), proof_options, 1, - transition_constraints, + EmptyConstraints, + "KECCAK_RC", ) - .with_name("KECCAK_RC") } /// Create ECSM core AIR (secp256k1 scalar-multiplication orchestrator). -pub fn create_ecsm_air(proof_options: &ProofOptions) -> VmAir { - let (transition_constraints, _) = crate::tables::ecsm::create_constraints(0); - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: ecsm_bus_interactions(), - }; - AirWithBuses::new( +pub fn create_ecsm_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( ecsm_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + ecsm_bus_interactions(), proof_options, 1, - transition_constraints, + EcsmConstraints, + "ECSM", ) - .with_name("ECSM") } /// Create EC_SCALAR AIR (serves the scalar bit-by-bit to ECDAS). -pub fn create_ec_scalar_air(proof_options: &ProofOptions) -> VmAir { - let (transition_constraints, _) = crate::tables::ec_scalar::create_constraints(0); - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: ec_scalar_bus_interactions(), - }; - AirWithBuses::new( +pub fn create_ec_scalar_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( ec_scalar_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + ec_scalar_bus_interactions(), proof_options, 1, - transition_constraints, + EcScalarConstraints, + "EC_SCALAR", ) - .with_name("EC_SCALAR") } /// Create ECDAS AIR (per-step double/add of the scalar-multiplication sequence). -pub fn create_ecdas_air(proof_options: &ProofOptions) -> VmAir { - let (transition_constraints, _) = crate::tables::ecdas::create_constraints(0); - let auxiliary_trace_build_data = AuxiliaryTraceBuildData { - interactions: ecdas_bus_interactions(), - }; - AirWithBuses::new( +pub fn create_ecdas_air(proof_options: &ProofOptions) -> ConcreteVmAir { + build_air( ecdas_cols::NUM_COLUMNS, - auxiliary_trace_build_data, + ecdas_bus_interactions(), proof_options, 1, - transition_constraints, + EcdasConstraints, + "ECDAS", ) - .with_name("ECDAS") } diff --git a/prover/src/tests/bitwise_bus_tests.rs b/prover/src/tests/bitwise_bus_tests.rs index fd3b55cba..1782bd0fc 100644 --- a/prover/src/tests/bitwise_bus_tests.rs +++ b/prover/src/tests/bitwise_bus_tests.rs @@ -4,12 +4,12 @@ //! - Completeness: Valid lookups to BITWISE are accepted //! - Soundness: Invalid lookups to BITWISE are rejected +use stark::constraints::builder::EmptyConstraints; use std::collections::HashMap; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::element::FieldElement; -use stark::constraints::transition::TransitionConstraintEvaluator; use stark::lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, BusValue, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -54,9 +54,7 @@ mod receiver_cols { fn new_sender_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::sender( BusId::ByteAlu, @@ -84,15 +82,13 @@ fn new_sender_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn new_receiver_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::receiver( BusId::ByteAlu, @@ -120,7 +116,7 @@ fn new_receiver_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } diff --git a/prover/src/tests/bitwise_tests.rs b/prover/src/tests/bitwise_tests.rs index 984271225..c824764d3 100644 --- a/prover/src/tests/bitwise_tests.rs +++ b/prover/src/tests/bitwise_tests.rs @@ -7,6 +7,7 @@ use crate::tables::bitwise::{ use crate::tables::types::{BusId, FE}; use crate::test_utils::multi_prove_ram; use math::field::element::FieldElement; +use stark::constraints::builder::EmptyConstraints; use stark::lookup::Multiplicity; use stark::proof::options::ProofOptions; @@ -415,7 +416,6 @@ fn test_preprocessed_commitment_is_nonzero() { mod soundness_tests { use super::*; use crypto::fiat_shamir::default_transcript::DefaultTranscript; - use stark::constraints::transition::TransitionConstraintEvaluator; use stark::lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, BusValue, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -451,10 +451,9 @@ mod soundness_tests { fn create_sender_air( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { use crate::tables::types::{BusId, alu_op}; - let transition_constraints: Vec>> = vec![]; let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::sender( BusId::ByteAlu, @@ -482,20 +481,20 @@ mod soundness_tests { auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn create_receiver_air( proof_options: &ProofOptions, - ) -> AirWithBuses { + ) -> AirWithBuses { create_receiver_air_impl(proof_options, None) } fn create_receiver_air_preprocessed( proof_options: &ProofOptions, commitment: stark::config::Commitment, - ) -> AirWithBuses { + ) -> AirWithBuses { // 3 precomputed columns: X, Y, AND (column 3 = MU_AND is multiplicity) create_receiver_air_impl(proof_options, Some((commitment, 3))) } @@ -503,10 +502,9 @@ mod soundness_tests { fn create_receiver_air_impl( proof_options: &ProofOptions, preprocessed: Option<(stark::config::Commitment, usize)>, - ) -> AirWithBuses { + ) -> AirWithBuses { use crate::tables::types::{BusId, alu_op}; - let transition_constraints: Vec>> = vec![]; let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::receiver( BusId::ByteAlu, @@ -534,7 +532,7 @@ mod soundness_tests { auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ); match preprocessed { diff --git a/prover/src/tests/branch_bus_tests.rs b/prover/src/tests/branch_bus_tests.rs index 636f6dd34..ee81ebb5a 100644 --- a/prover/src/tests/branch_bus_tests.rs +++ b/prover/src/tests/branch_bus_tests.rs @@ -6,12 +6,12 @@ //! - Padding: Auto-padding to power of 2 works correctly //! - Border cases: Edge values (0, MAX, signed boundaries) work +use stark::constraints::builder::EmptyConstraints; use std::collections::HashMap; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::element::FieldElement; -use stark::constraints::transition::TransitionConstraintEvaluator; use stark::lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, BusValue, LinearTerm, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -66,9 +66,7 @@ mod sender_cols { fn new_sender_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::sender( BusId::Branch, @@ -124,15 +122,13 @@ fn new_sender_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn new_receiver_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { // Use the same bus interaction format as the BRANCH table receiver let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::receiver( @@ -205,7 +201,7 @@ fn new_receiver_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } diff --git a/prover/src/tests/dvrm_tests.rs b/prover/src/tests/dvrm_tests.rs index 6dfbe34c5..8a8d8c31b 100644 --- a/prover/src/tests/dvrm_tests.rs +++ b/prover/src/tests/dvrm_tests.rs @@ -4,7 +4,7 @@ use stark::proof::options::ProofOptions; use stark::traits::AIR; use crate::tables::dvrm::{ - DvrmOperation, bus_interactions, cols, dvrm_constraints, generate_dvrm_trace, + DvrmConstraints, DvrmOperation, bus_interactions, cols, dvrm_constraints, generate_dvrm_trace, }; use crate::tables::types::FE; use crate::test_utils::{ @@ -420,7 +420,7 @@ fn test_padding_row() { /// AIR — no explicit div-by-zero remainder constraint is needed. #[test] fn test_dvrm_rejects_false_div_by_zero_remainder() { - let air = busless_air(cols::NUM_COLUMNS, dvrm_constraints(0).0); + let air = busless_air(cols::NUM_COLUMNS, DvrmConstraints); // numerator = 20, denominator = 0 => div-by-zero, honest remainder = 20. let mut trace = generate_dvrm_trace(&[(DvrmOperation::new(20, 0, UNSIGNED), true)]); assert!( diff --git a/prover/src/tests/local_to_global_bus_tests.rs b/prover/src/tests/local_to_global_bus_tests.rs index 263e3d938..2234208df 100644 --- a/prover/src/tests/local_to_global_bus_tests.rs +++ b/prover/src/tests/local_to_global_bus_tests.rs @@ -5,13 +5,13 @@ //! program-end receiver (final value of each cell). The bus balances iff every //! epoch's `fini` matches the next epoch's `init` (the cross-epoch telescoping). +use stark::constraints::builder::EmptyConstraints; use std::collections::HashMap; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::element::FieldElement; use stark::config::Commitment; -use stark::constraints::transition::TransitionConstraintEvaluator; use stark::lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, BusValue, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -48,8 +48,7 @@ type Token = (u64, u64, u64); fn l2g_air( proof_options: &ProofOptions, epoch_label: u64, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { AirWithBuses::new( local_to_global::cols::NUM_COLUMNS, AuxiliaryTraceBuildData { @@ -57,15 +56,14 @@ fn l2g_air( }, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn anchor_air( proof_options: &ProofOptions, is_sender: bool, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { let values = vec![ BusValue::Packed { start_column: anchor_cols::ADDR_LO, @@ -96,7 +94,7 @@ fn anchor_air( }, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } @@ -116,8 +114,7 @@ fn anchor_trace(tokens: &[Token]) -> TraceTable { /// L2G air on the epoch-LOCAL `Memory` bus (uses `memory_bus_interactions`). fn l2g_memory_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { AirWithBuses::new( local_to_global::cols::NUM_COLUMNS, AuxiliaryTraceBuildData { @@ -125,7 +122,7 @@ fn l2g_memory_air( }, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } @@ -159,8 +156,7 @@ mod range_recv_cols { /// cell's fini token at the last timestamp (cancelling L2G's fini-send). fn memw_sub_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { let init_send = BusInteraction::sender( BusId::Memory, Multiplicity::One, @@ -216,15 +212,14 @@ fn memw_sub_air( }, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn l2g_range_air( proof_options: &ProofOptions, epoch_label: u64, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { AirWithBuses::new( local_to_global::cols::NUM_COLUMNS, AuxiliaryTraceBuildData { @@ -232,14 +227,13 @@ fn l2g_range_air( }, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn range_receiver_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { let interactions = vec![ BusInteraction::receiver( BusId::AreBytes, @@ -293,7 +287,7 @@ fn range_receiver_air( AuxiliaryTraceBuildData { interactions }, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } @@ -389,8 +383,7 @@ fn prove_verify_l2g_range_with_trace( /// sub-table root committed in the bus proof. fn inert_l2g_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; +) -> AirWithBuses { AirWithBuses::new( local_to_global::cols::NUM_COLUMNS, AuxiliaryTraceBuildData { @@ -398,7 +391,7 @@ fn inert_l2g_air( }, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } diff --git a/prover/src/tests/lt_bus_tests.rs b/prover/src/tests/lt_bus_tests.rs index b6148cfdc..e95a81285 100644 --- a/prover/src/tests/lt_bus_tests.rs +++ b/prover/src/tests/lt_bus_tests.rs @@ -6,12 +6,12 @@ //! - Padding: Auto-padding to power of 2 works correctly //! - Border cases: Edge values (0, MAX, signed boundaries) work +use stark::constraints::builder::EmptyConstraints; use std::collections::HashMap; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::element::FieldElement; -use stark::constraints::transition::TransitionConstraintEvaluator; use stark::lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, BusValue, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -65,9 +65,7 @@ mod sender_cols { fn new_sender_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::sender( BusId::Alu, @@ -114,15 +112,13 @@ fn new_sender_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } fn new_receiver_air( proof_options: &ProofOptions, -) -> AirWithBuses { - let transition_constraints: Vec>> = vec![]; - +) -> AirWithBuses { // Use the same bus interaction as the LT table let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![BusInteraction::receiver( @@ -170,7 +166,7 @@ fn new_receiver_air( auxiliary_trace_build_data, proof_options, 1, - transition_constraints, + EmptyConstraints, ) } diff --git a/prover/src/tests/lt_tests.rs b/prover/src/tests/lt_tests.rs index 77d8d1a89..92068b67b 100644 --- a/prover/src/tests/lt_tests.rs +++ b/prover/src/tests/lt_tests.rs @@ -3,7 +3,9 @@ use stark::proof::options::ProofOptions; use stark::traits::AIR; -use crate::tables::lt::{LtOperation, bus_interactions, cols, generate_lt_trace, lt_constraints}; +use crate::tables::lt::{ + LtConstraints, LtOperation, bus_interactions, cols, generate_lt_trace, lt_constraints, +}; use crate::tables::types::FE; use crate::test_utils::{busless_air, create_lt_air, in_chip_constraint_count, validate_busless}; @@ -182,7 +184,7 @@ fn test_bus_interactions_count() { /// `LtFormula`, evaluated in isolation over a bus-less AIR. #[test] fn test_lt_rejects_false_comparison() { - let air = busless_air(cols::NUM_COLUMNS, lt_constraints(0).0); + let air = busless_air(cols::NUM_COLUMNS, LtConstraints); let mut trace = generate_lt_trace(&[LtOperation::new(20, 10, UNSIGNED)]); assert!( validate_busless(&air, &trace), @@ -217,7 +219,7 @@ fn test_lt_air_wires_in_chip_constraints() { /// here, since `LtFormula` only binds `lt`. #[test] fn test_lt_rejects_forged_out() { - let air = busless_air(cols::NUM_COLUMNS, lt_constraints(0).0); + let air = busless_air(cols::NUM_COLUMNS, LtConstraints); // 20 >> = vec![]; let auxiliary_trace_build_data = AuxiliaryTraceBuildData { interactions: vec![], // NO bus interactions }; - let cpu_air: AirWithBuses = - AirWithBuses::new( - crate::tables::cpu::cols::NUM_COLUMNS, - auxiliary_trace_build_data, - &proof_options, - 1, - transition_constraints, - ); + let cpu_air: AirWithBuses< + F, + E, + stark::lookup::NullBoundaryConstraintBuilder, + (), + EmptyConstraints, + > = AirWithBuses::new( + crate::tables::cpu::cols::NUM_COLUMNS, + auxiliary_trace_build_data, + &proof_options, + 1, + EmptyConstraints, + ); let air_trace_pairs: Vec<( &dyn AIR, @@ -2493,8 +2497,8 @@ fn test_crafted_zero_count_proof_must_not_verify() { _, _, )> = vec![ - (&airs.bitwise, &mut bitwise_trace, &()), - (&airs.decode, &mut decode_trace, &()), + (airs.bitwise.as_ref(), &mut bitwise_trace, &()), + (airs.decode.as_ref(), &mut decode_trace, &()), ]; let proof = multi_prove_ram(pairs, &mut DefaultTranscript::::new(&[])) @@ -3138,17 +3142,21 @@ fn test_epoch_proof_commits_l2g() { ); // Inert L2G AIR: commits the trace columns, but no bus and no constraints. - let transition_constraints: Vec>> = vec![]; - let inert_l2g_air: AirWithBuses = - AirWithBuses::new( - local_to_global::cols::NUM_COLUMNS, - AuxiliaryTraceBuildData { - interactions: vec![], - }, - &proof_options, - 1, - transition_constraints, - ); + let inert_l2g_air: AirWithBuses< + F, + E, + stark::lookup::NullBoundaryConstraintBuilder, + (), + EmptyConstraints, + > = AirWithBuses::new( + local_to_global::cols::NUM_COLUMNS, + AuxiliaryTraceBuildData { + interactions: vec![], + }, + &proof_options, + 1, + EmptyConstraints, + ); let mut pairs = airs.air_trace_pairs(&mut traces); pairs.push((&inert_l2g_air, &mut l2g_trace, &())); @@ -3292,17 +3300,21 @@ fn test_continuation_pipeline_end_to_end() { ); let mut l2g_trace = local_to_global::generate_local_to_global_trace(&boundaries[i]); - let transition_constraints: Vec>> = vec![]; - let inert_l2g_air: AirWithBuses = - AirWithBuses::new( - local_to_global::cols::NUM_COLUMNS, - AuxiliaryTraceBuildData { - interactions: vec![], - }, - &proof_options, - 1, - transition_constraints, - ); + let inert_l2g_air: AirWithBuses< + F, + E, + stark::lookup::NullBoundaryConstraintBuilder, + (), + EmptyConstraints, + > = AirWithBuses::new( + local_to_global::cols::NUM_COLUMNS, + AuxiliaryTraceBuildData { + interactions: vec![], + }, + &proof_options, + 1, + EmptyConstraints, + ); let mut pairs = airs.air_trace_pairs(&mut traces); pairs.push((&inert_l2g_air, &mut l2g_trace, &())); @@ -3417,17 +3429,21 @@ fn test_epoch_memory_bus_with_l2g_bookend() { ); // L2G air on the epoch-local Memory bus (the bookend that replaces PAGE). - let transition_constraints: Vec>> = vec![]; - let l2g_air: AirWithBuses = - AirWithBuses::new( - local_to_global::cols::NUM_COLUMNS, - AuxiliaryTraceBuildData { - interactions: local_to_global::memory_bus_interactions(), - }, - &proof_options, - 1, - transition_constraints, - ); + let l2g_air: AirWithBuses< + F, + E, + stark::lookup::NullBoundaryConstraintBuilder, + (), + EmptyConstraints, + > = AirWithBuses::new( + local_to_global::cols::NUM_COLUMNS, + AuxiliaryTraceBuildData { + interactions: local_to_global::memory_bus_interactions(), + }, + &proof_options, + 1, + EmptyConstraints, + ); // Take the L2G trace out of `traces` so `air_trace_pairs` can borrow the rest. let mut l2g_trace = std::mem::replace( diff --git a/scripts/cross_verify_vm.sh b/scripts/cross_verify_vm.sh new file mode 100755 index 000000000..75d9f35fd --- /dev/null +++ b/scripts/cross_verify_vm.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# +# cross_verify_vm.sh — cross-version verification of the FULL VM prover/verifier. +# +# WHY: the single-source constraints migration must preserve the constraint +# system EXACTLY — order, indices, num_base split, per-constraint degree, +# zerofier shape, and the transcript. Prove/verify within one version cannot +# see a self-consistent drift (a version that reorders constraints still +# accepts its own proofs). Cross-verifying — each side's proofs checked by the +# OTHER side's verifier — does: the verifier recomputes the OOD constraint +# evaluations from ITS OWN constraint definitions against the other side's +# commitments, so any semantic difference fails loudly. Needs no proof +# determinism (this system's proofs are nondeterministic by design: grinding + +# order-free HashMap trace tables). +# +# This is the VM-scale analog of scripts/cross_verify_examples.sh: it builds the +# `cli` binary (cargo build --release -p cli) at REF_OLD and REF_NEW in an +# isolated worktree (same build-both-refs pattern as scripts/bench_abba.sh) and +# exchanges real VM proofs over a handful of small test ELFs. +# +# WHAT IT DOES: +# 1. Builds bin/cli at REF_OLD and REF_NEW (isolated worktree). +# 2. Per ELF: prove NEW -> verify OLD, and prove OLD -> verify NEW. +# 3. Prints a per-ELF, per-direction PASS/FAIL table; exits nonzero on any +# failure. A failing direction is a REAL migration finding (ordering / +# num_base / alpha-power indexing / zerofier grouping / transcript) — +# diagnose and fix the NEW side, never the old one. +# +# USAGE: +# scripts/cross_verify_vm.sh REF_OLD REF_NEW +# REF_OLD ref or SHA with the pre-migration (boxed) constraint system +# REF_NEW ref or SHA with the migrated (single-source) constraint system +# Env: WORK work/output dir (default /tmp/cross_verify_vm) +# WT build worktree (default /tmp/cross_verify_vm_wt) +# ELFS space-separated absolute ELF paths (default: a few small asm ELFs +# from executor/program_artifacts/asm, built via +# `make compile-programs-asm` if absent) + +set -euo pipefail + +if [ $# -ne 2 ]; then + echo "usage: cross_verify_vm.sh REF_OLD REF_NEW" >&2 + exit 2 +fi +REF_OLD="$1" +REF_NEW="$2" + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +# --- ELF fixtures: small asm programs the prove_elfs tests exercise. ----------- +# The CLI consumes prebuilt ELF files; the asm artifacts are produced by +# `make compile-programs-asm` (a plain clang invocation, no sysroot needed). +ASM_DIR="$ROOT/executor/program_artifacts/asm" +DEFAULT_ELF_NAMES=(sub add arith_8) +if [ -z "${ELFS:-}" ]; then + # Build the asm artifacts if the ones we need are missing. + missing=0 + for n in "${DEFAULT_ELF_NAMES[@]}"; do + [ -f "$ASM_DIR/$n.elf" ] || missing=1 + done + if [ "$missing" = "1" ]; then + echo "==> Building asm ELF artifacts (make compile-programs-asm)" + make compile-programs-asm >/dev/null + fi + ELFS="" + for n in "${DEFAULT_ELF_NAMES[@]}"; do + ELFS="$ELFS $ASM_DIR/$n.elf" + done +fi +# shellcheck disable=SC2206 +ELF_LIST=($ELFS) + +WORK="${WORK:-/tmp/cross_verify_vm}" +WT="${WT:-/tmp/cross_verify_vm_wt}" + +SHA_OLD="$(git rev-parse "$REF_OLD")" +SHA_NEW="$(git rev-parse "$REF_NEW")" +echo "==> Refs" +echo " OLD $REF_OLD -> ${SHA_OLD:0:10}" +echo " NEW $REF_NEW -> ${SHA_NEW:0:10}" +echo "==> ELFs: ${ELF_LIST[*]}" + +mkdir -p "$WORK" + +# --- 1. Build both cli binaries in an isolated worktree ------------------------ +cleanup() { git worktree remove --force "$WT" 2>/dev/null || true; } +trap cleanup EXIT +git worktree remove --force "$WT" 2>/dev/null || true +git worktree add --detach "$WT" "$SHA_OLD" >/dev/null +build_cli() { # $1=sha $2=out (shared target dir -> 2nd build is incremental) + echo "==> Building cli @ ${1:0:10} -> $2" + git -C "$WT" checkout --quiet -f "$1" + if ! (cd "$WT" && cargo build --release -p cli >"$WORK/build_$2.log" 2>&1); then + echo "ERROR: cargo build failed for $2 (@ ${1:0:10}). Tail of $WORK/build_$2.log:" >&2 + tail -40 "$WORK/build_$2.log" >&2 + exit 1 + fi + cp "$WT/target/release/cli" "$WORK/$2" +} +build_cli "$SHA_OLD" cli_old +build_cli "$SHA_NEW" cli_new +cleanup +trap - EXIT + +# --- 2. Cross-verify every ELF in both directions ----------------------------- +fail=0 +check() { # $1=prover bin $2=verifier bin $3=elf path $4=direction label + local elf="$3" + local tag + tag="$(basename "$elf" .elf)" + local proof="$WORK/$tag.$4.bin" + if ! "$WORK/$1" prove "$elf" -o "$proof" >"$WORK/$tag.$4.prove.log" 2>&1; then + echo "FAIL $4 : $tag (PROVE errored; see $WORK/$tag.$4.prove.log)" + fail=1 + return + fi + if "$WORK/$2" verify "$proof" "$elf" >"$WORK/$tag.$4.verify.log" 2>&1; then + echo "PASS $4 : $tag" + else + echo "FAIL $4 : $tag (VERIFY rejected; see $WORK/$tag.$4.verify.log)" + fail=1 + fi +} + +echo "==> Cross-verifying ${#ELF_LIST[@]} ELFs, both directions" +for elf in "${ELF_LIST[@]}"; do + check cli_new cli_old "$elf" "prove-NEW-verify-OLD" + check cli_old cli_new "$elf" "prove-OLD-verify-NEW" +done + +echo +if [ "$fail" = "0" ]; then + echo "==> RESULT: all ${#ELF_LIST[@]} ELFs cross-verify in both directions." +else + echo "==> RESULT: FAILURES above — the migration drifted from the old constraint system." +fi +exit "$fail" From 4c1a94307f79ccbfa59d7e36f7a7d953c4eb612d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 10:34:15 -0300 Subject: [PATCH 31/52] scripts: VM cross-version verification harness + pre-deletion run log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cross_verify_vm.sh mirrors cross_verify_examples.sh + bench_abba.sh's build-both-refs worktree pattern, but builds bin/cli (cargo build --release -p cli) and exchanges real VM proofs (cli prove -o proof.bin / cli verify proof.bin ) over small asm test ELFs (sub, add, arith_8). Run REF_OLD=2499b2a8 (pre-switch boxed path) vs REF_NEW=3239eb8a (single-source): all 3 ELFs cross-verify in BOTH directions — the new prover's proofs verify under the old verifier and vice versa, so the constraint system (order, indices, num_base split, zerofier grouping, transcript) is preserved exactly. --- reports/sscs/cross_verify_vm_pre-deletion.log | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 reports/sscs/cross_verify_vm_pre-deletion.log diff --git a/reports/sscs/cross_verify_vm_pre-deletion.log b/reports/sscs/cross_verify_vm_pre-deletion.log new file mode 100644 index 000000000..a67a2ef34 --- /dev/null +++ b/reports/sscs/cross_verify_vm_pre-deletion.log @@ -0,0 +1,16 @@ +==> Refs + OLD 2499b2a8 -> 2499b2a821 + NEW 3239eb8a -> 3239eb8af6 +==> ELFs: /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/sub.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/add.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/arith_8.elf +Preparing worktree (detached HEAD 2499b2a8) +==> Building cli @ 2499b2a821 -> cli_old +==> Building cli @ 3239eb8af6 -> cli_new +==> Cross-verifying 3 ELFs, both directions +PASS prove-NEW-verify-OLD : sub +PASS prove-OLD-verify-NEW : sub +PASS prove-NEW-verify-OLD : add +PASS prove-OLD-verify-NEW : add +PASS prove-NEW-verify-OLD : arith_8 +PASS prove-OLD-verify-NEW : arith_8 + +==> RESULT: all 3 ELFs cross-verify in both directions. From 734faae04ce377d1732aa19d8c8594700d4f1aba Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 11:16:28 -0300 Subject: [PATCH 32/52] stark+prover: delete the old boxed constraint machinery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single-source path is the only path now; the oracle has served (VM cross-verification passed in both directions pre-deletion). Deleted: - crypto/stark/src/constraints/transition.rs (TransitionConstraintEvaluator, TransitionConstraint, TransitionConstraintAdapter, boxed()) — the zerofier bodies live on as the constraints/zerofier.rs free functions. - The old LogUp constraint structs (LookupBatchedTermConstraint, LookupAccumulatedConstraint) + their evaluate fns and the compute_multiplicity_from_step / compute_fingerprint_from_step helpers. - Every old per-constraint struct + TransitionConstraint impl across prover/src/tables/*.rs, prover/src/constraints/{cpu,templates}.rs, and all the old boxed builder fns (eq_constraints, lt_constraints, ..., create_all_cpu_constraints, create_constraints). - crypto/stark/src/tests/transition_tests.rs (old-trait users, incl. the period=2/offset=1 case — the last old-path user) and the zerofier.rs equivalence-test module (its oracle was the deleted trait defaults). Migration-scaffolding tests keep their teeth without the old oracle: the folder-vs-capture-interpret comparisons (all three interpretations of the ONE body must agree bit-for-bit on 1000 random rows) stay as permanent tests in constraint_set_tests_a/b, constraint_emit_tests and the lookup.rs LogUp tests; table unit tests (ecsm/ecdas/ec_scalar/cpu32) now drive the ConstraintSet through ProverEvalFolder instead of the old structs. cargo test -p stark: 164 passed. cargo test --release -p lambda-vm-prover: 491 passed, 5 failed (pre-existing missing rust-guest ELF artifacts, fail on main too). --- crypto/stark/src/constraints/mod.rs | 1 - crypto/stark/src/constraints/transition.rs | 459 ------------ crypto/stark/src/constraints/zerofier.rs | 226 +----- crypto/stark/src/lookup.rs | 481 ++---------- crypto/stark/src/tests/mod.rs | 1 - crypto/stark/src/tests/transition_tests.rs | 84 --- prover/src/constraints/cpu.rs | 689 +----------------- prover/src/constraints/templates.rs | 327 +-------- prover/src/tables/branch.rs | 215 +----- prover/src/tables/commit.rs | 135 +--- prover/src/tables/cpu.rs | 2 +- prover/src/tables/cpu32.rs | 269 +------ prover/src/tables/dvrm.rs | 342 --------- prover/src/tables/ec_scalar.rs | 106 --- prover/src/tables/ecdas.rs | 266 +------ prover/src/tables/ecsm.rs | 322 +------- prover/src/tables/eq.rs | 91 +-- prover/src/tables/keccak.rs | 117 +-- prover/src/tables/keccak_rnd.rs | 40 +- prover/src/tables/load.rs | 167 ----- prover/src/tables/lt.rs | 266 +------ prover/src/tables/memw.rs | 166 +---- prover/src/tables/memw_aligned.rs | 100 +-- prover/src/tables/memw_register.rs | 67 +- prover/src/tables/mul.rs | 226 ------ prover/src/tables/shift.rs | 263 +------ prover/src/tables/store.rs | 92 +-- prover/src/tests/branch_constraints_tests.rs | 49 +- prover/src/tests/commit_tests.rs | 30 +- prover/src/tests/constraint_emit_tests.rs | 372 ++-------- prover/src/tests/constraint_set_tests_a.rs | 212 ++---- prover/src/tests/constraint_set_tests_b.rs | 237 ++---- prover/src/tests/constraints_tests.rs | 171 +---- prover/src/tests/cpu32_tests.rs | 104 ++- prover/src/tests/dvrm_tests.rs | 5 +- prover/src/tests/ec_scalar_tests.rs | 76 +- prover/src/tests/ecdas_tests.rs | 140 ++-- prover/src/tests/ecsm_tests.rs | 174 +---- prover/src/tests/lt_tests.rs | 9 +- prover/src/tests/mul_tests.rs | 7 +- prover/src/tests/trace_builder_tests.rs | 8 +- .../impl-plan-single-source-constraints.md | 33 + 42 files changed, 539 insertions(+), 6608 deletions(-) delete mode 100644 crypto/stark/src/constraints/transition.rs delete mode 100644 crypto/stark/src/tests/transition_tests.rs diff --git a/crypto/stark/src/constraints/mod.rs b/crypto/stark/src/constraints/mod.rs index cf3921629..0deee0d41 100644 --- a/crypto/stark/src/constraints/mod.rs +++ b/crypto/stark/src/constraints/mod.rs @@ -3,5 +3,4 @@ pub mod builder; #[cfg(test)] mod builder_tests; pub mod evaluator; -pub mod transition; pub mod zerofier; diff --git a/crypto/stark/src/constraints/transition.rs b/crypto/stark/src/constraints/transition.rs deleted file mode 100644 index 1fe249c4c..000000000 --- a/crypto/stark/src/constraints/transition.rs +++ /dev/null @@ -1,459 +0,0 @@ -use core::ops::Div; - -use crate::domain::Domain; -use crate::traits::TransitionEvaluationContext; -use math::field::element::FieldElement; -use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; - -/// TransitionConstraintEvaluator represents the behaviour that a transition constraint -/// over the computation that wants to be proven must comply with. -pub trait TransitionConstraintEvaluator: Send + Sync -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - /// The degree of the constraint interpreting it as a multivariate polynomial. - fn degree(&self) -> usize; - - /// The index of the constraint. - /// Each transition constraint should have one index in the range [0, N), - /// where N is the total number of transition constraints. - fn constraint_idx(&self) -> usize; - - /// The function representing the evaluation of the constraint over elements - /// of the trace table. - /// - /// Elements of the trace table are found in the `frame` input, and depending on the - /// constraint, elements of `periodic_values` and `rap_challenges` may be used in - /// the evaluation. - /// Once computed, the evaluation should be inserted in the `transition_evaluations` - /// vector, in the index corresponding to the constraint as given by `constraint_idx()`. - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ); - - /// The periodicity the constraint is applied over the trace. - /// - /// Default value is 1, meaning that the constraint is applied to every - /// step of the trace. - fn period(&self) -> usize { - 1 - } - - /// The offset with respect to the first trace row, where the constraint - /// is applied. - /// For example, if the constraint has periodicity 2 and offset 1, this means - /// the constraint will be applied over trace rows of index 1, 3, 5, etc. - /// - /// Default value is 0, meaning that the constraint is applied from the first - /// element of the trace on. - fn offset(&self) -> usize { - 0 - } - - /// For a more fine-grained description of where the constraint should apply, - /// an exemptions period can be defined. - /// This specifies the periodicity of the row indexes where the constraint should - /// NOT apply, within the row indexes where the constraint applies, as specified by - /// `period()` and `offset()`. - /// - /// Default value is None. - fn exemptions_period(&self) -> Option { - None - } - - /// The offset value for periodic exemptions. Check documentation of `period()`, - /// `offset()` and `exemptions_period` for a better understanding. - fn periodic_exemptions_offset(&self) -> Option { - None - } - - /// The number of exemptions at the end of the trace. - /// - /// This method's output defines what trace elements should not be considered for - /// the constraint evaluation at the end of the trace. For example, for a fibonacci - /// computation that has to use the result 2 following steps, this method is defined - /// to return the value 2. - /// - /// Default value is 0, meaning the constraint applies to all rows including the last. - fn end_exemptions(&self) -> usize { - 0 - } - - /// Prover-optimized evaluation that writes base-field constraints to `base_evals` - /// and extension-field constraints to `ext_evals`. - /// - /// Constraints with `constraint_idx() < base_evals.len()` are "base" constraints - /// and MUST override this to write `FieldElement` into `base_evals[constraint_idx()]`. - /// Extension constraints (LogUp etc.) use the default, which asserts the index is - /// in the extension range and delegates to `evaluate()`. - fn evaluate_prover( - &self, - evaluation_context: &TransitionEvaluationContext, - base_evals: &mut [FieldElement], - ext_evals: &mut [FieldElement], - ) { - debug_assert!( - self.constraint_idx() >= base_evals.len(), - "Base constraint idx {} must override evaluate_prover()", - self.constraint_idx(), - ); - self.evaluate_verifier(evaluation_context, ext_evals); - } - - /// Roots of the end-exemptions polynomial `∏(x - rᵢ)`. - /// - /// The end-exemptions polynomial vanishes on the last `end_exemptions()` - /// rows the constraint must skip. This returns its roots `rᵢ` so callers can - /// evaluate the product `∏(x - rᵢ)` directly at the points they need — the - /// eval-form replacement for the former coefficient-form `end_exemptions_poly`. - /// The default implementation should normally not be changed. - fn end_exemptions_roots( - &self, - trace_primitive_root: &FieldElement, - trace_length: usize, - ) -> Vec> { - let end_exemptions = self.end_exemptions(); - if end_exemptions == 0 { - return Vec::new(); - } - // Last row in the constraint's evaluation domain is g^(offset + N - period); - // walking backward by g^period gives the remaining end-exemption roots. - let period = self.period(); - let decrement = trace_primitive_root.pow(trace_length - period); - let mut current = trace_primitive_root.pow(self.offset() + trace_length - period); - let mut roots = Vec::with_capacity(end_exemptions); - for _ in 0..end_exemptions { - roots.push(current.clone()); - current = ¤t * &decrement; - } - roots - } - - /// Evaluations of the end-exemptions polynomial `∏(x - rᵢ)` over the LDE - /// domain. - /// - /// Eval-form replacement for FFT-evaluating the coefficient-form polynomial: - /// the product has degree `end_exemptions()` (≤ 2 in practice), so the direct - /// `O(N · end_exemptions)` product over the precomputed LDE coset is cheaper - /// than an `O(N log N)` FFT. With no exemptions this yields all ones. - fn end_exemptions_lde_evaluations(&self, domain: &Domain) -> Vec> { - let roots = self.end_exemptions_roots( - &domain.trace_primitive_root, - domain.trace_roots_of_unity.len(), - ); - domain - .lde_roots_of_unity_coset - .iter() - .map(|x| { - roots - .iter() - .fold(FieldElement::::one(), |acc, r| acc * (x - r)) - }) - .collect() - } - - /// Compute evaluations of the constraints zerofier over a LDE domain. - #[allow(unstable_name_collisions)] - fn zerofier_evaluations_on_extended_domain(&self, domain: &Domain) -> Vec> { - let blowup_factor = domain.blowup_factor; - let trace_length = domain.trace_roots_of_unity.len(); - let trace_primitive_root = &domain.trace_primitive_root; - let coset_offset = &domain.coset_offset; - let lde_root_order = u64::from((blowup_factor * trace_length).trailing_zeros()); - let lde_root = F::get_primitive_root_of_unity(lde_root_order).unwrap(); - - // If there is an exemptions period defined for this constraint, the evaluations are calculated directly - // by computing P_exemptions(x) / Zerofier(x) - if let Some(exemptions_period) = self.exemptions_period() { - // FIXME: Rather than making this assertions here, it would be better to handle these - // errors or make these checks when the AIR is initialized. - - debug_assert!(exemptions_period.is_multiple_of(self.period())); - - debug_assert!(self.periodic_exemptions_offset().is_some()); - - // The elements of the domain have order `trace_length * blowup_factor`, so the zerofier evaluations - // without the end exemptions, repeat their values after `blowup_factor * exemptions_period` iterations, - // so we only need to compute those. - let last_exponent = blowup_factor * exemptions_period; - let numerator_power = trace_length / exemptions_period; - let denominator_power = trace_length / self.period(); - let offset_exponent = - trace_length * self.periodic_exemptions_offset().unwrap() / exemptions_period; - let numerator_offset = trace_primitive_root.pow(offset_exponent); - let denominator_offset = trace_primitive_root.pow(self.offset() * denominator_power); - let numerator_step = lde_root.pow(numerator_power); - let denominator_step = lde_root.pow(denominator_power); - let mut numerator_eval = coset_offset.pow(numerator_power); - let mut denominator_eval = coset_offset.pow(denominator_power); - - let mut numerators = Vec::with_capacity(last_exponent); - let mut denominators = Vec::with_capacity(last_exponent); - for _ in 0..last_exponent { - numerators.push(&numerator_eval - &numerator_offset); - denominators.push(&denominator_eval - &denominator_offset); - numerator_eval = &numerator_eval * &numerator_step; - denominator_eval = &denominator_eval * &denominator_step; - } - - // Batch inversion: O(3N) muls + 1 inversion instead of N individual inversions - // (each ~72 muls for Goldilocks Fermat chain). Denominators are guaranteed non-zero - // because the sets of powers of `offset_times_x` and `trace_primitive_root` are - // disjoint, provided that the offset is neither an element of the interpolation - // domain nor part of a subgroup with order less than n. - FieldElement::inplace_batch_inverse(&mut denominators).unwrap(); - - let evaluations: Vec<_> = numerators - .iter() - .zip(denominators.iter()) - .map(|(num, denom_inv)| num * denom_inv) - .collect(); - - // Mirror the else-branch fast path: with no end exemptions the zerofier stays - // cyclic, so return the short period-length vector and let the consumer cycle. - if self.end_exemptions() == 0 { - return evaluations; - } - - // FIXME: Instead of computing this evaluations for each constraint, they can be computed - // once for every constraint with the same end exemptions (combination of end_exemptions() - // and period). - let end_exemption_evaluations = self.end_exemptions_lde_evaluations(domain); - - let cycled_evaluations = evaluations - .iter() - .cycle() - .take(end_exemption_evaluations.len()); - - core::iter::zip(cycled_evaluations, end_exemption_evaluations) - .map(|(eval, exemption_eval)| eval * exemption_eval) - .collect() - - // In this else branch, the zerofiers are computed as the numerator, then inverted - // using batch inverse and then multiplied by P_exemptions(x). This way we don't do - // useless divisions. - } else { - let last_exponent = blowup_factor * self.period(); - let denominator_power = trace_length / self.period(); - let denominator_offset = trace_primitive_root.pow(self.offset() * denominator_power); - let denominator_step = lde_root.pow(denominator_power); - let mut denominator_eval = coset_offset.pow(denominator_power); - - let mut evaluations = Vec::with_capacity(last_exponent); - for _ in 0..last_exponent { - evaluations.push(&denominator_eval - &denominator_offset); - denominator_eval = &denominator_eval * &denominator_step; - } - - FieldElement::inplace_batch_inverse(&mut evaluations).unwrap(); - - // Fast path: when end_exemptions == 0 there are no exemption roots, so - // the zerofier stays cyclic — return the short period-length vector - // directly instead of expanding it over the full LDE domain. - if self.end_exemptions() == 0 { - return evaluations; - } - - let end_exemption_evaluations = self.end_exemptions_lde_evaluations(domain); - - let cycled_evaluations = evaluations - .iter() - .cycle() - .take(end_exemption_evaluations.len()); - - core::iter::zip(cycled_evaluations, end_exemption_evaluations) - .map(|(eval, exemption_eval)| eval * exemption_eval) - .collect() - } - } - - /// Returns the evaluation of the zerofier corresponding to this constraint in some point - /// `z`, which could be in a field extension. - #[allow(unstable_name_collisions)] - fn evaluate_zerofier( - &self, - z: &FieldElement, - trace_primitive_root: &FieldElement, - trace_length: usize, - ) -> FieldElement { - let end_exemptions_roots = self.end_exemptions_roots(trace_primitive_root, trace_length); - // Factor `z - rᵢ` written as `-(rᵢ - z)`: the field ops only go - // subfield − superfield, and `rᵢ ∈ F`, `z ∈ E`. - let end_exemptions_eval = end_exemptions_roots - .iter() - .fold(FieldElement::::one(), |acc, root| { - acc * -(root.clone() - z.clone()) - }); - - if let Some(exemptions_period) = self.exemptions_period() { - debug_assert!(exemptions_period.is_multiple_of(self.period())); - - debug_assert!(self.periodic_exemptions_offset().is_some()); - - let periodic_exemptions_offset = self.periodic_exemptions_offset().unwrap(); - let offset_exponent = trace_length * periodic_exemptions_offset / exemptions_period; - - let numerator = -trace_primitive_root.pow(offset_exponent) - + z.pow(trace_length / exemptions_period); - let denominator = -trace_primitive_root - .pow(self.offset() * trace_length / self.period()) - + z.pow(trace_length / self.period()); - // The denominator is non-zero: z is sampled outside the set of primitive roots. - return numerator - .div(denominator) - .expect("zerofier denominator is non-zero: z is sampled out-of-domain") - * &end_exemptions_eval; - } - - (-trace_primitive_root.pow(self.offset() * trace_length / self.period()) - + z.pow(trace_length / self.period())) - .inv() - .unwrap() - * &end_exemptions_eval - } -} - -// ============================================================================= -// User-facing TransitionConstraint trait + adapter -// ============================================================================= - -use crate::table::TableView; - -/// User-facing trait for defining transition constraints. -/// -/// Implement `evaluate()` to define the polynomial identity; the verifier and -/// prover evaluation paths are auto-generated via `.boxed()`. -/// -/// The `evaluate` method is generic over its field types so the same polynomial -/// works for both the prover (`TableView`) and verifier (`TableView`). -pub trait TransitionConstraint: Send + Sync -where - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - /// The degree of the constraint as a multivariate polynomial. - fn degree(&self) -> usize; - - /// Unique index in `[0, N)` where N is the total number of transition constraints. - fn constraint_idx(&self) -> usize; - - /// Number of exempted rows at the end of the trace. - fn end_exemptions(&self) -> usize { - 0 - } - - /// Evaluate the constraint polynomial on a trace step. - /// - /// Generic over the field so the same polynomial works for both - /// prover (FF=F, returns FieldElement) and verifier (FF=E, returns FieldElement). - fn evaluate(&self, step: &TableView) -> FieldElement - where - FF: IsSubFieldOf, - EE: IsField; - - /// Periodicity (default 1 = every row). - fn period(&self) -> usize { - 1 - } - - /// Offset for periodic application (default 0). - fn offset(&self) -> usize { - 0 - } - - /// Exemptions period (default None). - fn exemptions_period(&self) -> Option { - None - } - - /// Offset for periodic exemptions (default None). - fn periodic_exemptions_offset(&self) -> Option { - None - } - - /// Wrap into a boxed `TransitionConstraintEvaluator` for the evaluator. - /// - /// The adapter auto-generates `evaluate_verifier()` and `evaluate_prover()` - /// from the generic `evaluate()`. - fn boxed(self) -> Box> - where - Self: Sized + 'static, - { - Box::new(TransitionConstraintAdapter(self)) - } -} - -/// Adapter: implements `TransitionConstraintEvaluator` for any `TransitionConstraint`. -/// -/// Auto-generates `evaluate_verifier()` (E×E path) and `evaluate_prover()` (F path) -/// from the user's generic `evaluate()`. -pub struct TransitionConstraintAdapter(pub T); - -impl TransitionConstraintEvaluator for TransitionConstraintAdapter -where - T: TransitionConstraint + 'static, - F: IsSubFieldOf + IsFFTField + Send + Sync, - E: IsField + Send + Sync, -{ - fn degree(&self) -> usize { - self.0.degree() - } - fn constraint_idx(&self) -> usize { - self.0.constraint_idx() - } - fn end_exemptions(&self) -> usize { - self.0.end_exemptions() - } - fn period(&self) -> usize { - self.0.period() - } - fn offset(&self) -> usize { - self.0.offset() - } - fn exemptions_period(&self) -> Option { - self.0.exemptions_period() - } - fn periodic_exemptions_offset(&self) -> Option { - self.0.periodic_exemptions_offset() - } - - fn evaluate_verifier( - &self, - ctx: &TransitionEvaluationContext, - evals: &mut [FieldElement], - ) { - let idx = self.0.constraint_idx(); - match ctx { - TransitionEvaluationContext::Prover { frame, .. } => { - evals[idx] = self.0.evaluate(frame.get_evaluation_step(0)).to_extension(); - } - TransitionEvaluationContext::Verifier { frame, .. } => { - evals[idx] = self.0.evaluate(frame.get_evaluation_step(0)); - } - } - } - - fn evaluate_prover( - &self, - ctx: &TransitionEvaluationContext, - base_evals: &mut [FieldElement], - ext_evals: &mut [FieldElement], - ) { - let idx = self.0.constraint_idx(); - if idx < base_evals.len() { - // Base-field fast path: write FieldElement directly - if let TransitionEvaluationContext::Prover { frame, .. } = ctx { - base_evals[idx] = self.0.evaluate(frame.get_evaluation_step(0)); - } else { - unreachable!("evaluate_prover called with non-Prover context"); - } - } else { - // Fallback: AIR did not opt into base-field splitting, - // delegate to the verifier path which writes E evals. - self.evaluate_verifier(ctx, ext_evals); - } - } -} diff --git a/crypto/stark/src/constraints/zerofier.rs b/crypto/stark/src/constraints/zerofier.rs index debc0c389..f9ec83a6e 100644 --- a/crypto/stark/src/constraints/zerofier.rs +++ b/crypto/stark/src/constraints/zerofier.rs @@ -1,13 +1,11 @@ //! Zerofier evaluation as free functions of [`ConstraintMeta`]. //! -//! These are the bodies of the `TransitionConstraintEvaluator` default -//! methods (`crate::constraints::transition`), relocated verbatim to consume -//! plain constraint metadata instead of trait getters — they only ever read -//! `period` / `offset` / `exemptions_period` / `periodic_exemptions_offset` / -//! `end_exemptions`. The engine's zerofier machinery moves onto these once -//! tables convert to [`ConstraintSet`](crate::constraints::builder::ConstraintSet); -//! until then the trait defaults remain the production path (equivalence is -//! asserted by the tests below). +//! The production zerofier path: `AIR::transition_zerofier_evaluations_grouped` +//! (prover) and the verifier's OOD zerofier denominators both evaluate these +//! over each constraint's plain metadata (`period` / `offset` / +//! `exemptions_period` / `periodic_exemptions_offset` / `end_exemptions`). +//! The bodies were relocated verbatim from the deleted boxed-constraint trait's +//! default methods (equivalence was asserted by migration-time tests). use core::ops::Div; @@ -229,215 +227,3 @@ where .unwrap() * &end_exemptions_eval } - -// ============================================================================= -// Equivalence tests: free functions == the trait defaults they were moved from -// ============================================================================= - -#[cfg(test)] -mod tests { - use super::*; - use crate::constraints::transition::TransitionConstraintEvaluator; - use crate::traits::TransitionEvaluationContext; - use math::fft::roots_of_unity::get_powers_of_primitive_root_coset; - use math::field::element::FieldElement; - use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext; - use math::field::goldilocks::GoldilocksField as Fp; - - type FpE = FieldElement; - type ExtE = FieldElement; - - /// A boxed-path constraint whose zerofier shape is configurable — used to - /// run the ORIGINAL trait-default bodies for comparison. - struct DummyConstraint { - period: usize, - offset: usize, - exemptions_period: Option, - periodic_exemptions_offset: Option, - end_exemptions: usize, - } - - impl TransitionConstraintEvaluator for DummyConstraint { - fn degree(&self) -> usize { - 1 - } - fn constraint_idx(&self) -> usize { - 0 - } - fn evaluate_verifier( - &self, - _ctx: &TransitionEvaluationContext, - _evals: &mut [ExtE], - ) { - } - fn period(&self) -> usize { - self.period - } - fn offset(&self) -> usize { - self.offset - } - fn exemptions_period(&self) -> Option { - self.exemptions_period - } - fn periodic_exemptions_offset(&self) -> Option { - self.periodic_exemptions_offset - } - fn end_exemptions(&self) -> usize { - self.end_exemptions - } - } - - impl DummyConstraint { - fn meta(&self) -> ConstraintMeta { - let mut m = ConstraintMeta::base(0, 1) - .with_period(self.period) - .with_offset(self.offset) - .with_end_exemptions(self.end_exemptions); - if let Some(p) = self.exemptions_period { - m = m.with_exemptions(p, self.periodic_exemptions_offset.unwrap()); - } - m - } - } - - /// Hand-built prover Domain: trace length 16, blowup 4, coset offset 7. - fn sample_domain() -> Domain { - let trace_length: usize = 16; - let blowup_factor: usize = 4; - let coset_offset = FpE::from(7u64); - let root_order = trace_length.trailing_zeros(); - let trace_primitive_root = - ::get_primitive_root_of_unity(root_order as u64) - .unwrap(); - let trace_roots_of_unity = get_powers_of_primitive_root_coset( - root_order as u64, - trace_length, - &FieldElement::one(), - ) - .unwrap(); - let lde_root_order = (trace_length * blowup_factor).trailing_zeros(); - let lde_roots_of_unity_coset = get_powers_of_primitive_root_coset( - lde_root_order as u64, - trace_length * blowup_factor, - &coset_offset, - ) - .unwrap(); - Domain { - root_order, - lde_roots_of_unity_coset, - trace_primitive_root, - trace_roots_of_unity, - coset_offset, - blowup_factor, - interpolation_domain_size: trace_length, - } - } - - /// The zerofier configurations exercised: every branch of the moved - /// bodies (default; end exemptions; period/offset; periodic exemptions - /// with and without end exemptions). - fn sample_configs() -> Vec { - vec![ - // Every row, no exemptions (the common case). - DummyConstraint { - period: 1, - offset: 0, - exemptions_period: None, - periodic_exemptions_offset: None, - end_exemptions: 0, - }, - // End exemptions only. - DummyConstraint { - period: 1, - offset: 0, - exemptions_period: None, - periodic_exemptions_offset: None, - end_exemptions: 2, - }, - // Periodic constraint with offset. - DummyConstraint { - period: 2, - offset: 1, - exemptions_period: None, - periodic_exemptions_offset: None, - end_exemptions: 0, - }, - // Periodic constraint with offset and end exemptions. - DummyConstraint { - period: 4, - offset: 3, - exemptions_period: None, - periodic_exemptions_offset: None, - end_exemptions: 1, - }, - // Periodic exemptions (bit_flags-shaped), cyclic fast path. - DummyConstraint { - period: 1, - offset: 0, - exemptions_period: Some(4), - periodic_exemptions_offset: Some(3), - end_exemptions: 0, - }, - // Periodic exemptions + end exemptions. - DummyConstraint { - period: 1, - offset: 0, - exemptions_period: Some(4), - periodic_exemptions_offset: Some(1), - end_exemptions: 2, - }, - ] - } - - #[test] - fn lde_zerofier_evaluations_match_trait_defaults() { - let domain = sample_domain(); - for (i, c) in sample_configs().iter().enumerate() { - let expected = c.zerofier_evaluations_on_extended_domain(&domain); - let got = zerofier_evaluations_on_extended_domain(&c.meta(), &domain); - assert_eq!(got, expected, "config {i} mismatch"); - } - } - - #[test] - fn ood_zerofier_evaluations_match_trait_defaults() { - let domain = sample_domain(); - let trace_length = domain.trace_roots_of_unity.len(); - let root = &domain.trace_primitive_root; - // A few arbitrary OOD points with all three extension components set. - let zs = [ - ExtE::from_raw([FpE::from(123u64), FpE::from(456u64), FpE::from(789u64)]), - ExtE::from_raw([ - FpE::from(0xDEAD_BEEFu64), - FpE::from(0xCAFE_F00Du64), - FpE::from(0x1234_5678u64), - ]), - ]; - for (i, c) in sample_configs().iter().enumerate() { - for z in &zs { - let expected = c.evaluate_zerofier(z, root, trace_length); - let got = evaluate_zerofier(&c.meta(), z, root, trace_length); - assert_eq!(got, expected, "config {i} mismatch at z={z:?}"); - } - } - } - - #[test] - fn end_exemptions_helpers_match_trait_defaults() { - let domain = sample_domain(); - let trace_length = domain.trace_roots_of_unity.len(); - let root = &domain.trace_primitive_root; - for (i, c) in sample_configs().iter().enumerate() { - assert_eq!( - end_exemptions_roots(&c.meta(), root, trace_length), - c.end_exemptions_roots(root, trace_length), - "config {i} roots mismatch" - ); - assert_eq!( - end_exemptions_lde_evaluations(&c.meta(), &domain), - c.end_exemptions_lde_evaluations(&domain), - "config {i} lde evaluations mismatch" - ); - } - } -} diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 2929c2218..94b73a200 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -8,7 +8,6 @@ use crate::{ builder::{ ConstraintMeta, ConstraintSet, ProverEvalFolder, VerifierEvalFolder, num_base_from_meta, }, - transition::TransitionConstraintEvaluator, }, context::AirContext, proof::options::ProofOptions, @@ -1717,53 +1716,18 @@ where (bus_sums, sender_sums, receiver_sums) } -/// Computes multiplicity for an interaction from a `TableView`. -fn compute_multiplicity_from_step, B: IsField>( - step: &TableView, - multiplicity: &Multiplicity, -) -> FieldElement { - multiplicity.evaluate_with(|col| step.get_main_evaluation_element(0, col).clone()) -} - -/// Computes the fingerprint for an interaction from a `TableView`. -/// -/// Returns `z - (bus_id + α·v[0] + α²·v[1] + ...)` -fn compute_fingerprint_from_step, B: IsField>( - step: &TableView, - interaction: &BusInteraction, - z: &FieldElement, - alpha_powers: &[FieldElement], - shifts: &PackingShifts, -) -> FieldElement { - // α⁰ = 1: the bus-id term needs no multiply — embed it into B directly. - let mut linear_combination = FieldElement::::from(interaction.bus_id); - let mut alpha_idx = 1; - for bv in &interaction.values { - alpha_idx += bv.accumulate_fingerprint_from_step( - step, - alpha_powers, - alpha_idx, - &mut linear_combination, - shifts, - ); - } - z - &linear_combination -} - // ============================================================================= // LogUp single-source constraints (ConstraintBuilder front-end) // ============================================================================= // // The LogUp transition constraints are generated from the interaction config // (a [`LogUpLayout`]) through the generic [`ConstraintBuilder`], so ONE body -// serves the compiled prover folder, the verifier folder and IR capture. These -// are the single-source twins of the boxed `LookupBatchedTermConstraint` / -// `LookupAccumulatedConstraint` structs (which stay for now as the differential -// oracle; the engine switch deletes them). +// serves the compiled prover folder, the verifier folder and IR capture. This +// is the single source for the two LogUp constraint shapes (batched term and +// accumulated); there are no per-constraint objects. // // All LogUp constraints use the default zerofier shape (every row, no -// exemptions) — the structs override none of period/offset/exemptions — so -// [`logup_meta`] emits plain [`RootKind::Ext`] entries. +// exemptions), so [`logup_meta`] emits plain [`RootKind::Ext`] entries. // // Honesty note (matches the runtime body): `BusValue::Linear`'s data-dependent // "skip the multiply when the row value is zero" optimization is NOT reproduced @@ -1775,9 +1739,8 @@ use crate::constraints::builder::ConstraintBuilder; /// Config describing an [`AirWithBuses`] table's LogUp layout, exactly as /// computed by [`AirWithBuses::new`] from the interaction list (via -/// [`split_interactions`]). This is the plain-data replacement for the -/// per-constraint `LookupBatchedTermConstraint` / `LookupAccumulatedConstraint` -/// objects: [`emit_logup_constraints`] reads it to generate every LogUp +/// `split_interactions`). This is the plain-data source for the LogUp +/// constraints: [`emit_logup_constraints`] reads it to generate every LogUp /// constraint, and [`logup_meta`] reads it for the metadata. #[derive(Clone)] pub struct LogUpLayout { @@ -1832,7 +1795,7 @@ impl LogUpLayout { } /// Capture a [`Multiplicity`] as a base-field expression, mirroring -/// [`Multiplicity::evaluate_with`] (via [`compute_multiplicity_from_step`]). +/// [`Multiplicity::evaluate_with`]. fn emit_multiplicity(b: &B, multiplicity: &Multiplicity, offset: usize) -> B::Expr where F: IsField, @@ -2016,7 +1979,7 @@ where } /// Capture an interaction's fingerprint as an extension expression, mirroring -/// [`compute_fingerprint_from_step`]: `z - (bus_id + α·v[0] + α²·v[1] + ...)`. +/// `z - (bus_id + α·v[0] + α²·v[1] + ...)`. /// /// `α⁰ = 1`: the bus-id term needs no multiply and is added as a base constant. fn emit_fingerprint(b: &B, interaction: &BusInteraction, offset: usize) -> B::ExprE @@ -2052,8 +2015,7 @@ where z - lc } -/// Emit the batched-term constraint for committed pair `pair_idx`, mirroring -/// `LookupBatchedTermConstraint::capture`: +/// Emit the batched-term constraint for committed pair `pair_idx`: /// `c · fp_a · fp_b − sign_a·m_a·fp_b − sign_b·m_b·fp_a` (degree 3). fn emit_logup_batched_term(b: &mut B, layout: &LogUpLayout, pair_idx: usize, idx: usize) where @@ -2092,8 +2054,8 @@ where b.emit_ext(idx, main - term_a - term_b); } -/// Emit the accumulated constraint (with 1–2 absorbed interactions), mirroring -/// `LookupAccumulatedConstraint::capture`. `acc_curr` reads row 0; `acc_next`, +/// Emit the accumulated constraint (with 1–2 absorbed interactions). +/// `acc_curr` reads row 0; `acc_next`, /// the committed-term sum and the absorbed fingerprints/multiplicities all read /// the NEXT row (offset 1). /// @@ -2253,316 +2215,14 @@ where ext_evals } -/// Constraint for a batched pair of interactions sharing one aux column. -/// -/// Verifies: `c = m_a/fp_a + m_b/fp_b` where signs are baked into m_a, m_b. -/// -/// Clearing denominators: `c * fp_a * fp_b - sign_a * m_a * fp_b - sign_b * m_b * fp_a = 0` -/// -/// Degree 3: c (aux) × fp_a (linear in main) × fp_b (linear in main). -struct LookupBatchedTermConstraint { - interaction_a: BusInteraction, - interaction_b: BusInteraction, - term_column_idx: usize, - constraint_idx: usize, -} - -impl LookupBatchedTermConstraint { - pub fn new( - interaction_a: BusInteraction, - interaction_b: BusInteraction, - term_column_idx: usize, - constraint_idx: usize, - ) -> Self { - Self { - interaction_a, - interaction_b, - term_column_idx, - constraint_idx, - } - } -} - -impl TransitionConstraintEvaluator for LookupBatchedTermConstraint -where - F: IsFFTField + IsSubFieldOf + Send + Sync, - E: IsField + Send + Sync, -{ - fn degree(&self) -> usize { - 3 // c * fp_a * fp_b - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - fn evaluate_batched_term_constraint, B: IsField>( - step: &TableView, - term_column_idx: usize, - interaction_a: &BusInteraction, - interaction_b: &BusInteraction, - rap_challenges: &&[FieldElement], - alpha_powers: &[FieldElement], - shifts: &PackingShifts, - ) -> FieldElement { - let c = step.get_aux_evaluation_element(0, term_column_idx); - let z = &rap_challenges[0]; - - let m_a = compute_multiplicity_from_step(step, &interaction_a.multiplicity); - let m_b = compute_multiplicity_from_step(step, &interaction_b.multiplicity); - - let fp_a = compute_fingerprint_from_step(step, interaction_a, z, alpha_powers, shifts); - let fp_b = compute_fingerprint_from_step(step, interaction_b, z, alpha_powers, shifts); - - // c * fp_a * fp_b - sign_a * m_a * fp_b - sign_b * m_b * fp_a = 0 - // Use conditional negation instead of E×E sign multiplication - let term_a = m_a * &fp_b; - let term_a = if interaction_a.is_sender { - term_a - } else { - -term_a - }; - let term_b = m_b * &fp_a; - let term_b = if interaction_b.is_sender { - term_b - } else { - -term_b - }; - c * &fp_a * &fp_b - term_a - term_b - } - - let res = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - rap_challenges, - logup_alpha_powers, - packing_shifts, - .. - } => evaluate_batched_term_constraint( - frame.get_evaluation_step(0), - self.term_column_idx, - &self.interaction_a, - &self.interaction_b, - rap_challenges, - logup_alpha_powers, - packing_shifts, - ), - TransitionEvaluationContext::Verifier { - frame, - rap_challenges, - logup_alpha_powers, - packing_shifts, - .. - } => evaluate_batched_term_constraint( - frame.get_evaluation_step(0), - self.term_column_idx, - &self.interaction_a, - &self.interaction_b, - rap_challenges, - logup_alpha_powers, - packing_shifts, - ), - }; - - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx) { - *eval = res; - } - } -} - -/// Constraint for the accumulated column with absorbed interactions. -/// -/// The accumulated column tracks the running sum of all committed term columns -/// plus 1-2 "absorbed" interactions whose terms are verified inline (not committed). -/// -/// For 1 absorbed interaction: -/// `(acc_next - acc_curr - Σ terms + L/N) · f - sign · m = 0` (degree 2) -/// -/// For 2 absorbed interactions: -/// `(acc_next - acc_curr - Σ terms + L/N) · f₁·f₂ - sign₁·m₁·f₂ - sign₂·m₂·f₁ = 0` (degree 3) -struct LookupAccumulatedConstraint { - constraint_idx: usize, - /// Number of committed term columns (excludes absorbed interactions) - num_term_columns: usize, - /// Index of the accumulated column (= num_term_columns) - acc_column_idx: usize, - /// 1 or 2 interactions absorbed into this constraint (not committed as columns) - absorbed: Vec, -} - -impl LookupAccumulatedConstraint { - pub fn new( - constraint_idx: usize, - num_term_columns: usize, - absorbed: Vec, - ) -> Self { - Self { - constraint_idx, - num_term_columns, - acc_column_idx: num_term_columns, - absorbed, - } - } -} - -impl TransitionConstraintEvaluator for LookupAccumulatedConstraint -where - F: IsFFTField + IsSubFieldOf + Send + Sync, - E: IsField + Send + Sync, -{ - fn degree(&self) -> usize { - 1 + self.absorbed.len() // 2 for 1 absorbed, 3 for 2 absorbed - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate_verifier( - &self, - evaluation_context: &TransitionEvaluationContext, - transition_evaluations: &mut [FieldElement], - ) { - #[allow(clippy::too_many_arguments)] - fn evaluate_accumulated_constraint, B: IsField>( - first_step: &TableView, - second_step: &TableView, - acc_column_idx: usize, - num_term_columns: usize, - logup_table_offset: &FieldElement, - absorbed: &[BusInteraction], - rap_challenges: &&[FieldElement], - alpha_powers: &[FieldElement], - shifts: &PackingShifts, - ) -> FieldElement { - // Accumulated column values - let acc_curr = first_step.get_aux_evaluation_element(0, acc_column_idx); - let acc_next = second_step.get_aux_evaluation_element(0, acc_column_idx); - - // Sum of all committed term columns at the next step - let terms_sum: FieldElement = (0..num_term_columns) - .map(|i| second_step.get_aux_evaluation_element(0, i).clone()) - .sum(); - - // delta = acc_next - acc_curr - terms_sum + L/N - let delta = acc_next - acc_curr - terms_sum + logup_table_offset; - - let z = &rap_challenges[0]; - - // Clear denominators of absorbed interactions - debug_assert!(matches!(absorbed.len(), 1 | 2)); - // Use conditional negation instead of E×E sign multiplication where possible - match absorbed.len() { - 1 => { - // (delta) · f - sign · m = 0 - // sign multiply also promotes m from base field A to extension B - let m = compute_multiplicity_from_step(second_step, &absorbed[0].multiplicity); - let f = compute_fingerprint_from_step( - second_step, - &absorbed[0], - z, - alpha_powers, - shifts, - ); - let sign: FieldElement = if absorbed[0].is_sender { - FieldElement::one() - } else { - -FieldElement::one() - }; - delta * &f - m * sign - } - 2 => { - // (delta) · f₁ · f₂ - sign₁·m₁·f₂ - sign₂·m₂·f₁ = 0 - // m_i * f_j naturally promotes A→B, then conditionally negate - let m1 = compute_multiplicity_from_step(second_step, &absorbed[0].multiplicity); - let m2 = compute_multiplicity_from_step(second_step, &absorbed[1].multiplicity); - let f1 = compute_fingerprint_from_step( - second_step, - &absorbed[0], - z, - alpha_powers, - shifts, - ); - let f2 = compute_fingerprint_from_step( - second_step, - &absorbed[1], - z, - alpha_powers, - shifts, - ); - let term1 = m1 * &f2; - let term1 = if absorbed[0].is_sender { term1 } else { -term1 }; - let term2 = m2 * &f1; - let term2 = if absorbed[1].is_sender { term2 } else { -term2 }; - delta * &f1 * &f2 - term1 - term2 - } - _ => unreachable!("absorbed must contain 1 or 2 interactions"), - } - } - - let res = match evaluation_context { - TransitionEvaluationContext::Prover { - frame, - logup_table_offset, - rap_challenges, - logup_alpha_powers, - packing_shifts, - .. - } => evaluate_accumulated_constraint( - frame.get_evaluation_step(0), - frame.get_evaluation_step(1), - self.acc_column_idx, - self.num_term_columns, - logup_table_offset, - &self.absorbed, - rap_challenges, - logup_alpha_powers, - packing_shifts, - ), - TransitionEvaluationContext::Verifier { - frame, - logup_table_offset, - rap_challenges, - logup_alpha_powers, - packing_shifts, - .. - } => evaluate_accumulated_constraint( - frame.get_evaluation_step(0), - frame.get_evaluation_step(1), - self.acc_column_idx, - self.num_term_columns, - logup_table_offset, - &self.absorbed, - rap_challenges, - logup_alpha_powers, - packing_shifts, - ), - }; - - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx) { - *eval = res; - } - } -} - #[cfg(test)] mod logup_single_source_tests { - //! Differential tests for the single-source LogUp constraint bodies - //! ([`emit_logup_constraints`]) against the OLD boxed constraint structs - //! (`LookupBatchedTermConstraint` / `LookupAccumulatedConstraint`) that stay - //! in-branch as the transcription oracle until the final deletion phase. - //! - //! For every layout we compare, on 1000 random two-step frames (off-trace - //! points where a weakened or slipped transcription diverges with - //! overwhelming probability): - //! 1. [`ProverEvalFolder`] output == old `evaluate_prover` (ext slots); - //! 2. [`VerifierEvalFolder`] output == old `evaluate_verifier`; - //! 3. capture → flatten → interpret == old `evaluate_verifier`. + //! Regression tests for the single-source LogUp constraint bodies + //! ([`emit_logup_constraints`]) run three ways from ONE definition. For + //! every layout we assert, on 1000 + //! random two-step frames: [`ProverEvalFolder`] == capture→`eval_program` + //! (prover) and [`VerifierEvalFolder`] == capture→`eval_program_verifier` + //! (verifier) — all bit-for-bit. //! //! Coverage: the accumulated constraint's 1-absorbed AND 2-absorbed branches //! (the latter reads `aux(1, ·)` next-row cells), the batched-term @@ -2598,34 +2258,6 @@ mod logup_single_source_tests { } } - /// Build the OLD boxed constraints for a layout, index-for-index with - /// [`emit_logup_constraints`]: committed batched terms first (idx `idx_base` - /// onward), then the accumulated constraint. - fn old_boxed( - layout: &LogUpLayout, - idx_base: usize, - ) -> Vec>> { - let mut out: Vec>> = Vec::new(); - let mut idx = idx_base; - for pair_idx in 0..layout.num_committed_pairs { - out.push(Box::new(LookupBatchedTermConstraint::new( - layout.interactions[pair_idx * 2].clone(), - layout.interactions[pair_idx * 2 + 1].clone(), - pair_idx, - idx, - ))); - idx += 1; - } - if !layout.interactions.is_empty() { - out.push(Box::new(LookupAccumulatedConstraint::new( - idx, - layout.num_term_columns, - layout.absorbed().to_vec(), - ))); - } - out - } - /// Number of aux columns the layout uses: committed term columns + the /// accumulated column. fn num_aux_cols(layout: &LogUpLayout) -> usize { @@ -2644,31 +2276,32 @@ mod logup_single_source_tests { ]) } - /// The full three-way differential check for one layout, on `TRIALS` random - /// two-step frames. + /// The permanent regression check for one layout, on `TRIALS` random + /// two-step frames: the LogUp body run three ways from ONE definition must + /// agree bit-for-bit — [`ProverEvalFolder`] == capture→[`eval_program`] + /// (prover) and [`VerifierEvalFolder`] == capture→[`eval_program_verifier`] + /// (verifier). (The old boxed-constraint oracle these were originally + /// differentiated against is gone; the folder-vs-interpreter equality it + /// established stays as the standing invariant.) fn check_layout(label: &str, layout: &LogUpLayout, num_main_cols: usize) { let n_base = 0usize; // LogUp constraints are all extension-rooted. - let old = old_boxed(layout, n_base); - let n = old.len(); - assert_eq!(n, layout.num_constraints(), "[{label}] constraint count"); + let n = layout.num_constraints(); - // Meta parity vs the old boxed objects. + // Metadata self-consistency: all-ext, dense, and the batched/accumulated + // degree formula (3 per batched term; 1 + absorbed for the accumulator). let meta = logup_meta(layout, n_base); assert_eq!(meta.len(), n, "[{label}] meta count"); let num_base = num_base_from_meta(&meta); assert_eq!(num_base, 0, "[{label}] LogUp meta is all-ext"); for (i, m) in meta.iter().enumerate() { - let c = old.iter().find(|c| c.constraint_idx() == i).expect("dense"); assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); assert_eq!(m.kind, RootKind::Ext, "[{label}] meta kind {i}"); - assert_eq!(m.degree, c.degree(), "[{label}] degree {i}"); - assert_eq!(m.period, c.period(), "[{label}] period {i}"); - assert_eq!(m.offset, c.offset(), "[{label}] offset {i}"); - assert_eq!( - m.end_exemptions, - c.end_exemptions(), - "[{label}] end_exempt {i}" - ); + let expected_degree = if i < layout.num_committed_pairs { + 3 + } else { + 1 + layout.absorbed().len() + }; + assert_eq!(m.degree, expected_degree, "[{label}] degree {i}"); } // Capture once; tree-measured degree <= declared. @@ -2716,34 +2349,20 @@ mod logup_single_source_tests { &shifts, ); - // --- old prover-side reference: evaluate_prover into ext slots --- - let mut old_base = vec![Fp::zero(); n_base]; - let mut old_ext = vec![Fp3::zero(); n]; - for c in old.iter() { - c.evaluate_prover(&prover_ctx, &mut old_base, &mut old_ext); - } - - // --- 1. ProverEvalFolder == old evaluate_prover --- + // --- ProverEvalFolder == capture → interpret (prover) --- let mut base_out = vec![Fp::zero(); n_base]; let mut ext_out = vec![Fp3::zero(); n]; let mut folder = ProverEvalFolder::new(&prover_ctx, &mut base_out, &mut ext_out); emit_logup_constraints(&mut folder, layout, n_base); folder.assert_all_emitted(); - for i in 0..n { - assert_eq!( - ext_out[i], old_ext[i], - "[{label}] prover folder mismatch, constraint {i}, trial {trial}" - ); - } - // --- 3. capture → interpret == old evaluate_prover --- let mut ir_base = vec![Fp::zero(); n_base]; let mut ir_ext = vec![Fp3::zero(); n]; eval_program(&prog, &prover_ctx, &mut ir_base, &mut ir_ext); for i in 0..n { assert_eq!( - ir_ext[i], old_ext[i], - "[{label}] interpreter mismatch, constraint {i}, trial {trial}" + ext_out[i], ir_ext[i], + "[{label}] prover folder vs interpreter mismatch, constraint {i}, trial {trial}" ); } @@ -2769,30 +2388,28 @@ mod logup_single_source_tests { &table_offset, &vshifts, ); - let mut old_vext = vec![Fp3::zero(); n]; - for c in old.iter() { - c.evaluate_verifier(&vctx, &mut old_vext); - } - // --- 2. VerifierEvalFolder == old evaluate_verifier --- + // --- VerifierEvalFolder == capture → interpret (verifier) --- let mut vext_out = vec![Fp3::zero(); n]; let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); emit_logup_constraints(&mut vfolder, layout, n_base); vfolder.assert_all_emitted(); + + let mut ir_vext = vec![Fp3::zero(); n]; + eval_program_verifier(&prog, &vctx, &mut ir_vext); for i in 0..n { assert_eq!( - vext_out[i], old_vext[i], - "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" + vext_out[i], ir_vext[i], + "[{label}] verifier folder vs interpreter mismatch, constraint {i}, trial {trial}" ); } - // capture → interpret (verifier) == old evaluate_verifier --- - let mut ir_vext = vec![Fp3::zero(); n]; - eval_program_verifier(&prog, &vctx, &mut ir_vext); + // Prover base-promotion and verifier evaluations must agree + // (the prover frame embedded == the verifier frame). for i in 0..n { assert_eq!( - ir_vext[i], old_vext[i], - "[{label}] verifier interpreter mismatch, constraint {i}, trial {trial}" + ext_out[i], vext_out[i], + "[{label}] prover vs verifier folder mismatch, constraint {i}, trial {trial}" ); } } @@ -2816,7 +2433,7 @@ mod logup_single_source_tests { } #[test] - fn logup_one_absorbed_matches_old() { + fn logup_one_absorbed() { // 3 interactions → split(3) = (1 committed pair, 1 absorbed): // idx 0: batched term (interactions 0,1) // idx 1: accumulated, 1 absorbed (interaction 2), degree 2. @@ -2828,7 +2445,7 @@ mod logup_single_source_tests { } #[test] - fn logup_two_absorbed_matches_old() { + fn logup_two_absorbed() { // 4 interactions → split(4) = (1 committed pair, 2 absorbed): // idx 0: batched term (interactions 0,1) // idx 1: accumulated, 2 absorbed (interactions 2,3), degree 3. @@ -2856,7 +2473,7 @@ mod logup_single_source_tests { } #[test] - fn logup_matches_old_for_all_packing_variants() { + fn logup_all_packing_variants() { // Drive every Packing arm through the fingerprint of a committed pair // and an absorbed interaction. DWordBL/QuadHL are the widest (8 cols); // give a generous column budget. diff --git a/crypto/stark/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index 7a3884832..8184e05d3 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -14,4 +14,3 @@ pub mod small_trace_tests; #[cfg(feature = "disk-spill")] pub mod table_disk_spill_tests; pub mod trace_test_helpers; -pub mod transition_tests; diff --git a/crypto/stark/src/tests/transition_tests.rs b/crypto/stark/src/tests/transition_tests.rs deleted file mode 100644 index 81b207d3d..000000000 --- a/crypto/stark/src/tests/transition_tests.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::constraints::builder::ConstraintMeta; -use crate::constraints::transition::TransitionConstraintEvaluator; -use crate::constraints::zerofier; -use crate::traits::TransitionEvaluationContext; -use math::field::element::FieldElement; -use math::field::goldilocks::GoldilocksField; -use math::field::traits::IsFFTField; -use std::marker::PhantomData; - -/// Dummy evaluator that only exposes the trait knobs we need (`period`, `offset`, -/// `end_exemptions`) to exercise the OLD trait-default `end_exemptions_roots`. -/// -/// Kept solely for the period ≠ 1 case below; it dies with the trait machinery -/// in the final deletion phase. -struct DummyConstraint { - period: usize, - offset: usize, - end_exemptions: usize, - phantom: PhantomData, -} - -impl TransitionConstraintEvaluator for DummyConstraint { - fn degree(&self) -> usize { - 1 - } - fn constraint_idx(&self) -> usize { - 0 - } - fn period(&self) -> usize { - self.period - } - fn offset(&self) -> usize { - self.offset - } - fn end_exemptions(&self) -> usize { - self.end_exemptions - } - fn evaluate_verifier(&self, _: &TransitionEvaluationContext, _: &mut [FieldElement]) {} -} - -#[test] -fn end_exemptions_roots_default_offset_matches_last_rows() { - let trace_length = 8usize; - let g = - GoldilocksField::get_primitive_root_of_unity(trace_length.trailing_zeros() as u64).unwrap(); - let meta = ConstraintMeta::base(0, 1).with_end_exemptions(2); - - let roots = zerofier::end_exemptions_roots(&meta, &g, trace_length); - - // Constraint applies on rows 0..8; last two rows are 6 and 7. - assert_eq!(roots, vec![g.pow(7u64), g.pow(6u64)]); -} - -// NOTE(single-source constraints): this case exists PURELY to exercise the -// period ≠ 1 zerofier shape, which no production constraint uses. It stays on -// the OLD trait path untouched and dies together with the period machinery in -// the final deletion phase. -#[test] -fn end_exemptions_roots_nonzero_offset_walks_the_offset_domain() { - let trace_length = 8usize; - let g = - GoldilocksField::get_primitive_root_of_unity(trace_length.trailing_zeros() as u64).unwrap(); - let c = DummyConstraint:: { - period: 2, - offset: 1, - end_exemptions: 2, - phantom: PhantomData, - }; - - let roots = c.end_exemptions_roots(&g, trace_length); - - // Constraint applies on rows {1, 3, 5, 7}; last two are 5 and 7. - assert_eq!(roots, vec![g.pow(7u64), g.pow(5u64)]); -} - -#[test] -fn end_exemptions_roots_zero_exemptions_is_empty() { - let trace_length = 8usize; - let g = - GoldilocksField::get_primitive_root_of_unity(trace_length.trailing_zeros() as u64).unwrap(); - let meta = ConstraintMeta::base(0, 1); - - assert!(zerofier::end_exemptions_roots::(&meta, &g, trace_length).is_empty()); -} diff --git a/prover/src/constraints/cpu.rs b/prover/src/constraints/cpu.rs index 210cedaec..3340c28ef 100644 --- a/prover/src/constraints/cpu.rs +++ b/prover/src/constraints/cpu.rs @@ -15,15 +15,10 @@ //! `JALR` is the `mem_flags` byte read directly: under `BRANCH` only the JALR bit //! of `mem_flags` can be set, so `mem_flags ∈ {0,1} = JALR` there. -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; -use stark::table::TableView; - use crate::tables::cpu::cols; use crate::tables::types::{GoldilocksExtension, GoldilocksField, SHIFT_16}; -use super::templates::{AddConstraint, AddOperand, IsBitConstraint}; +use super::templates::AddOperand; // ========================================================================= // Range: IS_BIT flag columns @@ -45,555 +40,6 @@ pub const BIT_FLAG_COLUMNS: &[usize] = &[ cols::PREV_PC_TIMESTAMP_BORROW, ]; -/// Creates all IS_BIT constraints for CPU flag columns. -pub fn create_is_bit_constraints(constraint_idx_start: usize) -> (Vec, usize) { - super::templates::new_is_bit_constraints(BIT_FLAG_COLUMNS, constraint_idx_start) -} - -// ========================================================================= -// Generic helpers -// ========================================================================= - -/// `cast(res, DWordWL)` low/high words from the four `res` halves (DWordHL). -#[inline] -fn res_word(step: &TableView, high: bool) -> FieldElement -where - F: IsSubFieldOf, - E: IsField, -{ - let (lo_col, hi_col) = if high { - (cols::RES_2, cols::RES_3) - } else { - (cols::RES_0, cols::RES_1) - }; - let shift_16: FieldElement = FieldElement::from(SHIFT_16); - step.get_main_evaluation_element(0, lo_col) - + step.get_main_evaluation_element(0, hi_col) * shift_16 -} - -// ========================================================================= -// decode group: word_instr mutex -// ========================================================================= - -/// Constraint `col_a · col_b = 0`. Used for the decode mutexes -/// `word_instr · {MEMORY, BRANCH, ECALL} = 0`. -pub struct ProductZeroConstraint { - col_a: usize, - col_b: usize, - constraint_idx: usize, -} - -impl ProductZeroConstraint { - pub fn new(col_a: usize, col_b: usize, constraint_idx: usize) -> Self { - Self { - col_a, - col_b, - constraint_idx, - } - } -} - -impl TransitionConstraint for ProductZeroConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - step.get_main_evaluation_element(0, self.col_a) - * step.get_main_evaluation_element(0, self.col_b) - } -} - -/// `(1 - MEMORY - BRANCH) · read_register2 · imm[i] = 0`: when neither MEMORY nor -/// BRANCH is set, the `arg2` multiplex needs at most one of `rv2`/`imm` nonzero. -/// Decoding already guarantees this; a spec defense-in-depth assumption. -pub struct Arg2ExclusiveConstraint { - imm_col: usize, - constraint_idx: usize, -} - -impl Arg2ExclusiveConstraint { - pub fn new(imm_col: usize, constraint_idx: usize) -> Self { - Self { - imm_col, - constraint_idx, - } - } -} - -impl TransitionConstraint for Arg2ExclusiveConstraint { - fn degree(&self) -> usize { - 3 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let memory = step.get_main_evaluation_element(0, cols::MEMORY).clone(); - let branch = step.get_main_evaluation_element(0, cols::BRANCH).clone(); - let rr2 = step.get_main_evaluation_element(0, cols::READ_REGISTER2); - let imm = step.get_main_evaluation_element(0, self.imm_col); - (one - memory - branch) * rr2 * imm - } -} - -/// `IS_BIT` on non-MEMORY rows: `(1 - MEMORY) · mem_flags · (1 - mem_flags) = 0`. -/// On non-memory rows `mem_flags` carries only the JALR bit, so it must be 0/1. -/// A spec defense-in-depth assumption (the DECODE lookup already enforces it). -pub struct MemFlagsBitConstraint { - constraint_idx: usize, -} - -impl MemFlagsBitConstraint { - pub fn new(constraint_idx: usize) -> Self { - Self { constraint_idx } - } -} - -impl TransitionConstraint for MemFlagsBitConstraint { - fn degree(&self) -> usize { - 3 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let memory = step.get_main_evaluation_element(0, cols::MEMORY).clone(); - let mem_flags = step.get_main_evaluation_element(0, cols::MEM_FLAGS).clone(); - (one.clone() - memory) * &mem_flags * (one - &mem_flags) - } -} - -// ========================================================================= -// mem group: register zero-forcing -// ========================================================================= - -/// Constraint `(1 − flag) · value = 0`: when `flag = 0`, `value` must be 0. -/// Used for `¬read_registerN ⇒ rvN[i] = 0`. -pub struct RegNotReadIsZeroConstraint { - flag_col: usize, - value_col: usize, - constraint_idx: usize, -} - -impl RegNotReadIsZeroConstraint { - pub fn new(flag_col: usize, value_col: usize, constraint_idx: usize) -> Self { - Self { - flag_col, - value_col, - constraint_idx, - } - } -} - -impl TransitionConstraint for RegNotReadIsZeroConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let flag = step.get_main_evaluation_element(0, self.flag_col).clone(); - let value = step.get_main_evaluation_element(0, self.value_col); - (one - flag) * value - } -} - -// ========================================================================= -// alu group: arg2 multiplex -// ========================================================================= - -/// `arg2` multiplex (`cpu.toml` CPU-A1), for word index -/// `word_idx ∈ {0,1}`: -/// -/// ```text -/// arg2[i] = MEMORY·imm[i] -/// + BRANCH·rv2[i] -/// + (1−MEMORY−BRANCH)·(rv2[i] + imm[i]) -/// ``` -/// -/// For BRANCH rows `arg2 = rv2` (JAL/JALR read no rs2, so `rv2 = 0`; conditional -/// branches feed `rv2` to the EQ/LT comparison). The final `rv2 + imm` term has -/// no inter-word carry because decode assumption A2 guarantees at most one of -/// `rv2`/`imm` is nonzero when `MEMORY+BRANCH = 0`. `MEMORY` and `BRANCH` are -/// mutually exclusive (enforced by the live `MEMORY·BRANCH = 0` constraint), so -/// `1−MEMORY−BRANCH ∈ {0,1}` and matches the degree-2 spec form. -pub struct Arg2Constraint { - /// 0 = low word, 1 = high word. - word_idx: usize, - constraint_idx: usize, -} - -impl Arg2Constraint { - pub fn new(word_idx: usize, constraint_idx: usize) -> Self { - Self { - word_idx, - constraint_idx, - } - } -} - -impl TransitionConstraint for Arg2Constraint { - fn degree(&self) -> usize { - // (1 - MEMORY - BRANCH) [deg 1] · (rv2 + imm) [deg 1] = 2. The degree-2 - // form relies on the live MEMORY·BRANCH = 0 mutex. - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let (arg2_col, imm_col, rv2_col) = if self.word_idx == 0 { - (cols::ARG2_0, cols::IMM_0, cols::RV2_0) - } else { - (cols::ARG2_1, cols::IMM_1, cols::RV2_1) - }; - - let one = FieldElement::::one(); - let arg2 = step.get_main_evaluation_element(0, arg2_col).clone(); - let imm = step.get_main_evaluation_element(0, imm_col).clone(); - let rv2 = step.get_main_evaluation_element(0, rv2_col).clone(); - let memory = step.get_main_evaluation_element(0, cols::MEMORY).clone(); - let branch = step.get_main_evaluation_element(0, cols::BRANCH).clone(); - - // MEMORY · imm - let mut expected = &memory * &imm; - // BRANCH · rv2 - expected += &branch * &rv2; - // (1 - MEMORY - BRANCH) · (rv2 + imm) - expected += (&one - &memory - &branch) * (&rv2 + &imm); - - arg2 - expected - } -} - -// ========================================================================= -// mem group: ¬MEMORY ∧ ¬JALR ⇒ rvd = cast(res, WL) -// ========================================================================= - -/// `(1 − MEMORY − BRANCH) · (rvd[i] − cast(res, WL)[i]) = 0` (`cpu.toml` CPU-M*). -/// -/// On plain ALU rows `rvd = res`. BRANCH rows are exempt: their `rvd` is the -/// return address `pc + instruction_length`, pinned by [`BranchRvdConstraint`]. -/// `MEMORY` and `BRANCH` are mutually exclusive (decode assumption), so -/// `1 − MEMORY − BRANCH ∈ {0,1}`. For LOAD/STORE `rvd` comes from the MEMORY bus. -pub struct RvdEqResConstraint { - /// 0 = low word, 1 = high word. - word_idx: usize, - constraint_idx: usize, -} - -impl RvdEqResConstraint { - pub fn new(word_idx: usize, constraint_idx: usize) -> Self { - Self { - word_idx, - constraint_idx, - } - } -} - -impl TransitionConstraint for RvdEqResConstraint { - fn degree(&self) -> usize { - // (1 - MEMORY - BRANCH) [deg 1] · (rvd - cast(res, WL)) [deg 1] = 2. - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let high = self.word_idx == 1; - let rvd_col = if high { cols::RVD_1 } else { cols::RVD_0 }; - let one = FieldElement::::one(); - let memory = step.get_main_evaluation_element(0, cols::MEMORY).clone(); - let branch = step.get_main_evaluation_element(0, cols::BRANCH).clone(); - let rvd = step.get_main_evaluation_element(0, rvd_col).clone(); - let res_w = res_word(step, high); - (&one - &memory - &branch) * (rvd - res_w) - } -} - -// ========================================================================= -// branch group: BRANCH ⇒ rvd = pc + instruction_length -// ========================================================================= - -/// `BRANCH · carry · (1 − carry) = 0` for the 64-bit addition -/// `rvd = pc + instruction_length` (the JAL/JALR return address), in two -/// instances (`carry_0` / `carry_1`). Mirrors [`NextPcAddConstraint`] so the -/// low→high carry is propagated: the spec computes `rvd` with the same -/// carry-correct `ADD` template as `next_pc` (`cpu.toml` branch group), so the -/// high word must include the carry out of `pc[0] + instruction_length`. -/// -/// On every BRANCH row `rvd` holds the return address `pc + instruction_length` -/// (written to `rd` only by JAL/JALR; conditional branches compute it but never -/// write it). See [`RvdEqResConstraint`] for the complementary -/// `¬MEMORY ∧ ¬BRANCH ⇒ rvd = res` case. -pub struct BranchRvdConstraint { - /// 0 = low-word carry, 1 = high-word carry. - carry_idx: usize, - constraint_idx: usize, -} - -impl BranchRvdConstraint { - pub fn new(carry_idx: usize, constraint_idx: usize) -> Self { - assert!(carry_idx <= 1); - Self { - carry_idx, - constraint_idx, - } - } - - pub fn new_pair(constraint_idx_start: usize) -> (Self, Self) { - ( - Self::new(0, constraint_idx_start), - Self::new(1, constraint_idx_start + 1), - ) - } - - fn compute_carry_0(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let pc_lo = step.get_main_evaluation_element(0, cols::PC_0).clone(); - let rvd_lo = step.get_main_evaluation_element(0, cols::RVD_0).clone(); - let half_len = step - .get_main_evaluation_element(0, cols::HALF_INSTRUCTION_LENGTH) - .clone(); - let instr_len = &half_len + &half_len; // real byte length = 2 * half - let inv_2_32 = FieldElement::::from(super::templates::INV_SHIFT_32); - (pc_lo + instr_len - rvd_lo) * inv_2_32 - } - - fn compute_carry_1(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let pc_hi = step.get_main_evaluation_element(0, cols::PC_1).clone(); - let rvd_hi = step.get_main_evaluation_element(0, cols::RVD_1).clone(); - let carry_0 = self.compute_carry_0(step); - let inv_2_32 = FieldElement::::from(super::templates::INV_SHIFT_32); - (pc_hi + carry_0 - rvd_hi) * inv_2_32 - } -} - -impl TransitionConstraint for BranchRvdConstraint { - fn degree(&self) -> usize { - // BRANCH (deg 1) · carry · (1 − carry) = 3. - 3 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let branch = step.get_main_evaluation_element(0, cols::BRANCH).clone(); - let carry = match self.carry_idx { - 0 => self.compute_carry_0(step), - 1 => self.compute_carry_1(step), - _ => unreachable!("carry_idx validated <= 1 at construction"), - }; - branch * &carry * (&one - &carry) - } -} - -// ========================================================================= -// branch group: branch_cond -// ========================================================================= - -/// `branch_cond = BRANCH·JALR + BRANCH·(1−JALR)·res[0]` (`cpu.toml` CPU-B1). -/// `JALR = mem_flags` (bit, under BRANCH); `res[0]` is the low half of `res`. -pub struct BranchCondConstraint { - constraint_idx: usize, -} - -impl BranchCondConstraint { - pub fn new(constraint_idx: usize) -> Self { - Self { constraint_idx } - } -} - -impl TransitionConstraint for BranchCondConstraint { - fn degree(&self) -> usize { - 3 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let branch = step.get_main_evaluation_element(0, cols::BRANCH).clone(); - let jalr = step.get_main_evaluation_element(0, cols::MEM_FLAGS).clone(); - let res0 = step.get_main_evaluation_element(0, cols::RES_0).clone(); - let branch_cond = step - .get_main_evaluation_element(0, cols::BRANCH_COND) - .clone(); - - let expected = &branch * &jalr + &branch * (&one - &jalr) * res0; - branch_cond - expected - } -} - -// ========================================================================= -// branch group: next_pc = pc + instruction_length (when not branching) -// ========================================================================= - -/// `(1 − branch_cond) · carry · (1 − carry) = 0` for the 64-bit addition -/// `next_pc = pc + instruction_length`. Two instances (carry_0/carry_1). -pub struct NextPcAddConstraint { - carry_idx: usize, - constraint_idx: usize, -} - -impl NextPcAddConstraint { - pub fn new(carry_idx: usize, constraint_idx: usize) -> Self { - assert!(carry_idx <= 1); - Self { - carry_idx, - constraint_idx, - } - } - - pub fn new_pair(constraint_idx_start: usize) -> (Self, Self) { - ( - Self::new(0, constraint_idx_start), - Self::new(1, constraint_idx_start + 1), - ) - } - - fn compute_carry_0(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let pc_lo = step.get_main_evaluation_element(0, cols::PC_0).clone(); - let next_pc_lo = step.get_main_evaluation_element(0, cols::NEXT_PC_0).clone(); - let half_len = step - .get_main_evaluation_element(0, cols::HALF_INSTRUCTION_LENGTH) - .clone(); - let instr_len = &half_len + &half_len; // real byte length = 2 * half - let inv_2_32 = FieldElement::::from(super::templates::INV_SHIFT_32); - (pc_lo + instr_len - next_pc_lo) * inv_2_32 - } - - fn compute_carry_1(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let pc_hi = step.get_main_evaluation_element(0, cols::PC_1).clone(); - let next_pc_hi = step.get_main_evaluation_element(0, cols::NEXT_PC_1).clone(); - let carry_0 = self.compute_carry_0(step); - let inv_2_32 = FieldElement::::from(super::templates::INV_SHIFT_32); - (pc_hi + carry_0 - next_pc_hi) * inv_2_32 - } -} - -impl TransitionConstraint for NextPcAddConstraint { - fn degree(&self) -> usize { - 3 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let branch_cond = step - .get_main_evaluation_element(0, cols::BRANCH_COND) - .clone(); - let one = FieldElement::::one(); - let not_branch = &one - branch_cond; - let carry = match self.carry_idx { - 0 => self.compute_carry_0(step), - 1 => self.compute_carry_1(step), - _ => unreachable!("carry_idx validated <= 1 at construction"), - }; - not_branch * &carry * (one - carry) - } -} - -// ========================================================================= -// alu group: ADD / SUB fast-path templates -// ========================================================================= - -/// ADD fast-path: `cond = ADD`, `rv1 + arg2 = cast(res, WL)`. Covers ADD, LOAD, -/// STORE and JAL(R) (all set `ADD`). -pub fn create_add_constraints(constraint_idx_start: usize) -> (Vec, usize) { - let lhs = AddOperand::dword(cols::RV1_0); - let rhs = AddOperand::dword(cols::ARG2_0); - let sum = AddOperand::from_dword_hl(cols::RES_0); - let (c0, c1) = AddConstraint::new_pair(vec![cols::ADD], lhs, rhs, sum, constraint_idx_start); - (vec![c0, c1], constraint_idx_start + 2) -} - -/// SUB fast-path: `cond = SUB`, `res = rv1 − arg2`, verified as `arg2 + res = rv1`. -pub fn create_sub_constraints(constraint_idx_start: usize) -> (Vec, usize) { - let lhs = AddOperand::dword(cols::ARG2_0); - let rhs = AddOperand::from_dword_hl(cols::RES_0); - let sum = AddOperand::dword(cols::RV1_0); - let (c0, c1) = AddConstraint::new_pair(vec![cols::SUB], lhs, rhs, sum, constraint_idx_start); - (vec![c0, c1], constraint_idx_start + 2) -} - // ========================================================================= // Assembly // ========================================================================= @@ -612,123 +58,19 @@ pub fn create_sub_constraints(constraint_idx_start: usize) -> (Vec ( - Vec, - Vec, - Vec>>, - usize, -) { - let mut next_idx = 0; - - // range: IS_BIT - let (is_bit, next) = create_is_bit_constraints(next_idx); - next_idx = next; - - // alu: ADD + SUB fast-paths - let (mut add_constraints, next) = create_add_constraints(next_idx); - next_idx = next; - let (sub, next) = create_sub_constraints(next_idx); - next_idx = next; - add_constraints.extend(sub); - - let mut other: Vec< - Box>, - > = Vec::new(); - - // decode: word_instr mutex with MEMORY / BRANCH / ECALL, plus word_instr ⇒ - // {write,read1,read2}_register = 0 (word instructions are delegated to CPU32 - // and must not touch the main register file — leaving these free is unsound). - // The register-read gates are spec-mandated ("out of caution"). - for &col in &[ - cols::MEMORY, - cols::BRANCH, - cols::ECALL, - cols::WRITE_REGISTER, - cols::READ_REGISTER1, - cols::READ_REGISTER2, - ] { - other.push(ProductZeroConstraint::new(cols::WORD_INSTR, col, next_idx).boxed()); - next_idx += 1; - } - - // alu: arg2 multiplex (low, high words) - other.push(Arg2Constraint::new(0, next_idx).boxed()); - next_idx += 1; - other.push(Arg2Constraint::new(1, next_idx).boxed()); - next_idx += 1; - - // mem: register zero-forcing (rv1/rv2 are DWordWL → 2 words each) - for &value_col in &[cols::RV1_0, cols::RV1_1] { - other.push( - RegNotReadIsZeroConstraint::new(cols::READ_REGISTER1, value_col, next_idx).boxed(), - ); - next_idx += 1; - } - for &value_col in &[cols::RV2_0, cols::RV2_1] { - other.push( - RegNotReadIsZeroConstraint::new(cols::READ_REGISTER2, value_col, next_idx).boxed(), - ); - next_idx += 1; - } - - // mem: ¬MEMORY ∧ ¬BRANCH ⇒ rvd = cast(res, WL) - other.push(RvdEqResConstraint::new(0, next_idx).boxed()); - next_idx += 1; - other.push(RvdEqResConstraint::new(1, next_idx).boxed()); - next_idx += 1; - - // branch: BRANCH ⇒ rvd = pc + instruction_length (JAL/JALR return), carry-aware - let (branch_rvd_0, branch_rvd_1) = BranchRvdConstraint::new_pair(next_idx); - other.push(branch_rvd_0.boxed()); - other.push(branch_rvd_1.boxed()); - next_idx += 2; - - // branch: branch_cond + next_pc - other.push(BranchCondConstraint::new(next_idx).boxed()); - next_idx += 1; - let (next_pc_0, next_pc_1) = NextPcAddConstraint::new_pair(next_idx); - other.push(next_pc_0.boxed()); - other.push(next_pc_1.boxed()); - next_idx += 2; - - // assumptions (spec defense-in-depth, redundant with the DECODE lookup): - // MEMORY/BRANCH mutex, arg2 multiplex exclusivity, and IS_BIT on - // non-memory rows. - other.push(ProductZeroConstraint::new(cols::MEMORY, cols::BRANCH, next_idx).boxed()); - next_idx += 1; - for &imm_col in &[cols::IMM_0, cols::IMM_1] { - other.push(Arg2ExclusiveConstraint::new(imm_col, next_idx).boxed()); - next_idx += 1; - } - other.push(MemFlagsBitConstraint::new(next_idx).boxed()); - next_idx += 1; - - (is_bit, add_constraints, other, next_idx) -} - // ========================================================================= // Single-body emit functions (ConstraintBuilder front-end) // ========================================================================= // -// Non-destructive twins of the boxed constraint structs above, written once -// against the generic `ConstraintBuilder` so one body serves the compiled -// prover folder, the verifier folder and IR capture. The structs stay for -// now (they are the differential oracle for the emit functions); the table -// conversion deletes them. -// -// All constraints here use the default zerofier shape (every row, no -// exemptions), matching the structs (none override period/offset/ -// exemptions). +// One body per constraint against the generic `ConstraintBuilder` serves the +// compiled prover folder, the verifier folder and IR capture. All constraints +// here use the default zerofier shape (every row, no exemptions). use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use super::templates::{INV_SHIFT_32, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta}; -/// `col_a · col_b = 0`. Twin of [`ProductZeroConstraint`]. +/// `col_a · col_b = 0`. Twin of `ProductZeroConstraint`. pub fn emit_product_zero>( b: &mut B, idx: usize, @@ -745,7 +87,7 @@ pub fn product_zero_meta(idx: usize) -> ConstraintMeta { } /// `(1 − MEMORY − BRANCH) · read_register2 · imm[i] = 0`. Twin of -/// [`Arg2ExclusiveConstraint`]. +/// `Arg2ExclusiveConstraint`. pub fn emit_arg2_exclusive>( b: &mut B, idx: usize, @@ -765,7 +107,7 @@ pub fn arg2_exclusive_meta(idx: usize) -> ConstraintMeta { } /// `(1 − MEMORY) · mem_flags · (1 − mem_flags) = 0`. Twin of -/// [`MemFlagsBitConstraint`]. +/// `MemFlagsBitConstraint`. pub fn emit_mem_flags_bit>( b: &mut B, idx: usize, @@ -784,7 +126,7 @@ pub fn mem_flags_bit_meta(idx: usize) -> ConstraintMeta { ConstraintMeta::base(idx, 3) } -/// `(1 − flag) · value = 0`. Twin of [`RegNotReadIsZeroConstraint`]. +/// `(1 − flag) · value = 0`. Twin of `RegNotReadIsZeroConstraint`. pub fn emit_reg_not_read_is_zero>( b: &mut B, idx: usize, @@ -803,7 +145,7 @@ pub fn reg_not_read_is_zero_meta(idx: usize) -> ConstraintMeta { } /// `arg2` multiplex for word index `word_idx ∈ {0, 1}`. Twin of -/// [`Arg2Constraint`]: +/// `Arg2Constraint`: /// /// ```text /// arg2[i] − (MEMORY·imm[i] + BRANCH·rv2[i] + (1−MEMORY−BRANCH)·(rv2[i] + imm[i])) @@ -852,7 +194,7 @@ fn res_word_expr>( } /// `(1 − MEMORY − BRANCH) · (rvd[i] − cast(res, WL)[i]) = 0`. Twin of -/// [`RvdEqResConstraint`]. +/// `RvdEqResConstraint`. pub fn emit_rvd_eq_res>( b: &mut B, idx: usize, @@ -909,7 +251,7 @@ fn emit_pc_len_add_pair>( b: &mut B, idx: usize, @@ -928,7 +270,7 @@ pub fn branch_rvd_meta(idx: usize) -> [ConstraintMeta; 2] { } /// `branch_cond − (BRANCH·JALR + BRANCH·(1−JALR)·res[0])`. Twin of -/// [`BranchCondConstraint`]. +/// `BranchCondConstraint`. pub fn emit_branch_cond>( b: &mut B, idx: usize, @@ -950,7 +292,7 @@ pub fn branch_cond_meta(idx: usize) -> ConstraintMeta { /// `(1 − branch_cond) · carry · (1 − carry) = 0` for /// `next_pc = pc + instruction_length` (two instances at `idx`, `idx + 1`). -/// Twin of [`NextPcAddConstraint::new_pair`]. +/// Twin of `NextPcAddConstraint::new_pair`. pub fn emit_next_pc_add_pair>( b: &mut B, idx: usize, @@ -973,9 +315,8 @@ pub fn next_pc_add_meta(idx: usize) -> [ConstraintMeta; 2] { // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// The CPU table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`create_all_cpu_constraints`] index-for-index (39 constraints, -/// all base-field): +/// The CPU table's transition constraints as a single [`ConstraintSet`] +/// ([`NUM_CPU_CONSTRAINTS`] = 39 constraints, all base-field): /// - idx 0..11: IS_BIT (unconditional) on each of [`BIT_FLAG_COLUMNS`]; /// - idx 12,13: ADD fast-path carry pair (conditional on `ADD`); /// - idx 14,15: SUB fast-path carry pair (conditional on `SUB`); diff --git a/prover/src/constraints/templates.rs b/prover/src/constraints/templates.rs index e5adbd9b7..5c66044f6 100644 --- a/prover/src/constraints/templates.rs +++ b/prover/src/constraints/templates.rs @@ -11,10 +11,6 @@ //! - lhs, rhs, sum: DWordWL (2 × 32-bit words) //! - Embeds carry constraints inline -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::{constraints::transition::TransitionConstraint, table::TableView}; - use crate::tables::types::{GoldilocksExtension, GoldilocksField}; // ========================================================================= @@ -29,84 +25,6 @@ pub const SHIFT_32: u64 = 1u64 << 32; /// Verify: INV_SHIFT_32 * SHIFT_32 ≡ 1 (mod p) pub const INV_SHIFT_32: u64 = 18446744065119617026; -/// 2^(-32) in the field, used for carry extraction. -#[inline] -fn inv_2_32() -> FieldElement { - FieldElement::from(INV_SHIFT_32) -} - -// ========================================================================= -// IS_BIT Template -// ========================================================================= - -/// Enforces that a value is binary (0 or 1). -/// -/// Two modes: -/// - Conditional: `cond * X * (1-X) = 0` (degree 3) -/// - Unconditional: `X * (1-X) = 0` (degree 2) -pub struct IsBitConstraint { - /// Column index for the condition (None = unconditional) - cond_col: Option, - /// Column index for the value to check (X) - value_col: usize, - /// Unique constraint identifier - constraint_idx: usize, -} - -impl IsBitConstraint { - /// Creates a conditional IS_BIT constraint. - /// - /// Constraint: `cond * X * (1-X) = 0` - pub fn new(cond_col: usize, value_col: usize, constraint_idx: usize) -> Self { - Self { - cond_col: Some(cond_col), - value_col, - constraint_idx, - } - } - - /// Creates an unconditional IS_BIT constraint. - /// - /// Constraint: `X * (1-X) = 0` - pub fn unconditional(value_col: usize, constraint_idx: usize) -> Self { - Self { - cond_col: None, - value_col, - constraint_idx, - } - } -} - -impl TransitionConstraint for IsBitConstraint { - fn degree(&self) -> usize { - match self.cond_col { - Some(_) => 3, // cubic: cond * X * (1-X) - None => 2, // quadratic: X * (1-X) - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let x = step.get_main_evaluation_element(0, self.value_col).clone(); - let one = FieldElement::::one(); - - match self.cond_col { - Some(cond_col) => { - let cond = step.get_main_evaluation_element(0, cond_col).clone(); - &cond * &x * (one - x) - } - None => &x * (one - &x), - } - } -} - // ========================================================================= // ADD Template (Embedded Carry Approach) // ========================================================================= @@ -159,71 +77,7 @@ pub enum AddOperand { }, } -impl AddLinearTerm { - /// Evaluate this term using values from the trace. - fn eval(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - match self { - AddLinearTerm::Column { - coefficient, - column, - } => { - let col_val = step.get_main_evaluation_element(0, *column); - col_val * FieldElement::::from(*coefficient) - } - AddLinearTerm::Constant(value) => FieldElement::::from(*value), - } - } -} - -/// Evaluate a slice of terms as a sum. -fn eval_terms(terms: &[AddLinearTerm], step: &TableView) -> FieldElement -where - F: IsSubFieldOf, - E: IsField, -{ - if terms.is_empty() { - FieldElement::zero() - } else { - terms - .iter() - .map(|t| t.eval(step)) - .fold(FieldElement::zero(), |acc, x| acc + x) - } -} - impl AddOperand { - /// Get the low word value from the trace. - pub fn eval_lo(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - match self { - AddOperand::DWordWL { start_column } => { - step.get_main_evaluation_element(0, *start_column).clone() - } - AddOperand::Linear { lo, .. } => eval_terms(lo, step), - } - } - - /// Get the high word value from the trace. - pub fn eval_hi(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - match self { - AddOperand::DWordWL { start_column } => step - .get_main_evaluation_element(0, *start_column + 1) - .clone(), - AddOperand::Linear { hi, .. } => eval_terms(hi, step), - } - } - // ------------------------------------------------------------------------- // Convenience constructors for common cast types // ------------------------------------------------------------------------- @@ -333,183 +187,6 @@ impl AddOperand { } } -// ------------------------------------------------------------------------- -// AddConstraint -// ------------------------------------------------------------------------- - -/// 64-bit addition constraint with embedded carry. -/// -/// Enforces: `lhs + rhs = sum (mod 2^64)` -/// -/// Uses DWordWL representation (2 × 32-bit words): -/// - lhs = [lhs_lo, lhs_hi] -/// - rhs = [rhs_lo, rhs_hi] -/// - sum = [sum_lo, sum_hi] -/// -/// Embeds virtual carry columns inline: -/// - carry_0 = (lhs_lo + rhs_lo - sum_lo) / 2^32 -/// - carry_1 = (lhs_hi + rhs_hi + carry_0 - sum_hi) / 2^32 -/// -/// Constraints: -/// - carry_0 is a bit: cond * carry_0 * (1 - carry_0) = 0 -/// - carry_1 is a bit: cond * carry_1 * (1 - carry_1) = 0 -/// -/// Assumptions (must be verified via bus lookups): -/// - lhs_lo, lhs_hi, rhs_lo, rhs_hi, sum_lo, sum_hi are all valid 32-bit words -pub struct AddConstraint { - /// Column indices for condition flags (constraint active when sum > 0) - cond_cols: Vec, - /// Left-hand side operand (flexible representation) - lhs: AddOperand, - /// Right-hand side operand (flexible representation) - rhs: AddOperand, - /// Sum/output operand (flexible representation) - sum: AddOperand, - /// Which carry constraint this is (0 or 1) - carry_idx: usize, - /// Unique constraint identifier - constraint_idx: usize, -} - -impl AddConstraint { - /// Creates ADD constraints for both carries. - /// - /// Returns two constraints: one for carry_0 and one for carry_1. - /// - /// # Arguments - /// * `cond_cols` - Column indices for condition flags (constraint active when sum > 0) - /// * `lhs` - Left-hand side operand (flexible representation) - /// * `rhs` - Right-hand side operand (flexible representation) - /// * `sum` - Sum/output operand (flexible representation) - /// * `constraint_idx_start` - Starting constraint index (uses 2 consecutive indices) - pub fn new_pair( - cond_cols: Vec, - lhs: AddOperand, - rhs: AddOperand, - sum: AddOperand, - constraint_idx_start: usize, - ) -> (Self, Self) { - let carry_0 = Self { - cond_cols: cond_cols.clone(), - lhs: lhs.clone(), - rhs: rhs.clone(), - sum: sum.clone(), - carry_idx: 0, - constraint_idx: constraint_idx_start, - }; - - let carry_1 = Self { - cond_cols, - lhs, - rhs, - sum, - carry_idx: 1, - constraint_idx: constraint_idx_start + 1, - }; - - (carry_0, carry_1) - } - - /// Compute carry_0 inline from trace values. - fn compute_carry_0(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let lhs_lo = self.lhs.eval_lo(step); - let rhs_lo = self.rhs.eval_lo(step); - let sum_lo = self.sum.eval_lo(step); - - // carry_0 = (lhs_lo + rhs_lo - sum_lo) * 2^(-32) - (lhs_lo + rhs_lo - sum_lo) * inv_2_32::() - } - - /// Compute carry_1 inline from trace values. - fn compute_carry_1(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let lhs_hi = self.lhs.eval_hi(step); - let rhs_hi = self.rhs.eval_hi(step); - let sum_hi = self.sum.eval_hi(step); - let carry_0 = self.compute_carry_0(step); - - // carry_1 = (lhs_hi + rhs_hi + carry_0 - sum_hi) * 2^(-32) - (lhs_hi + rhs_hi + carry_0 - sum_hi) * inv_2_32::() - } - - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - - let carry = match self.carry_idx { - 0 => self.compute_carry_0(step), - 1 => self.compute_carry_1(step), - _ => unreachable!("carry_idx validated <= 1 at construction"), - }; - - if self.cond_cols.is_empty() { - // Unconditional: carry * (1 - carry) - &carry * (one - &carry) - } else { - // Conditional: cond * carry * (1 - carry) - let cond = self - .cond_cols - .iter() - .map(|&col| step.get_main_evaluation_element(0, col).clone()) - .fold(FieldElement::::zero(), |acc, x| acc + x); - cond * &carry * (one - carry) - } - } -} - -impl TransitionConstraint for AddConstraint { - fn degree(&self) -> usize { - if self.cond_cols.is_empty() { 2 } else { 3 } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -// ========================================================================= -// Helper Functions -// ========================================================================= - -/// Creates multiple unconditional IS_BIT constraints for the given columns. -/// -/// # Arguments -/// * `value_cols` - Slice of column indices to constrain -/// * `constraint_idx_start` - Starting index for constraint numbering -/// -/// # Returns -/// Vector of IS_BIT constraints and the next available constraint index. -pub fn new_is_bit_constraints( - value_cols: &[usize], - constraint_idx_start: usize, -) -> (Vec, usize) { - let constraints = value_cols - .iter() - .enumerate() - .map(|(i, &col)| IsBitConstraint::unconditional(col, constraint_idx_start + i)) - .collect(); - - (constraints, constraint_idx_start + value_cols.len()) -} - // ========================================================================= // Single-body emit functions (ConstraintBuilder front-end) // ========================================================================= @@ -528,7 +205,7 @@ pub fn new_is_bit_constraints( use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta}; /// IS_BIT: `x·(1−x)`, optionally gated by a condition column: -/// `cond·x·(1−x)`. Twin of [`IsBitConstraint`]. +/// `cond·x·(1−x)`. Twin of `IsBitConstraint`. pub fn emit_is_bit>( b: &mut B, idx: usize, @@ -602,7 +279,7 @@ fn add_operand_hi>( } /// The ADD carry pair, emitted from ONE body at `idx` and `idx + 1` -/// (twin of [`AddConstraint::new_pair`]): +/// (twin of `AddConstraint::new_pair`): /// /// ```text /// carry_0 = (lhs.lo + rhs.lo − sum.lo)·2⁻³² diff --git a/prover/src/tables/branch.rs b/prover/src/tables/branch.rs index bffafaa87..b4a4fcb8d 100644 --- a/prover/src/tables/branch.rs +++ b/prover/src/tables/branch.rs @@ -26,12 +26,8 @@ //! - Sender: IS_HALFWORD (×3 for next_pc_high[0..3]) //! - Receiver: BRANCH (provides branch targets to CPU) -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; -use stark::constraints::transition::TransitionConstraint; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use std::collections::HashMap; @@ -357,215 +353,6 @@ pub fn bus_interactions() -> Vec { ] } -// ========================================================================= -// Constraints -// ========================================================================= - -/// BRANCH table conditional ADD constraint. -/// -/// Implements two conditional ADD templates per the spec: -/// - `ADD(pc, offset) = next_pc_unmasked` conditioned on `(1 - JALR)` -/// - `ADD(register, offset) = next_pc_unmasked` conditioned on `JALR` -/// -/// Each ADD template produces two carry IS_BIT constraints (carry_0 and carry_1), -/// for a total of 4 constraints, all at degree 3: -/// `cond * carry * (1 - carry) = 0` -/// -/// The carries are computed from degree-1 operands (pc or register, not both), -/// so carry is degree 1 and the full constraint is degree 3. -pub struct BranchConstraint { - /// Unique constraint identifier - constraint_idx: usize, - /// Which constraint to check - kind: BranchConstraintKind, -} - -/// Kind of BRANCH constraint. -/// -/// Four variants: two carries × two conditions (pc-path and register-path). -#[derive(Debug, Clone, Copy)] -pub enum BranchConstraintKind { - /// `(1 - JALR) * carry_0_pc * (1 - carry_0_pc) = 0` - /// where carry_0_pc = (pc[0] + offset[0] - next_pc_unmasked[0]) / 2^32 - PcCarry0IsBit, - /// `(1 - JALR) * carry_1_pc * (1 - carry_1_pc) = 0` - /// where carry_1_pc = (pc[1] + offset[1] + carry_0_pc - next_pc_unmasked[1]) / 2^32 - PcCarry1IsBit, - /// `IS_BIT`: `JALR * (1 - JALR) = 0` (spec defense-in-depth assumption) - JalrIsBit, - /// `JALR * carry_0_reg * (1 - carry_0_reg) = 0` - /// where carry_0_reg = (register[0] + offset[0] - next_pc_unmasked[0]) / 2^32 - RegCarry0IsBit, - /// `JALR * carry_1_reg * (1 - carry_1_reg) = 0` - /// where carry_1_reg = (register[1] + offset[1] + carry_0_reg - next_pc_unmasked[1]) / 2^32 - RegCarry1IsBit, -} - -impl BranchConstraint { - /// Creates a new BRANCH constraint. - pub fn new(kind: BranchConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } - - /// Compute virtual next_pc_unmasked as DWordWL. - /// - /// next_pc_unmasked[0] = unmasked_low_byte + 2^8 * next_pc_low[1] + 2^16 * next_pc_high[0] - /// next_pc_unmasked[1] = next_pc_high[1] + 2^16 * next_pc_high[2] - fn compute_next_pc_unmasked(step: &TableView) -> (FieldElement, FieldElement) - where - F: IsSubFieldOf, - E: IsField, - { - let unmasked_low_byte = step - .get_main_evaluation_element(0, cols::UNMASKED_LOW_BYTE) - .clone(); - let next_pc_low_1 = step - .get_main_evaluation_element(0, cols::NEXT_PC_LOW_1) - .clone(); - let next_pc_high_0 = step - .get_main_evaluation_element(0, cols::NEXT_PC_HIGH_0) - .clone(); - let next_pc_high_1 = step - .get_main_evaluation_element(0, cols::NEXT_PC_HIGH_1) - .clone(); - let next_pc_high_2 = step - .get_main_evaluation_element(0, cols::NEXT_PC_HIGH_2) - .clone(); - - let shift_8 = FieldElement::::from(SHIFT_8); - let shift_16 = FieldElement::::from(SHIFT_16); - - let unmasked_0 = - &unmasked_low_byte + &next_pc_low_1 * &shift_8 + &next_pc_high_0 * &shift_16; - let unmasked_1 = &next_pc_high_1 + &next_pc_high_2 * &shift_16; - - (unmasked_0, unmasked_1) - } - - /// Compute carry_0 for a given base column pair. - /// - /// carry_0 = (base[0] + offset[0] - next_pc_unmasked[0]) / 2^32 - fn compute_carry_0_for(base_col_0: usize, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let base_0 = step.get_main_evaluation_element(0, base_col_0).clone(); - let offset_0 = step.get_main_evaluation_element(0, cols::OFFSET_0).clone(); - let (unmasked_0, _) = Self::compute_next_pc_unmasked(step); - - let inv_2_32 = FieldElement::::from(crate::constraints::templates::INV_SHIFT_32); - (base_0 + offset_0 - unmasked_0) * inv_2_32 - } - - /// Compute carry_1 for a given base column pair. - /// - /// carry_1 = (base[1] + offset[1] + carry_0 - next_pc_unmasked[1]) / 2^32 - fn compute_carry_1_for( - base_col_0: usize, - base_col_1: usize, - step: &TableView, - ) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let base_1 = step.get_main_evaluation_element(0, base_col_1).clone(); - let offset_1 = step.get_main_evaluation_element(0, cols::OFFSET_1).clone(); - let carry_0 = Self::compute_carry_0_for(base_col_0, step); - let (_, unmasked_1) = Self::compute_next_pc_unmasked(step); - - let inv_2_32 = FieldElement::::from(crate::constraints::templates::INV_SHIFT_32); - (base_1 + offset_1 + carry_0 - unmasked_1) * inv_2_32 - } - - /// Compute the constraint value: `cond * carry * (1 - carry)`. - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let jalr = step.get_main_evaluation_element(0, cols::JALR).clone(); - let one = FieldElement::::one(); - - match self.kind { - BranchConstraintKind::JalrIsBit => &jalr * (&one - &jalr), - BranchConstraintKind::PcCarry0IsBit => { - let cond = &one - &jalr; - let c = Self::compute_carry_0_for(cols::PC_0, step); - cond * &c * (&one - c) - } - BranchConstraintKind::PcCarry1IsBit => { - let cond = &one - &jalr; - let c = Self::compute_carry_1_for(cols::PC_0, cols::PC_1, step); - cond * &c * (&one - c) - } - BranchConstraintKind::RegCarry0IsBit => { - let cond = jalr; - let c = Self::compute_carry_0_for(cols::REGISTER_0, step); - cond * &c * (&one - c) - } - BranchConstraintKind::RegCarry1IsBit => { - let cond = jalr; - let c = Self::compute_carry_1_for(cols::REGISTER_0, cols::REGISTER_1, step); - cond * &c * (&one - c) - } - } - } -} - -impl TransitionConstraint for BranchConstraint { - fn degree(&self) -> usize { - match self.kind { - // JALR * (1 - JALR) = degree 2 - BranchConstraintKind::JalrIsBit => 2, - // cond (degree 1) * carry (degree 1) * (1 - carry) (degree 1) = degree 3 - _ => 3, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Creates all constraints for the BRANCH table. -/// -/// Returns 5 constraints (two conditional ADD templates × 2 carries each, plus -/// the `IS_BIT` defense-in-depth assumption): -/// - PcCarry0IsBit: `(1 - JALR) * carry_0 * (1 - carry_0) = 0` (pc path) -/// - PcCarry1IsBit: `(1 - JALR) * carry_1 * (1 - carry_1) = 0` (pc path) -/// - RegCarry0IsBit: `JALR * carry_0 * (1 - carry_0) = 0` (register path) -/// - RegCarry1IsBit: `JALR * carry_1 * (1 - carry_1) = 0` (register path) -/// - JalrIsBit: `JALR * (1 - JALR) = 0` -pub fn branch_constraints(constraint_idx_start: usize) -> (Vec, usize) { - let mut idx = constraint_idx_start; - let mut next = || { - let i = idx; - idx += 1; - i - }; - let constraints = vec![ - BranchConstraint::new(BranchConstraintKind::PcCarry0IsBit, next()), - BranchConstraint::new(BranchConstraintKind::PcCarry1IsBit, next()), - BranchConstraint::new(BranchConstraintKind::RegCarry0IsBit, next()), - BranchConstraint::new(BranchConstraintKind::RegCarry1IsBit, next()), - BranchConstraint::new(BranchConstraintKind::JalrIsBit, next()), - ]; - (constraints, idx) -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= @@ -614,7 +401,7 @@ fn carry_1_expr>( } /// The BRANCH table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`branch_constraints`] index-for-index (5 constraints): +/// mirroring `branch_constraints` index-for-index (5 constraints): /// - idx 0: `(1 − JALR)·carry_0·(1 − carry_0)` on the pc path (degree 3); /// - idx 1: `(1 − JALR)·carry_1·(1 − carry_1)` on the pc path (degree 3); /// - idx 2: `JALR·carry_0·(1 − carry_0)` on the register path (degree 3); diff --git a/prover/src/tables/commit.rs b/prover/src/tables/commit.rs index 2da9a8c45..cf781b186 100644 --- a/prover/src/tables/commit.rs +++ b/prover/src/tables/commit.rs @@ -43,17 +43,13 @@ //! - `count_decr_carry_0`: SUB template carry_0 for count_decr + 1 = count (degree 2) //! - `count_decr_carry_1`: SUB template carry_1 for count_decr + 1 = count (degree 2) //! -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use crate::constraints::templates::{ - AddConstraint, AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, + AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, }; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; @@ -729,139 +725,12 @@ pub fn bus_interactions() -> Vec { ] } -// ========================================================================= -// Constraints -// ========================================================================= - -/// Creates all constraints for the COMMIT table (8 total). -/// -/// Returns constraint objects and the next available constraint index. -/// -/// Constraints 0-2: IS_BIT for first, end, mu -/// Constraint 3: (first + end) * (1 - mu) = 0 -/// Constraints 4-5: ADD template for address + 1 = address_incr (unconditional) -/// Constraints 6-7: SUB template for count_decr + 1 = count (unconditional) -pub fn create_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - let mut constraints: Vec< - Box>, - > = Vec::with_capacity(8); - let mut idx = constraint_idx_start; - - // 0-2: IS_BIT for first, end, mu - let (is_bit_constraints, next) = crate::constraints::templates::new_is_bit_constraints( - &[cols::FIRST, cols::END, cols::MU], - idx, - ); - for c in is_bit_constraints { - constraints.push(c.boxed()); - } - idx = next; - - // 3: (first + end) * (1 - mu) = 0 - constraints.push( - (CommitConstraint { - kind: CommitConstraintKind::FirstOrEndImpliesMu, - constraint_idx: idx, - }) - .boxed(), - ); - idx += 1; - - // 4-5: ADD template for address + 1 = address_incr (unconditional, degree 2) - // lhs = address (DWordWL), rhs = 1, sum = address_incr (DWordHL → DWordWL) - let (add_c0, add_c1) = AddConstraint::new_pair( - vec![], // unconditional - AddOperand::dword(cols::ADDRESS_0), - AddOperand::constant(1), - AddOperand::from_dword_hl(cols::ADDRESS_INCR_0), - idx, - ); - constraints.push(add_c0.boxed()); - constraints.push(add_c1.boxed()); - idx += 2; - - // 6-7: SUB template for count - 1 = count_decr (unconditional, degree 2) - // Expressed as ADD: count_decr + 1 = count - // lhs = count_decr (DWordHL → DWordWL), rhs = 1, sum = count (DWordWL) - let (sub_c0, sub_c1) = AddConstraint::new_pair( - vec![], // unconditional - AddOperand::from_dword_hl(cols::COUNT_DECR_0), - AddOperand::constant(1), - AddOperand::dword(cols::COUNT_0), - idx, - ); - constraints.push(sub_c0.boxed()); - constraints.push(sub_c1.boxed()); - idx += 2; - - (constraints, idx) -} - -/// The kind of COMMIT-specific constraint (not covered by templates). -#[derive(Debug, Clone, Copy)] -enum CommitConstraintKind { - /// (first + end) * (1 - mu) = 0 - FirstOrEndImpliesMu, -} - -/// A constraint for the COMMIT table. -struct CommitConstraint { - kind: CommitConstraintKind, - constraint_idx: usize, -} - -impl CommitConstraint { - fn compute( - &self, - step: &stark::table::TableView, - ) -> math::field::element::FieldElement - where - F: math::field::traits::IsSubFieldOf, - E: math::field::traits::IsField, - { - let one = math::field::element::FieldElement::::one(); - - match self.kind { - CommitConstraintKind::FirstOrEndImpliesMu => { - let first = step.get_main_evaluation_element(0, cols::FIRST).clone(); - let end = step.get_main_evaluation_element(0, cols::END).clone(); - let mu = step.get_main_evaluation_element(0, cols::MU).clone(); - // (first + end) * (1 - mu) = 0 - (first + end) * (one - mu) - } - } - } -} - -impl TransitionConstraint for CommitConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= /// The COMMIT table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`create_constraints`] index-for-index (8 constraints): +/// mirroring `create_constraints` index-for-index (8 constraints): /// - idx 0-2: `IS_BIT` on `first`, `end`, `μ`; /// - idx 3: `(first + end)·(1 − μ) = 0` (first/end ⇒ μ); /// - idx 4,5: `ADD` pair `address + 1 = address_incr` (unconditional); diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 1752022b9..7c7dc3313 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -314,7 +314,7 @@ impl CpuOperation { // address `pc + instruction_length` on every BRANCH row (written to `rd` // only by JAL/JALR — `cpu.toml` branch group); `res` // otherwise. The spec computes this `pc + len` via the ADD chip gated on - // `BRANCH`; we pin it with [`BranchRvdConstraint`] (carry-omitting, like + // `BRANCH`; we pin it with `BranchRvdConstraint` (carry-omitting, like // `next_pc`). For conditional branches `rvd` is computed but never // written (`write_register = 0`). let store = f.memory && jalr; // under MEMORY, mem_flags bit 0 = memory_op (1 = store) diff --git a/prover/src/tables/cpu32.rs b/prover/src/tables/cpu32.rs index 7f6c7a845..7b37bf16a 100644 --- a/prover/src/tables/cpu32.rs +++ b/prover/src/tables/cpu32.rs @@ -17,11 +17,7 @@ //! //! Register reads use the cast-to-`DWordWL` encoding. -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use super::types::{ @@ -31,8 +27,7 @@ use super::types::{ use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use crate::constraints::templates::{ - AddConstraint, AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, - new_is_bit_constraints, + AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, }; // ========================================================================= @@ -589,272 +584,12 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// Arithmetic constraints for CPU32: the sign-extension `ext` group plus the -/// register-zero checks. (`IS_BIT` flags and the ADD/SUB carries are produced -/// by the template helpers in [`cpu32_constraints`].) -pub struct Cpu32Constraint { - constraint_idx: usize, - kind: Cpu32ConstraintKind, -} - -#[derive(Debug, Clone, Copy)] -pub enum Cpu32ConstraintKind { - /// `arg1[0] = rv1[0] + 2^16·rv1[1]` (low word of `arg1`). - Arg1Lo, - /// `arg1[1] = (2^32-1)·rv1_sign` (sign/zero extension of the high word; - /// `rv1_sign` already folds in `signed` via `SIGN(rv1[1], signed)`). - Arg1Hi, - /// `arg2[0] = rv2[0] + 2^16·rv2[1] + imm[0]`. - Arg2Lo, - /// `arg2[1] = (2^32-1)·rv2_sign + imm[1]` (`rv2_sign` folds in `signed`). - Arg2Hi, - /// `rvd[0] = res[0] + 2^16·res[1]`. - RvdLo, - /// `rvd[1] = (2^32-1)·res_sign` (the `*W` result is always sign-extended). - RvdHi, - /// `(1 - read_col)·value_col = 0` (an unread register half is zero). - RegZero { read_col: usize, value_col: usize }, - /// `read_register2·imm[i] = 0` (decoding guarantees at most one is nonzero; - /// spec defense-in-depth assumption). `usize` is the `imm` limb column. - Arg2Exclusive { imm_col: usize }, - /// `(1 - signed)·sign_col = 0`: the arith half of `SIGN(rv·[1], signed)` — - /// when the inputs are not sign-extended the sign bit must be 0 (the MSB16 - /// lookup is gated by `signed`, so it is not pinned otherwise). `usize` is - /// the sign column (`RV1_SIGN`/`RV2_SIGN`). - SignZeroWhenUnsigned { sign_col: usize }, - /// `(1 - μ)·flag = 0`: a flag that drives a bus interaction or a high-word - /// fill must be 0 on a padding row (`μ = 0`). For the register flags this - /// prevents a disconnected row from emitting a forged register read/write - /// token (no DECODE binding, no CPU32 delegation); for `signed` it closes - /// the soundness hole where a free `signed` on padding (the `BYTE_ALU` - /// extractor is gated by `μ`) leaks into the `arg1/arg2` high words; for - /// `res_sign` (gated by the μ-gated `MSB16`) it is the arith half of - /// `SIGN(res, μ)`, keeping the `rvd` high word zero on padding. Spec - /// `cpu32.toml` (PR #646). `usize` is the flag column. - FlagImpliesMu { flag_col: usize }, -} - -impl Cpu32Constraint { - pub fn new(kind: Cpu32ConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } -} - -impl TransitionConstraint for Cpu32Constraint { - fn degree(&self) -> usize { - match self.kind { - // `arg·[1] = (2^32-1)·rv·_sign` is now linear (`signed` is folded into - // `rv·_sign`); the lo/rvd fills are linear too. - Cpu32ConstraintKind::Arg1Lo - | Cpu32ConstraintKind::Arg1Hi - | Cpu32ConstraintKind::Arg2Lo - | Cpu32ConstraintKind::Arg2Hi - | Cpu32ConstraintKind::RvdLo - | Cpu32ConstraintKind::RvdHi => 1, - // (1-read)·value, read2·imm, (1-μ)·flag, (1-signed)·sign — all degree 2 - Cpu32ConstraintKind::RegZero { .. } - | Cpu32ConstraintKind::Arg2Exclusive { .. } - | Cpu32ConstraintKind::FlagImpliesMu { .. } - | Cpu32ConstraintKind::SignZeroWhenUnsigned { .. } => 2, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let get = |c: usize| step.get_main_evaluation_element(0, c).clone(); - let shift16 = FieldElement::::from(SHIFT_16); - let hi_fill = FieldElement::::from(HI_FILL); - let one = FieldElement::::one(); - - match self.kind { - Cpu32ConstraintKind::Arg1Lo => { - get(cols::ARG1_0) - get(cols::RV1_0) - &shift16 * get(cols::RV1_1) - } - Cpu32ConstraintKind::Arg1Hi => get(cols::ARG1_1) - hi_fill * get(cols::RV1_SIGN), - Cpu32ConstraintKind::Arg2Lo => { - get(cols::ARG2_0) - - get(cols::RV2_0) - - &shift16 * get(cols::RV2_1) - - get(cols::IMM_0) - } - Cpu32ConstraintKind::Arg2Hi => { - get(cols::ARG2_1) - hi_fill * get(cols::RV2_SIGN) - get(cols::IMM_1) - } - Cpu32ConstraintKind::RvdLo => { - get(cols::RVD_0) - get(cols::RES_0) - &shift16 * get(cols::RES_1) - } - Cpu32ConstraintKind::RvdHi => get(cols::RVD_1) - hi_fill * get(cols::RES_SIGN), - Cpu32ConstraintKind::RegZero { - read_col, - value_col, - } => (one - get(read_col)) * get(value_col), - Cpu32ConstraintKind::Arg2Exclusive { imm_col } => { - get(cols::READ_REGISTER2) * get(imm_col) - } - Cpu32ConstraintKind::FlagImpliesMu { flag_col } => { - (one - get(cols::MU)) * get(flag_col) - } - Cpu32ConstraintKind::SignZeroWhenUnsigned { sign_col } => { - (one - get(cols::SIGNED)) * get(sign_col) - } - } - } -} - -/// Creates all transition constraints for the CPU32 table: -/// `IS_BIT` on the flag columns, the `ADD`/`SUB` fast-path carries, the -/// register-zero checks, and the sign-extension `ext` arithmetic. -pub fn cpu32_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - let mut constraints: Vec< - Box>, - > = Vec::new(); - - // IS_BIT on the flag columns and the multiplicity. - let (is_bit, mut idx) = new_is_bit_constraints( - &[ - cols::READ_REGISTER1, - cols::READ_REGISTER2, - cols::WRITE_REGISTER, - cols::ALU, - cols::ADD, - cols::SUB, - cols::MU, - ], - constraint_idx_start, - ); - for c in is_bit { - constraints.push(c.boxed()); - } - - // ADD fast-path: arg1 + arg2 = res (cond = ADD). - let (add_lo, add_hi) = AddConstraint::new_pair( - vec![cols::ADD], - AddOperand::dword(cols::ARG1_0), - AddOperand::dword(cols::ARG2_0), - AddOperand::from_dword_hl(cols::RES_0), - idx, - ); - idx += 2; - constraints.push(add_lo.boxed()); - constraints.push(add_hi.boxed()); - - // SUB fast-path: res = arg1 - arg2, encoded as arg2 + res = arg1 (cond = SUB). - let (sub_lo, sub_hi) = AddConstraint::new_pair( - vec![cols::SUB], - AddOperand::dword(cols::ARG2_0), - AddOperand::from_dword_hl(cols::RES_0), - AddOperand::dword(cols::ARG1_0), - idx, - ); - idx += 2; - constraints.push(sub_lo.boxed()); - constraints.push(sub_hi.boxed()); - - // Unread register limbs are zero. `rv1`/`rv2` span three limbs - // (low halfword, high halfword, high word), so all three must be forced to - // zero when the register is not read — the bus reads the full word - // `[lo0 + 2^16·lo1, hi]`, leaving `RV*_2` free otherwise. - for (read_col, value_col) in [ - (cols::READ_REGISTER1, cols::RV1_0), - (cols::READ_REGISTER1, cols::RV1_1), - (cols::READ_REGISTER1, cols::RV1_2), - (cols::READ_REGISTER2, cols::RV2_0), - (cols::READ_REGISTER2, cols::RV2_1), - (cols::READ_REGISTER2, cols::RV2_2), - ] { - constraints.push( - Cpu32Constraint::new( - Cpu32ConstraintKind::RegZero { - read_col, - value_col, - }, - idx, - ) - .boxed(), - ); - idx += 1; - } - - // Sign-extension (`ext`) arithmetic for arg1, arg2, rvd. - for kind in [ - Cpu32ConstraintKind::Arg1Lo, - Cpu32ConstraintKind::Arg1Hi, - Cpu32ConstraintKind::Arg2Lo, - Cpu32ConstraintKind::Arg2Hi, - Cpu32ConstraintKind::RvdLo, - Cpu32ConstraintKind::RvdHi, - ] { - constraints.push(Cpu32Constraint::new(kind, idx).boxed()); - idx += 1; - } - - // arith half of `SIGN(rv·[1], signed)`: when not sign-extending, the sign - // bit is 0 (the MSB16 is gated by `signed`, so it is not otherwise pinned). - for sign_col in [cols::RV1_SIGN, cols::RV2_SIGN] { - constraints.push( - Cpu32Constraint::new(Cpu32ConstraintKind::SignZeroWhenUnsigned { sign_col }, idx) - .boxed(), - ); - idx += 1; - } - - // arg2 multiplex exclusivity (spec assumption): read_register2·imm[i] = 0. - for imm_col in [cols::IMM_0, cols::IMM_1] { - constraints.push( - Cpu32Constraint::new(Cpu32ConstraintKind::Arg2Exclusive { imm_col }, idx).boxed(), - ); - idx += 1; - } - - // flag ⇒ μ: a flag must be 0 on padding rows (μ = 0). The register flags - // gate MEMW interactions, so a free flag would inject a forged register - // access; `signed` (extracted via a μ-gated BYTE_ALU) would otherwise be - // free on padding and leak into the `arg1/arg2` high-word fills; `res_sign` - // (from the μ-gated MSB16) would otherwise be free and leak into the `rvd` - // high word. This is the arith half of `SIGN(res, μ)`. Spec `cpu32.toml`, - // PR #646. ALU is not gated: with `write_register = 0` its ALU-lookup - // result is never written back, so it has no side effect. - for flag_col in [ - cols::READ_REGISTER1, - cols::READ_REGISTER2, - cols::WRITE_REGISTER, - cols::SIGNED, - cols::RES_SIGN, - ] { - constraints.push( - Cpu32Constraint::new(Cpu32ConstraintKind::FlagImpliesMu { flag_col }, idx).boxed(), - ); - idx += 1; - } - - (constraints, idx) -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= /// The CPU32 table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`cpu32_constraints`] index-for-index (32 constraints): +/// mirroring `cpu32_constraints` index-for-index (32 constraints): /// - idx 0-6: `IS_BIT` on `read_register1/2`, `write_register`, `alu`, `add`, /// `sub`, `μ`; /// - idx 7,8: `ADD` pair `arg1 + arg2 = res` (gated on `add`); diff --git a/prover/src/tables/dvrm.rs b/prover/src/tables/dvrm.rs index 7df15ba83..148b27400 100644 --- a/prover/src/tables/dvrm.rs +++ b/prover/src/tables/dvrm.rs @@ -29,11 +29,7 @@ //! - Sender: ZERO (×5 for div_by_zero, overflow, NEG template) //! - Receiver: DVRM (×2 for quotient and remainder results) -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::TransitionConstraint; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use std::collections::HashMap; @@ -973,344 +969,6 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// DVRM table constraint kinds. -#[derive(Debug, Clone, Copy)] -pub enum DvrmConstraintKind { - /// DVRM-A3: signed * (1 - signed) = 0 - SignedIsBit, - /// DVRM-C1: (r[0]+r[1]+r[2]+r[3]) * (sign_r - sign_n) = 0 - RemainderSignMatchesNumerator, - /// DVRM-C4.i: (1-sign_r) * (abs_r[i] - (r::DWordWL)[i]) = 0 - AbsRFormula(usize), - /// DVRM-C6.i: (1-sign_d) * (abs_d[i] - (d::DWordWL)[i]) = 0 - AbsDFormula(usize), - /// DVRM-C7: signed * (1-overflow) - sign_q = 0 - SignQFormula, - /// DVRM-C12.i: carry[i] * (1 - carry[i]) = 0 (virtual carries from n = n_sub_r + r) - CarryIsBit(usize), - /// DVRM-C15: sign_n_sub_r * (1-sign_n_sub_r) = 0 - SignNSubRIsBit, - /// DVRM-C18b: (1-signed) * sign_n = 0 - UnsignedSignN, - /// DVRM-C19b: (1-signed) * sign_r = 0 - UnsignedSignR, - /// DVRM-C20b: (1-signed) * sign_d = 0 - UnsignedSignD, - /// DVRM-C16.i: div_by_zero * (q[i] - 65535) = 0 - DivByZeroQ(usize), -} - -/// DVRM table constraint. -pub struct DvrmConstraint { - constraint_idx: usize, - kind: DvrmConstraintKind, -} - -impl DvrmConstraint { - /// Create a new DVRM constraint. - pub fn new(kind: DvrmConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } - - /// Compute the constraint value. - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - - match self.kind { - DvrmConstraintKind::SignedIsBit => { - // signed * (1 - signed) = 0 - let signed = step.get_main_evaluation_element(0, cols::SIGNED).clone(); - &signed * (&one - &signed) - } - DvrmConstraintKind::RemainderSignMatchesNumerator => { - // (r[0]+r[1]+r[2]+r[3]) * (sign_r - sign_n) = 0 - let r0 = step.get_main_evaluation_element(0, cols::R_0).clone(); - let r1 = step.get_main_evaluation_element(0, cols::R_1).clone(); - let r2 = step.get_main_evaluation_element(0, cols::R_2).clone(); - let r3 = step.get_main_evaluation_element(0, cols::R_3).clone(); - let sign_r = step.get_main_evaluation_element(0, cols::SIGN_R).clone(); - let sign_n = step.get_main_evaluation_element(0, cols::SIGN_N).clone(); - let r_sum = &r0 + &r1 + &r2 + &r3; - &r_sum * (&sign_r - &sign_n) - } - DvrmConstraintKind::AbsRFormula(i) => { - // (1-sign_r) * (abs_r[i] - (r::DWordWL)[i]) = 0 - let sign_r = step.get_main_evaluation_element(0, cols::SIGN_R).clone(); - let abs_r_col = if i == 0 { cols::ABS_R_0 } else { cols::ABS_R_1 }; - let abs_r = step.get_main_evaluation_element(0, abs_r_col).clone(); - - // r::DWordWL[i]: lo32 = r[0] + r[1]*2^16, hi32 = r[2] + r[3]*2^16 - let shift_16 = FieldElement::::from(SHIFT_16); - let r_wl = if i == 0 { - let r0 = step.get_main_evaluation_element(0, cols::R_0).clone(); - let r1 = step.get_main_evaluation_element(0, cols::R_1).clone(); - &r0 + &r1 * &shift_16 - } else { - let r2 = step.get_main_evaluation_element(0, cols::R_2).clone(); - let r3 = step.get_main_evaluation_element(0, cols::R_3).clone(); - &r2 + &r3 * &shift_16 - }; - - (&one - &sign_r) * (&abs_r - &r_wl) - } - DvrmConstraintKind::AbsDFormula(i) => { - // (1-sign_d) * (abs_d[i] - (d::DWordWL)[i]) = 0 - let sign_d = step.get_main_evaluation_element(0, cols::SIGN_D).clone(); - let abs_d_col = if i == 0 { cols::ABS_D_0 } else { cols::ABS_D_1 }; - let abs_d = step.get_main_evaluation_element(0, abs_d_col).clone(); - - let shift_16 = FieldElement::::from(SHIFT_16); - let d_wl = if i == 0 { - let d0 = step.get_main_evaluation_element(0, cols::D_0).clone(); - let d1 = step.get_main_evaluation_element(0, cols::D_1).clone(); - &d0 + &d1 * &shift_16 - } else { - let d2 = step.get_main_evaluation_element(0, cols::D_2).clone(); - let d3 = step.get_main_evaluation_element(0, cols::D_3).clone(); - &d2 + &d3 * &shift_16 - }; - - (&one - &sign_d) * (&abs_d - &d_wl) - } - DvrmConstraintKind::SignQFormula => { - // signed * (1-overflow) - sign_q = 0 - let signed = step.get_main_evaluation_element(0, cols::SIGNED).clone(); - let overflow = step.get_main_evaluation_element(0, cols::OVERFLOW).clone(); - let sign_q = step.get_main_evaluation_element(0, cols::SIGN_Q).clone(); - &signed * (&one - &overflow) - &sign_q - } - DvrmConstraintKind::CarryIsBit(i) => { - // Virtual carry from n = n_sub_r + r - // carry[i] * (1 - carry[i]) = 0 - let carry = self.compute_carry(i, step); - &carry * (&one - &carry) - } - DvrmConstraintKind::SignNSubRIsBit => { - // sign_n_sub_r * (1 - sign_n_sub_r) = 0 - let sign = step - .get_main_evaluation_element(0, cols::SIGN_N_SUB_R) - .clone(); - &sign * (&one - &sign) - } - DvrmConstraintKind::UnsignedSignN => { - // (1-signed) * sign_n = 0 - let signed = step.get_main_evaluation_element(0, cols::SIGNED).clone(); - let sign_n = step.get_main_evaluation_element(0, cols::SIGN_N).clone(); - (&one - &signed) * &sign_n - } - DvrmConstraintKind::UnsignedSignR => { - // (1-signed) * sign_r = 0 - let signed = step.get_main_evaluation_element(0, cols::SIGNED).clone(); - let sign_r = step.get_main_evaluation_element(0, cols::SIGN_R).clone(); - (&one - &signed) * &sign_r - } - DvrmConstraintKind::UnsignedSignD => { - // (1-signed) * sign_d = 0 - let signed = step.get_main_evaluation_element(0, cols::SIGNED).clone(); - let sign_d = step.get_main_evaluation_element(0, cols::SIGN_D).clone(); - (&one - &signed) * &sign_d - } - DvrmConstraintKind::DivByZeroQ(i) => { - // div_by_zero * (q[i] - 65535) = 0 - let dbz = step - .get_main_evaluation_element(0, cols::DIV_BY_ZERO) - .clone(); - let q_col = match i { - 0 => cols::Q_0, - 1 => cols::Q_1, - 2 => cols::Q_2, - 3 => cols::Q_3, - _ => unreachable!(), - }; - let q = step.get_main_evaluation_element(0, q_col).clone(); - let fill = FieldElement::::from(SIGN_FILL); - &dbz * (&q - &fill) - } - } - } - - /// Compute virtual carry[i] for the addition n_sub_r + r = n. - /// - /// The carries verify that n = n_sub_r + r by checking the carry chain. - /// We use sign-extended versions for signed arithmetic. - fn compute_carry(&self, i: usize, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let shift_16 = FieldElement::::from(SHIFT_16); - let inv_2_32 = FieldElement::::from(crate::constraints::templates::INV_SHIFT_32); - let sign_fill = FieldElement::::from(SIGN_FILL); - - // Get n, n_sub_r, r halfwords - let n: [FieldElement; 4] = [ - step.get_main_evaluation_element(0, cols::N_0).clone(), - step.get_main_evaluation_element(0, cols::N_1).clone(), - step.get_main_evaluation_element(0, cols::N_2).clone(), - step.get_main_evaluation_element(0, cols::N_3).clone(), - ]; - let nsr: [FieldElement; 4] = [ - step.get_main_evaluation_element(0, cols::N_SUB_R_0).clone(), - step.get_main_evaluation_element(0, cols::N_SUB_R_1).clone(), - step.get_main_evaluation_element(0, cols::N_SUB_R_2).clone(), - step.get_main_evaluation_element(0, cols::N_SUB_R_3).clone(), - ]; - let r: [FieldElement; 4] = [ - step.get_main_evaluation_element(0, cols::R_0).clone(), - step.get_main_evaluation_element(0, cols::R_1).clone(), - step.get_main_evaluation_element(0, cols::R_2).clone(), - step.get_main_evaluation_element(0, cols::R_3).clone(), - ]; - - let sign_n = step.get_main_evaluation_element(0, cols::SIGN_N).clone(); - let sign_r = step.get_main_evaluation_element(0, cols::SIGN_R).clone(); - let sign_nsr = step - .get_main_evaluation_element(0, cols::SIGN_N_SUB_R) - .clone(); - - // Build extended QuadWL values (4 words each) - // extended_n[0] = n[0] + n[1]*2^16 - // extended_n[1] = n[2] + n[3]*2^16 - // extended_n[2] = sign_n * 0xFFFFFFFF - // extended_n[3] = sign_n * 0xFFFFFFFF - let ext_n = self.build_extended_quad(&n, &sign_n, &shift_16, &sign_fill); - let ext_r = self.build_extended_quad(&r, &sign_r, &shift_16, &sign_fill); - let ext_nsr = self.build_extended_quad(&nsr, &sign_nsr, &shift_16, &sign_fill); - - // carry[0] = (ext_nsr[0] + ext_r[0] - ext_n[0]) / 2^32 - // carry[i] = (ext_nsr[i] + ext_r[i] + carry[i-1] - ext_n[i]) / 2^32 - if i == 0 { - (&ext_nsr[0] + &ext_r[0] - &ext_n[0]) * &inv_2_32 - } else { - let prev_carry = self.compute_carry(i - 1, step); - (&ext_nsr[i] + &ext_r[i] + &prev_carry - &ext_n[i]) * &inv_2_32 - } - } - - /// Build sign-extended QuadWL representation. - fn build_extended_quad, E: IsField>( - &self, - halfwords: &[FieldElement; 4], - sign: &FieldElement, - shift_16: &FieldElement, - sign_fill: &FieldElement, - ) -> [FieldElement; 4] { - let ext_word = sign * sign_fill + sign * sign_fill * shift_16; - [ - &halfwords[0] + &halfwords[1] * shift_16, - &halfwords[2] + &halfwords[3] * shift_16, - ext_word.clone(), - ext_word, - ] - } -} - -impl TransitionConstraint for DvrmConstraint { - fn degree(&self) -> usize { - match self.kind { - DvrmConstraintKind::SignedIsBit => 2, - DvrmConstraintKind::RemainderSignMatchesNumerator => 2, - DvrmConstraintKind::AbsRFormula(_) => 2, - DvrmConstraintKind::AbsDFormula(_) => 2, - DvrmConstraintKind::SignQFormula => 2, - DvrmConstraintKind::CarryIsBit(_) => 2, - DvrmConstraintKind::SignNSubRIsBit => 2, - DvrmConstraintKind::UnsignedSignN => 2, - DvrmConstraintKind::UnsignedSignR => 2, - DvrmConstraintKind::UnsignedSignD => 2, - DvrmConstraintKind::DivByZeroQ(_) => 2, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Creates all constraints for the DVRM table. -/// -/// Returns: (constraints, next_constraint_idx) -pub fn dvrm_constraints(constraint_idx_start: usize) -> (Vec, usize) { - let mut idx = constraint_idx_start; - let mut constraints = Vec::new(); - - // DVRM-A3: signed is bit - constraints.push(DvrmConstraint::new(DvrmConstraintKind::SignedIsBit, idx)); - idx += 1; - - // DVRM-C1: remainder sign matches numerator sign - constraints.push(DvrmConstraint::new( - DvrmConstraintKind::RemainderSignMatchesNumerator, - idx, - )); - idx += 1; - - // DVRM-C4: abs_r formula (×2) - for i in 0..2 { - constraints.push(DvrmConstraint::new(DvrmConstraintKind::AbsRFormula(i), idx)); - idx += 1; - } - - // DVRM-C6: abs_d formula (×2) - for i in 0..2 { - constraints.push(DvrmConstraint::new(DvrmConstraintKind::AbsDFormula(i), idx)); - idx += 1; - } - - // DVRM-C7: sign_q formula - constraints.push(DvrmConstraint::new(DvrmConstraintKind::SignQFormula, idx)); - idx += 1; - - // DVRM-C12.i: carry is bit (×4) - for i in 0..4 { - constraints.push(DvrmConstraint::new(DvrmConstraintKind::CarryIsBit(i), idx)); - idx += 1; - } - - // DVRM-C15: sign_n_sub_r is bit - constraints.push(DvrmConstraint::new(DvrmConstraintKind::SignNSubRIsBit, idx)); - idx += 1; - - // DVRM-C18b: unsigned sign_n = 0 - constraints.push(DvrmConstraint::new(DvrmConstraintKind::UnsignedSignN, idx)); - idx += 1; - - // DVRM-C19b: unsigned sign_r = 0 - constraints.push(DvrmConstraint::new(DvrmConstraintKind::UnsignedSignR, idx)); - idx += 1; - - // DVRM-C20b: unsigned sign_d = 0 - constraints.push(DvrmConstraint::new(DvrmConstraintKind::UnsignedSignD, idx)); - idx += 1; - - // DVRM-C16.i: div_by_zero implies q = all 1s (×4) - for i in 0..4 { - constraints.push(DvrmConstraint::new(DvrmConstraintKind::DivByZeroQ(i), idx)); - idx += 1; - } - - (constraints, idx) -} - // ========================================================================= // Single-body constraint set (ConstraintSet front-end) // ========================================================================= diff --git a/prover/src/tables/ec_scalar.rs b/prover/src/tables/ec_scalar.rs index 99d59374a..87040f2bb 100644 --- a/prover/src/tables/ec_scalar.rs +++ b/prover/src/tables/ec_scalar.rs @@ -16,15 +16,10 @@ //! //! `limb = Σ 2^i · limb_bits[i]` is virtual (a linear combination, never stored). -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; -use crate::constraints::templates::new_is_bit_constraints; // ========================================================================= // Column indices @@ -274,107 +269,6 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// `a · b = 0` or `a · (1 - b) = 0` (degree 2), used for the spec's implication -/// constraints (`limb_bits_i = 1 ⇒ μ = 1`, `last_limb ⇒ μ`, `last_limb ⇒ offset = 0`). -pub struct MulZeroConstraint { - pub a: usize, - pub b: usize, - /// when true, the second factor is `(1 - b)` instead of `b` - pub b_complement: bool, - pub constraint_idx: usize, -} - -impl TransitionConstraint for MulZeroConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let a = step.get_main_evaluation_element(0, self.a).clone(); - let b = step.get_main_evaluation_element(0, self.b).clone(); - if self.b_complement { - a * (FieldElement::::one() - b) - } else { - a * b - } - } -} - -/// Creates all EC_SCALAR transition constraints (20 total). -pub fn create_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - let mut constraints: Vec< - Box>, - > = Vec::with_capacity(20); - let mut idx = constraint_idx_start; - - // IS_BIT for mu, limb_bits[0..8], last_limb. - let mut bit_cols = vec![cols::MU]; - bit_cols.extend((0..8).map(cols::limb_bit)); - bit_cols.push(cols::LAST_LIMB); - let (bit_constraints, next) = new_is_bit_constraints(&bit_cols, idx); - for c in bit_constraints { - constraints.push(c.boxed()); - } - idx = next; - - // limb_bits[i] = 1 ⇒ mu = 1 : limb_bits[i] · (1 - mu) = 0 - for i in 0..8 { - constraints.push( - MulZeroConstraint { - a: cols::limb_bit(i), - b: cols::MU, - b_complement: true, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - } - - // last_limb = 1 ⇒ mu = 1 : last_limb · (1 - mu) = 0 - constraints.push( - MulZeroConstraint { - a: cols::LAST_LIMB, - b: cols::MU, - b_complement: true, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - - // last_limb = 1 ⇒ offset = 0 : last_limb · offset = 0 - constraints.push( - MulZeroConstraint { - a: cols::LAST_LIMB, - b: cols::OFFSET, - b_complement: false, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - - (constraints, idx) -} - // ========================================================================= // Single-body constraint set (ConstraintSet front-end) // ========================================================================= diff --git a/prover/src/tables/ecdas.rs b/prover/src/tables/ecdas.rs index 88321263a..b8d6ee31d 100644 --- a/prover/src/tables/ecdas.rs +++ b/prover/src/tables/ecdas.rs @@ -10,15 +10,10 @@ //! See `spec/src/ecdas.toml`. Constraints are **unconditional**; padding rows set the quotients //! to `r` and `op = 0`, which makes every relation hold with zero carries. -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; -use crate::constraints::templates::IsBitConstraint; use crate::tables::ecsm::ecdas_tuple; use ecsm::{EcdasStep, P_BYTES}; @@ -255,26 +250,7 @@ pub fn bus_interactions() -> Vec { out } -// ========================================================================= -// Constraints -// ========================================================================= - -fn p_byte(m: usize) -> FieldElement { - if m < 32 { - FieldElement::from(P_BYTES[m] as u64) - } else { - FieldElement::zero() - } -} - -fn r_byte(m: usize) -> FieldElement { - if m < 33 { - FieldElement::from(R_BYTES[m] as u64) - } else { - FieldElement::zero() - } -} - +/// Which convolution relation an ECDAS carry constraint enforces. #[derive(Clone, Copy)] pub enum Relation { Lambda, @@ -282,240 +258,6 @@ pub enum Relation { Yr, } -/// Unconditional convolution carry constraint at limb `i`: `2^8·c_i − c_{i-1} − S_i = 0`. -pub struct ConvCarry { - pub relation: Relation, - pub i: usize, - pub constraint_idx: usize, -} - -impl ConvCarry { - fn s_i(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let i = self.i; - let col = |c: usize| -> FieldElement { step.get_main_evaluation_element(0, c).clone() }; - // bytes (zero beyond the stored length) - let b = |base: usize, len: usize, j: usize| -> FieldElement { - if j < len { - col(base + j) - } else { - FieldElement::zero() - } - }; - let lam = |j: usize| b(cols::LAMBDA, 32, j); - let xg = |j: usize| b(cols::XG, 32, j); - let xa = |j: usize| b(cols::XA, 32, j); - let ya = |j: usize| b(cols::YA, 32, j); - let yg = |j: usize| b(cols::YG, 32, j); - let xr = |j: usize| b(cols::XR, 32, j); - let yr = |j: usize| b(cols::YR, 32, j); - let op = col(cols::OP); - let one = FieldElement::::one(); - - // r·P − q·P convolution (shared structure across all three relations). - let rq = |qbase: usize| -> FieldElement { - let mut s = FieldElement::::zero(); - for j in 0..=i { - s += (r_byte::(j) - b(qbase, 33, j)) * p_byte::(i - j); - } - s - }; - - match self.relation { - Relation::Lambda => { - // op·(Σ λ_j(xG-xA)_{i-j} + (yA_i - yG_i)) - let mut op_branch = ya(i) - yg(i); - for j in 0..=i { - op_branch += lam(j) * (xg(i - j) - xa(i - j)); - } - // (1-op)·Σ (2 λ_j yA_{i-j} - 3 xA_j xA_{i-j}) - let mut notop_branch = FieldElement::::zero(); - for j in 0..=i { - notop_branch = notop_branch - + FieldElement::::from(2u64) * lam(j) * ya(i - j) - - FieldElement::::from(3u64) * xa(j) * xa(i - j); - } - op.clone() * op_branch + (one - op) * notop_branch + rq(cols::Q0) - } - Relation::Xr => { - // Σ λ_j λ_{i-j} − xA_i − xG_i − xR_i − (1-op)(xA_i − xG_i) + rq - let mut s = FieldElement::::zero(); - for j in 0..=i { - s += lam(j) * lam(i - j); - } - s - xa(i) - xg(i) - xr(i) - (one - op) * (xa(i) - xg(i)) + rq(cols::Q1) - } - Relation::Yr => { - // Σ λ_j(xA-xR)_{i-j} − yA_i − yR_i + rq - let mut s = FieldElement::::zero(); - for j in 0..=i { - s += lam(j) * (xa(i - j) - xr(i - j)); - } - s - ya(i) - yr(i) + rq(cols::Q2) - } - } - } -} - -impl TransitionConstraint for ConvCarry { - fn degree(&self) -> usize { - match self.relation { - Relation::Lambda => 3, // op · (λ · Δx) - Relation::Xr | Relation::Yr => 2, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let c_base = match self.relation { - Relation::Lambda => cols::C0, - Relation::Xr => cols::C1, - Relation::Yr => cols::C2, - }; - let c_i = step.get_main_evaluation_element(0, c_base + self.i).clone(); - let c_prev = if self.i == 0 { - FieldElement::::zero() - } else { - step.get_main_evaluation_element(0, c_base + self.i - 1) - .clone() - }; - FieldElement::::from(256u64) * c_i - c_prev - self.s_i(step) - } -} - -/// `col = 0` (unconditional, degree 1). Used for the closing `c_63 = 0`. -pub struct ColIsZero { - pub col: usize, - pub constraint_idx: usize, -} - -impl TransitionConstraint for ColIsZero { - fn degree(&self) -> usize { - 1 - } - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - step.get_main_evaluation_element(0, self.col).clone() - } -} - -/// `a · b = 0` or `a · (1 - b) = 0` (degree 2). -pub struct MulZero { - pub a: usize, - pub b: usize, - pub b_complement: bool, - pub constraint_idx: usize, -} - -impl TransitionConstraint for MulZero { - fn degree(&self) -> usize { - 2 - } - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let a = step.get_main_evaluation_element(0, self.a).clone(); - let b = step.get_main_evaluation_element(0, self.b).clone(); - if self.b_complement { - a * (FieldElement::::one() - b) - } else { - a * b - } - } -} - -/// Creates all ECDAS transition constraints (200 total). -pub fn create_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - let mut constraints: Vec< - Box>, - > = Vec::new(); - let mut idx = constraint_idx_start; - - // IS_BIT on μ, op and next_op (the spec range-checks op: ecdas:c:range_op). - for col in [cols::MU, cols::OP, cols::NEXT_OP] { - constraints.push(IsBitConstraint::unconditional(col, idx).boxed()); - idx += 1; - } - - // op · next_op = 0 - constraints.push( - MulZero { - a: cols::OP, - b: cols::NEXT_OP, - b_complement: false, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - // next_op · (1 - mu) = 0 - constraints.push( - MulZero { - a: cols::NEXT_OP, - b: cols::MU, - b_complement: true, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - - // λ, xR, yR convolution carries + closings. - for (relation, c_base) in [ - (Relation::Lambda, cols::C0), - (Relation::Xr, cols::C1), - (Relation::Yr, cols::C2), - ] { - for i in 0..64 { - constraints.push( - ConvCarry { - relation, - i, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - } - constraints.push( - ColIsZero { - col: c_base + 63, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - } - - (constraints, idx) -} - // ========================================================================= // Single-body constraint set (ConstraintSet front-end) // ========================================================================= @@ -549,7 +291,7 @@ impl EcdasConstraints { } } - /// Byte `m` of `R` (zero beyond 33 bytes). Twin of [`r_byte`]. + /// Byte `m` of `R` (zero beyond 33 bytes). Twin of `r_byte`. fn r_byte_expr>( b: &B, m: usize, @@ -591,7 +333,7 @@ impl EcdasConstraints { s } - /// `S_i` for `relation` at limb `i` (twin of [`ConvCarry::s_i`]). + /// `S_i` for `relation` at limb `i` (twin of `ConvCarry::s_i`). fn s_i>( b: &B, relation: Relation, @@ -643,7 +385,7 @@ impl EcdasConstraints { } } - /// `256·c_i − c_{i-1} − S_i` (twin of [`ConvCarry::evaluate`]). + /// `256·c_i − c_{i-1} − S_i` (twin of `ConvCarry::evaluate`). fn conv_carry>( b: &B, relation: Relation, diff --git a/prover/src/tables/ecsm.rs b/prover/src/tables/ecsm.rs index a9ec75a44..f23e2c7d7 100644 --- a/prover/src/tables/ecsm.rs +++ b/prover/src/tables/ecsm.rs @@ -18,15 +18,11 @@ //! virtual-carry checks remain µ-gated as before. use executor::vm::instruction::execution::ECSM_SYSCALL_NUMBER; -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; -use crate::constraints::templates::{INV_SHIFT_32, IsBitConstraint}; +use crate::constraints::templates::INV_SHIFT_32; use ecsm::{B, EcsmWitness, N_BYTES, P_BYTES}; // Bias signed convolution carries into IsHalfword [0, 2^16); see spec ecsm.typ "Carry offset" (@ecsm-limb_carry). @@ -583,10 +579,6 @@ pub fn ecdas_tuple( v } -// ========================================================================= -// Constraints -// ========================================================================= - /// Which convolution relation a carry constraint enforces. #[derive(Clone, Copy)] pub enum Relation { @@ -596,126 +588,7 @@ pub enum Relation { Yg, } -fn p_byte(m: usize) -> FieldElement { - if m < 32 { - FieldElement::from(P_BYTES[m] as u64) - } else { - FieldElement::zero() - } -} - -/// Convolution carry constraint at limb `i`: `2^8·c_i − c_{i-1} − S_i = 0`, with `c_{-1} = 0`. -/// Unconditional (degree 2); the only µ-gated term is the curve constant `µ·b` inside `S_i` -/// for the yG relation at limb 0 (see [`ConvCarry::s_i`]). -pub struct ConvCarry { - pub relation: Relation, - pub i: usize, - pub constraint_idx: usize, -} - -impl ConvCarry { - fn s_i(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let i = self.i; - let col = |c: usize| -> FieldElement { step.get_main_evaluation_element(0, c).clone() }; - let byte = |base: usize, len: usize, j: usize| -> FieldElement { - if j < len { - col(base + j) - } else { - FieldElement::zero() - } - }; - let mut s = FieldElement::::zero(); - match self.relation { - Relation::X2 => { - // Σ xG_j·xG_{i-j} − x2_i − Σ q0_j·P_{i-j} - for j in 0..=i { - s += byte(cols::XG, 32, j) * byte(cols::XG, 32, i - j); - s = s - byte(cols::Q0, 32, j) * p_byte::(i - j); - } - s = s - byte(cols::X2, 32, i); - } - Relation::Yg => { - // Σ (yG_j·yG_{i-j} + P_j·P_{i-j} − x2_j·xG_{i-j} − q1_j·P_{i-j}) − b_i - for j in 0..=i { - s += byte(cols::YG, 32, j) * byte(cols::YG, 32, i - j); - s += p_byte::(j) * p_byte::(i - j); - s = s - byte(cols::X2, 32, j) * byte(cols::XG, 32, i - j); - s = s - byte(cols::Q1, 33, j) * p_byte::(i - j); - } - if i == 0 { - // Only the curve constant `b` is gated by `µ`: it vanishes on padding - // (µ=0) and equals `b` on real rows (µ=1). `B` is the zero-extension of - // `b`, so `B_i = 0` for i ≥ 1 — nothing to gate there. The rest of the - // relation stays unconditional. - let mu = step.get_main_evaluation_element(0, cols::MU).clone(); - s = s - mu * FieldElement::::from(B); - } - } - } - s - } -} - -impl TransitionConstraint for ConvCarry { - fn degree(&self) -> usize { - 2 // degree-2 convolution; the only µ-gated term (µ·b) is degree 1 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let c_base = match self.relation { - Relation::X2 => cols::C0, - Relation::Yg => cols::C1, - }; - let c_i = step.get_main_evaluation_element(0, c_base + self.i).clone(); - let c_prev = if self.i == 0 { - FieldElement::::zero() - } else { - step.get_main_evaluation_element(0, c_base + self.i - 1) - .clone() - }; - FieldElement::::from(256u64) * c_i - c_prev - self.s_i(step) - } -} - -/// `col = 0` (unconditional, degree 1). Used for the closing `c_63 = 0`. -pub struct ColIsZero { - pub col: usize, - pub constraint_idx: usize, -} - -impl TransitionConstraint for ColIsZero { - fn degree(&self) -> usize { - 1 - } - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - step.get_main_evaluation_element(0, self.col).clone() - } -} - -/// The two 256-bit addition-overflow checks (`k < N` and `xR < p`), whose 8 word-carries -/// `c` are virtual. Each `c_i = 2^-32·(addend0_i + addend1_i + c_{i-1} − sum_i)`. The addition -/// must overflow `2^256` (carry-out `c_7 = 1`), which proves the strict inequality: -/// `k < N` is `N + k_sub_N = k + 2^256` (with `k_sub_N = k − N mod 2^256`); `xR < p` is -/// `p + xR_sub_p = xR + 2^256` (with `xR_sub_p = xR − p mod 2^256`). +/// A range-check overflow addition: `p + xR_sub_p = xR + 2^256` (`k(kind: OverflowKind, step: &TableView) -> [FieldElement; 8] -where - F: IsSubFieldOf, - E: IsField, -{ - let inv = FieldElement::::from(INV_SHIFT_32); - let hl = kind.addend_hl_base(); - let bl = kind.sum_bl_base(); - let mut c: [FieldElement; 8] = std::array::from_fn(|_| FieldElement::zero()); - let mut prev = FieldElement::::zero(); - for (i, slot) in c.iter_mut().enumerate() { - // addend1 word i (from halfwords): hl[2i] + 2^16·hl[2i+1] - let addend1 = step.get_main_evaluation_element(0, hl + 2 * i).clone() - + step.get_main_evaluation_element(0, hl + 2 * i + 1).clone() - * FieldElement::::from(1u64 << 16); - // sum word i (from bytes): Σ bl[4i+b]·2^{8b} - let mut sum = FieldElement::::zero(); - for b in 0..4 { - sum += step.get_main_evaluation_element(0, bl + 4 * i + b).clone() - * FieldElement::::from(1u64 << (8 * b)); - } - let addend0 = FieldElement::::from(kind.const_word(i)); - let ci = (addend0 + addend1 + prev.clone() - sum) * inv.clone(); - *slot = ci.clone(); - prev = ci; - } - c -} - -/// `µ · c_i · (1 - c_i) = 0` for a virtual carry bit (degree 3, since `c_i` is linear). -pub struct CarryBit { - pub kind: OverflowKind, - pub i: usize, - pub constraint_idx: usize, -} - -impl TransitionConstraint for CarryBit { - fn degree(&self) -> usize { - 3 - } - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let c = carry_chain(self.kind, step); - let mu = step.get_main_evaluation_element(0, cols::MU).clone(); - let one = FieldElement::::one(); - mu * c[self.i].clone() * (one - c[self.i].clone()) - } -} - -/// `µ · (1 - c_7) = 0`: the top carry must be 1 (the addition overflows). -pub struct OverflowRequired { - pub kind: OverflowKind, - pub constraint_idx: usize, -} - -impl TransitionConstraint for OverflowRequired { - fn degree(&self) -> usize { - 2 - } - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let c = carry_chain(self.kind, step); - let mu = step.get_main_evaluation_element(0, cols::MU).clone(); - mu * (FieldElement::::one() - c[7].clone()) - } -} - -/// Creates all ECSM transition constraints (148 total). -pub fn create_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - let mut constraints: Vec< - Box>, - > = Vec::new(); - let mut idx = constraint_idx_start; - - // IS_BIT(mu) - constraints.push(IsBitConstraint::unconditional(cols::MU, idx).boxed()); - idx += 1; - - // x2 convolution: 64 carries + closing. - for i in 0..64 { - constraints.push( - ConvCarry { - relation: Relation::X2, - i, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - } - constraints.push( - ColIsZero { - col: cols::c0(63), - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - - // yG convolution: 64 carries + closing. - for i in 0..64 { - constraints.push( - ConvCarry { - relation: Relation::Yg, - i, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - } - constraints.push( - ColIsZero { - col: cols::c1(63), - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - - // IS_BIT(q1[32]) - constraints.push(IsBitConstraint::unconditional(cols::q1(32), idx).boxed()); - idx += 1; - - // k < N: 7 carry bits + overflow-required. - for i in 0..7 { - constraints.push( - CarryBit { - kind: OverflowKind::KLtN, - i, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - } - constraints.push( - OverflowRequired { - kind: OverflowKind::KLtN, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - - // xR < p: 7 carry bits + overflow-required. - for i in 0..7 { - constraints.push( - CarryBit { - kind: OverflowKind::XrLtP, - i, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - } - constraints.push( - OverflowRequired { - kind: OverflowKind::XrLtP, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; - - (constraints, idx) -} - // ========================================================================= // Single-body constraint set (ConstraintSet front-end) // ========================================================================= @@ -991,7 +677,7 @@ impl EcsmConstraints { } } - /// `S_i` for `relation` at limb `i` (twin of [`ConvCarry::s_i`]). + /// `S_i` for `relation` at limb `i` (twin of `ConvCarry::s_i`). fn s_i>( b: &B, relation: Relation, @@ -1027,7 +713,7 @@ impl EcsmConstraints { s } - /// `256·c_i − c_{i-1} − S_i` (twin of [`ConvCarry::evaluate`]). + /// `256·c_i − c_{i-1} − S_i` (twin of `ConvCarry::evaluate`). fn conv_carry>( b: &B, relation: Relation, diff --git a/prover/src/tables/eq.rs b/prover/src/tables/eq.rs index e6c467c48..945dfd094 100644 --- a/prover/src/tables/eq.rs +++ b/prover/src/tables/eq.rs @@ -21,19 +21,14 @@ //! four range-checked halves is `0` iff `diff == 0` iff `a == b`), and //! `res = eq XOR invert`. -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; use crate::constraints::templates::{ - AddConstraint, AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, - new_is_bit_constraints, + AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, }; // ========================================================================= @@ -249,93 +244,11 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// Enforces `res = eq XOR invert`, i.e. `res = eq + invert - 2*eq*invert`. -pub struct EqXorConstraint { - constraint_idx: usize, -} - -impl EqXorConstraint { - pub fn new(constraint_idx: usize) -> Self { - Self { constraint_idx } - } -} - -impl TransitionConstraint for EqXorConstraint { - fn degree(&self) -> usize { - 2 // eq * invert - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let res = step.get_main_evaluation_element(0, cols::RES).clone(); - let eq = step.get_main_evaluation_element(0, cols::EQ).clone(); - let invert = step.get_main_evaluation_element(0, cols::INVERT).clone(); - let two = FieldElement::::from(2u64); - // res - (eq + invert - 2*eq*invert) - res - (&eq + &invert - two * &eq * &invert) - } -} - -/// Creates all transition constraints for the EQ table. -/// -/// Returns the boxed constraints and the next available constraint index: -/// - `ADD` template pair enforcing `b + diff = a` (i.e. `diff = a - b`); -/// - `IS_BIT(invert)`; -/// - `res = eq XOR invert`. -pub fn eq_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - let mut idx = constraint_idx_start; - let mut constraints: Vec< - Box>, - > = Vec::new(); - - // diff = a - b, encoded as b + diff = a (unconditional). - let (add_lo, add_hi) = AddConstraint::new_pair( - vec![], - AddOperand::dword(cols::B_0), - AddOperand::from_dword_hl(cols::DIFF_0), - AddOperand::dword(cols::A_0), - idx, - ); - idx += 2; - constraints.push(add_lo.boxed()); - constraints.push(add_hi.boxed()); - - // IS_BIT(invert) - let (is_bit, next) = new_is_bit_constraints(&[cols::INVERT], idx); - idx = next; - for c in is_bit { - constraints.push(c.boxed()); - } - - // res = eq XOR invert - constraints.push(EqXorConstraint::new(idx).boxed()); - idx += 1; - - (constraints, idx) -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// The EQ table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`eq_constraints`] index-for-index: +/// The EQ table's transition constraints as a single [`ConstraintSet`]: /// - idx 0,1: `ADD` pair `b + diff = a` (unconditional); /// - idx 2: `IS_BIT(invert)` (unconditional); /// - idx 3: `res = eq XOR invert`. diff --git a/prover/src/tables/keccak.rs b/prover/src/tables/keccak.rs index f70fd0f02..77e71ed5e 100644 --- a/prover/src/tables/keccak.rs +++ b/prover/src/tables/keccak.rs @@ -16,17 +16,13 @@ //! | mu | 1 | Multiplicity flag | use executor::vm::instruction::execution::KECCAK_SYSCALL_NUMBER; -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::{AddConstraint, AddOperand, INV_SHIFT_32}; +use crate::constraints::templates::{AddOperand, INV_SHIFT_32}; // ========================================================================= // Column indices @@ -455,121 +451,12 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -struct KeccakAddressNoOverflowConstraint { - constraint_idx: usize, -} - -impl KeccakAddressNoOverflowConstraint { - fn new(constraint_idx: usize) -> Self { - Self { constraint_idx } - } - - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let addr_lo = step.get_main_evaluation_element(0, cols::addr(0)).clone() - + step.get_main_evaluation_element(0, cols::addr(1)) * FieldElement::::from(256) - + step.get_main_evaluation_element(0, cols::addr(2)) * FieldElement::::from(65536) - + step.get_main_evaluation_element(0, cols::addr(3)) - * FieldElement::::from(16777216); - let addr_hi = step.get_main_evaluation_element(0, cols::addr(4)).clone() - + step.get_main_evaluation_element(0, cols::addr(5)) * FieldElement::::from(256) - + step.get_main_evaluation_element(0, cols::addr(6)) * FieldElement::::from(65536) - + step.get_main_evaluation_element(0, cols::addr(7)) - * FieldElement::::from(16777216); - - let ptr_lo = step - .get_main_evaluation_element(0, cols::state_ptr(24, 0)) - .clone() - + step.get_main_evaluation_element(0, cols::state_ptr(24, 1)) - * FieldElement::::from(65536); - let ptr_hi = step - .get_main_evaluation_element(0, cols::state_ptr(24, 2)) - .clone() - + step.get_main_evaluation_element(0, cols::state_ptr(24, 3)) - * FieldElement::::from(65536); - - let inv_2_32 = FieldElement::::from(INV_SHIFT_32); - let carry_0 = (addr_lo + FieldElement::::from(192) - ptr_lo) * inv_2_32.clone(); - let carry_1 = (addr_hi + carry_0 - ptr_hi) * inv_2_32; - step.get_main_evaluation_element(0, cols::MU).clone() * carry_1 - } -} - -impl TransitionConstraint - for KeccakAddressNoOverflowConstraint -{ - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Create constraints for the KECCAK core chip. -/// -/// Per spec (keccak:c:state_ptr): ADD template for each lane: -/// state_ptr[lane] = addr + 8 * lane_idx -/// -/// 25 lane pointers × 2 constraints per ADD + 1 top-lane no-overflow -/// constraint = 51 constraints total. -/// Conditional on mu (only real rows). -pub fn create_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - let mut constraints: Vec< - Box>, - > = Vec::with_capacity(51); - let mut idx = constraint_idx_start; - - // state_ptr[lane] = addr + 8*lane_idx - // addr is DWordBL (8 bytes), state_ptr is DWordHL (4 halfwords) - // ADD: lhs = addr (DWordBL→DWordWL), rhs = 8*lane_idx (constant), sum = state_ptr (DWordHL→DWordWL) - for lane_idx in 0..25 { - let offset = (lane_idx * 8) as i64; - let (c0, c1) = AddConstraint::new_pair( - vec![cols::MU], // conditional on mu - AddOperand::from_dword_bl(cols::ADDR), - AddOperand::constant(offset), - AddOperand::from_dword_hl(cols::state_ptr(lane_idx, 0)), - idx, - ); - constraints.push(c0.boxed()); - constraints.push(c1.boxed()); - idx += 2; - } - - constraints.push(KeccakAddressNoOverflowConstraint::new(idx).boxed()); - idx += 1; - - (constraints, idx) -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= /// The KECCAK core table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`create_constraints`] index-for-index (51 constraints): +/// mirroring `create_constraints` index-for-index (51 constraints): /// - idx 0-49: for `lane_idx ∈ 0..25`, the `ADD` carry pair (gated on `μ`) /// enforcing `state_ptr[lane] = addr + 8·lane_idx` (`addr` DWordBL, /// `state_ptr` DWordHL); diff --git a/prover/src/tables/keccak_rnd.rs b/prover/src/tables/keccak_rnd.rs index fa65da3d3..f77cb373d 100644 --- a/prover/src/tables/keccak_rnd.rs +++ b/prover/src/tables/keccak_rnd.rs @@ -30,7 +30,6 @@ use executor::vm::instruction::execution::{KECCAK_RC, KECCAK_RHO}; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; @@ -897,49 +896,12 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// KECCAK_RND polynomial constraints: 20 IS_BIT(μ; Cxz_right) constraints. -/// -/// Per spec d75944ee, `Cxz_right` is typed `[Bit, 4], 5` and range-checked via -/// IS_BIT polynomial constraints (kind="template", cond="μ"), not lookups: -/// μ * Cxz_right[x][hw] * (1 - Cxz_right[x][hw]) = 0 -/// -/// - pi is a spec [[variables.virtual]] inlined in chi bus interactions. -/// - rnc/rbc are spec [[variables.constant]] inlined as compile-time constants. -/// -/// All other checks (XOR, AND, HWSL, ARE_BYTES, IS_HALF, KECCAK, KECCAK_RC) are -/// enforced via bus interactions against the BITWISE/KECCAK_RC chips. -pub fn create_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - use crate::constraints::templates::IsBitConstraint; - - let mut constraints: Vec< - Box>, - > = Vec::with_capacity(20); - let mut idx = constraint_idx_start; - for x in 0..5 { - for hw in 0..4 { - constraints - .push(IsBitConstraint::new(cols::MU, cols::cxz_right_bit(x, hw), idx).boxed()); - idx += 1; - } - } - (constraints, idx) -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= /// The KECCAK round table's transition constraints as a single -/// [`ConstraintSet`], mirroring [`create_constraints`] index-for-index (20 +/// [`ConstraintSet`], mirroring `create_constraints` index-for-index (20 /// constraints): for `x ∈ 0..5`, `hw ∈ 0..4` (idx `x·4 + hw`), the μ-gated /// `IS_BIT` on `Cxz_right[x][hw]` — `μ · Cxz_right·(1 − Cxz_right)`. pub struct KeccakRndConstraints; diff --git a/prover/src/tables/load.rs b/prover/src/tables/load.rs index cf83c6d39..b72b8bfdf 100644 --- a/prover/src/tables/load.rs +++ b/prover/src/tables/load.rs @@ -23,11 +23,7 @@ //! - Sender: MEMW (to read from memory) //! - Sender: MSB8 (for sign bit extraction) -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; @@ -475,169 +471,6 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// LOAD table constraint kinds. -#[derive(Debug, Clone, Copy)] -pub enum LoadConstraintKind { - /// (read2 + read4 + read8) => μ: if reading 2+ bytes, row must be active - ReadImpliesMu, - /// Extension constraint for res[i] when not reading those bytes - /// !read8 => res[i] = signed * sign_bit * 255 for i in 4..8 - ExtensionHigh(usize), - /// !read4 && !read8 => res[i] = signed * sign_bit * 255 for i in 2..4 - ExtensionMid(usize), - /// !read2 && !read4 && !read8 => res[1] = signed * sign_bit * 255 - ExtensionLow, - /// `IS_BIT`: `flag * (1 - flag) = 0` for a boolean flag used as a bus - /// multiplicity / extension selector (`load.toml` `signed`/`read2`/`read4`/ - /// `read8`). `usize` is the flag column. - FlagIsBit(usize), - /// `IS_BIT`: the width selector sum is boolean, so - /// `read1 = μ − sum` is well-formed (`load.toml:107-109`). - WidthSumIsBit, -} - -/// LOAD table constraint. -pub struct LoadConstraint { - constraint_idx: usize, - kind: LoadConstraintKind, -} - -impl LoadConstraint { - pub fn new(kind: LoadConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } - - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let ff = FieldElement::::from(255u64); // 0xFF for sign extension - - let mu = step.get_main_evaluation_element(0, cols::MU).clone(); - let read2 = step.get_main_evaluation_element(0, cols::READ2).clone(); - let read4 = step.get_main_evaluation_element(0, cols::READ4).clone(); - let read8 = step.get_main_evaluation_element(0, cols::READ8).clone(); - let signed = step.get_main_evaluation_element(0, cols::SIGNED).clone(); - let sign_bit = step.get_main_evaluation_element(0, cols::SIGN_BIT).clone(); - - match self.kind { - LoadConstraintKind::ReadImpliesMu => { - // (read2 + read4 + read8) * (1 - μ) = 0 - let read_sum = &read2 + &read4 + &read8; - &read_sum * (&one - &mu) - } - LoadConstraintKind::ExtensionHigh(i) => { - // (1 - read8) * (res[i] - signed * sign_bit * 255) = 0 - // i should be in 4..8 - let res_i = step.get_main_evaluation_element(0, cols::RES[i]).clone(); - let expected = &signed * &sign_bit * &ff; - (&one - &read8) * (&res_i - &expected) - } - LoadConstraintKind::ExtensionMid(i) => { - // (1 - read4 - read8) * (res[i] - signed * sign_bit * 255) = 0 - // i should be in 2..4 - let res_i = step.get_main_evaluation_element(0, cols::RES[i]).clone(); - let expected = &signed * &sign_bit * &ff; - (&one - &read4 - &read8) * (&res_i - &expected) - } - LoadConstraintKind::ExtensionLow => { - // (1 - read2 - read4 - read8) * (res[1] - signed * sign_bit * 255) = 0 - let res_1 = step.get_main_evaluation_element(0, cols::RES[1]).clone(); - let expected = &signed * &sign_bit * &ff; - (&one - &read2 - &read4 - &read8) * (&res_1 - &expected) - } - LoadConstraintKind::FlagIsBit(col) => { - // flag * (1 - flag) = 0 - let flag = step.get_main_evaluation_element(0, col).clone(); - &flag * (&one - &flag) - } - LoadConstraintKind::WidthSumIsBit => { - // sum * (1 - sum) = 0, sum = read2 + read4 + read8 - let sum = &read2 + &read4 + &read8; - &sum * (&one - &sum) - } - } - } -} - -impl TransitionConstraint for LoadConstraint { - fn degree(&self) -> usize { - match self.kind { - LoadConstraintKind::ReadImpliesMu => 2, - // Extension constraints: (1 - readX) * (res[i] - signed * sign_bit * 255) - // = degree 1 * (degree 1 - degree 2) = degree 3 - LoadConstraintKind::ExtensionHigh(_) => 3, - LoadConstraintKind::ExtensionMid(_) => 3, - LoadConstraintKind::ExtensionLow => 3, - // flag * (1 - flag) and sum * (1 - sum) - LoadConstraintKind::FlagIsBit(_) => 2, - LoadConstraintKind::WidthSumIsBit => 2, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Creates all constraints for the LOAD table. -pub fn constraints() --> Vec>> { - let mut constraints: Vec< - Box>, - > = Vec::new(); - - let mut idx = 0; - - // IS_BIT on the width/sign flags (used as bus multiplicities + extension - // selectors): signed, read2, read4, read8 (`load.toml` `all` group). - for flag_col in [cols::SIGNED, cols::READ2, cols::READ4, cols::READ8] { - constraints.push(LoadConstraint::new(LoadConstraintKind::FlagIsBit(flag_col), idx).boxed()); - idx += 1; - } - // IS_BIT on the width-selector sum (so read1 = μ − sum is well-formed). - constraints.push(LoadConstraint::new(LoadConstraintKind::WidthSumIsBit, idx).boxed()); - idx += 1; - - // (read2 + read4 + read8) => μ - constraints.push(LoadConstraint::new(LoadConstraintKind::ReadImpliesMu, idx).boxed()); - idx += 1; - - // Extension constraints for high bytes (4..8): !read8 => res[i] = extended - for i in 4..8 { - constraints.push(LoadConstraint::new(LoadConstraintKind::ExtensionHigh(i), idx).boxed()); - idx += 1; - } - - // Extension constraints for mid bytes (2..4): !(read4 + read8) => res[i] = extended - for i in 2..4 { - constraints.push(LoadConstraint::new(LoadConstraintKind::ExtensionMid(i), idx).boxed()); - idx += 1; - } - - // Extension constraint for low byte (1): !(read2 + read4 + read8) => res[1] = extended - constraints.push(LoadConstraint::new(LoadConstraintKind::ExtensionLow, idx).boxed()); - - constraints -} - // ========================================================================= // Single-body constraint set (ConstraintSet front-end) // ========================================================================= diff --git a/prover/src/tables/lt.rs b/prover/src/tables/lt.rs index c8351d5b0..9302e2224 100644 --- a/prover/src/tables/lt.rs +++ b/prover/src/tables/lt.rs @@ -26,11 +26,7 @@ //! - Receiver: ALU (all less-than lookups — CPU SLT/BLT/BGE dispatch and the //! internal `memw`/`memw_aligned`/`dvrm` timestamp / |r|<|d| checks) -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::TransitionConstraint; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use std::collections::HashMap; @@ -349,270 +345,12 @@ pub fn bus_interactions() -> Vec { ] } -// ========================================================================= -// Constraints -// ========================================================================= - -/// LT table constraint for virtual carry IS_BIT checks and the LT formula. -/// -/// This constraint embeds the virtual carry computations and verifies: -/// 1. IS_BIT and IS_BIT (carry values are 0 or 1) -/// 2. LT formula: lt = signed * (A*(1-B) + A*C + (1-B)*C) + (1-signed) * unsigned_lt -/// -/// Where A = lhs_msb, B = rhs_msb, C = carry[1], unsigned_lt = carry[1] -pub struct LtConstraint { - /// Unique constraint identifier - constraint_idx: usize, - /// Which constraint to check (0 = carry[0] IS_BIT, 1 = carry[1] IS_BIT, 2 = LT formula) - kind: LtConstraintKind, -} - -/// Kind of LT constraint. -#[derive(Debug, Clone, Copy)] -pub enum LtConstraintKind { - /// IS_BIT constraint on virtual carry[0] - Carry0IsBit, - /// IS_BIT constraint on virtual carry[1] - Carry1IsBit, - /// LT formula constraint - LtFormula, - /// `out = lt XOR invert`, i.e. `out - (lt + invert - 2*lt*invert) = 0` - /// (`lt.toml:159`). The ALU bus consumes `out`, while `LtFormula` only binds - /// `lt` — without this the `out` column (used for BGE/BGEU via `invert`) is - /// free and any comparison result can be forged. - OutXorInvert, - /// IS_BIT constraint on `invert` (`lt:c:range_invert`). - InvertIsBit, - /// IS_BIT constraint on `signed` (`lt:c:range_signed`). - SignedIsBit, -} - -impl LtConstraint { - /// Creates a new LT constraint. - pub fn new(kind: LtConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } - - /// Compute virtual carry[0] from the addition check. - /// - /// carry[0] = 2^(-32) * (rhs[0] + cast(lhs_sub_rhs, DWordWL)[0] - lhs[0]) - /// - /// Where cast(lhs_sub_rhs, DWordWL)[0] = lhs_sub_rhs[0] + 2^16 * lhs_sub_rhs[1] - fn compute_carry_0(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let lhs_0 = step.get_main_evaluation_element(0, cols::LHS_0).clone(); - let rhs_0 = step.get_main_evaluation_element(0, cols::RHS_0).clone(); - let sub_0 = step - .get_main_evaluation_element(0, cols::LHS_SUB_RHS_0) - .clone(); - let sub_1 = step - .get_main_evaluation_element(0, cols::LHS_SUB_RHS_1) - .clone(); - - // cast(lhs_sub_rhs, DWordWL)[0] = sub_0 + 2^16 * sub_1 - let shift_16 = FieldElement::::from(SHIFT_16); - let sub_lo = &sub_0 + &sub_1 * &shift_16; - - // carry[0] = (rhs[0] + sub_lo - lhs[0]) / 2^32 - let inv_2_32 = FieldElement::::from(crate::constraints::templates::INV_SHIFT_32); - (&rhs_0 + &sub_lo - &lhs_0) * &inv_2_32 - } - - /// Compute virtual carry[1] from the addition check. - /// - /// carry[1] = 2^(-32) * (cast(rhs, DWordWL)[1] + cast(lhs_sub_rhs, DWordWL)[1] + carry[0] - cast(lhs, DWordWL)[1]) - /// - /// Where: - /// - cast(rhs, DWordWL)[1] = rhs[1] + 2^16 * rhs[2] - /// - cast(lhs_sub_rhs, DWordWL)[1] = lhs_sub_rhs[2] + 2^16 * lhs_sub_rhs[3] - /// - cast(lhs, DWordWL)[1] = lhs[1] + 2^16 * lhs[2] - fn compute_carry_1(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let lhs_1 = step.get_main_evaluation_element(0, cols::LHS_1).clone(); - let lhs_2 = step.get_main_evaluation_element(0, cols::LHS_2).clone(); - let rhs_1 = step.get_main_evaluation_element(0, cols::RHS_1).clone(); - let rhs_2 = step.get_main_evaluation_element(0, cols::RHS_2).clone(); - let sub_2 = step - .get_main_evaluation_element(0, cols::LHS_SUB_RHS_2) - .clone(); - let sub_3 = step - .get_main_evaluation_element(0, cols::LHS_SUB_RHS_3) - .clone(); - - let shift_16 = FieldElement::::from(SHIFT_16); - - // cast(lhs, DWordWL)[1] = lhs[1] + 2^16 * lhs[2] - let lhs_hi = &lhs_1 + &lhs_2 * &shift_16; - - // cast(rhs, DWordWL)[1] = rhs[1] + 2^16 * rhs[2] - let rhs_hi = &rhs_1 + &rhs_2 * &shift_16; - - // cast(lhs_sub_rhs, DWordWL)[1] = sub_2 + 2^16 * sub_3 - let sub_hi = &sub_2 + &sub_3 * &shift_16; - - // carry[0] - let carry_0 = self.compute_carry_0(step); - - // carry[1] = (rhs_hi + sub_hi + carry_0 - lhs_hi) / 2^32 - let inv_2_32 = FieldElement::::from(crate::constraints::templates::INV_SHIFT_32); - (&rhs_hi + &sub_hi + &carry_0 - &lhs_hi) * &inv_2_32 - } - - /// Compute the constraint value. - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - - match self.kind { - LtConstraintKind::Carry0IsBit => { - // IS_BIT: carry[0] * (1 - carry[0]) = 0 - let c0 = self.compute_carry_0(step); - &c0 * (one - &c0) - } - LtConstraintKind::Carry1IsBit => { - // IS_BIT: carry[1] * (1 - carry[1]) = 0 - let c1 = self.compute_carry_1(step); - &c1 * (one - &c1) - } - LtConstraintKind::LtFormula => { - // LT formula: - // lt = signed * (A*(1-B) + A*C + (1-B)*C) + (1-signed) * unsigned_lt - // Where A = lhs_msb, B = rhs_msb, C = carry[1], unsigned_lt = carry[1] - let lt = step.get_main_evaluation_element(0, cols::LT).clone(); - let signed = step.get_main_evaluation_element(0, cols::SIGNED).clone(); - let a = step.get_main_evaluation_element(0, cols::LHS_MSB).clone(); - let b = step.get_main_evaluation_element(0, cols::RHS_MSB).clone(); - let c = self.compute_carry_1(step); - - // unsigned_lt = carry[1] - let unsigned_lt = c.clone(); - - // signed_lt = A*(1-B) + A*C + (1-B)*C - // = A - A*B + A*C + C - B*C - // = A*(1-B+C) + C*(1-B) - let one_minus_b = &one - &b; - let signed_lt = &a * &one_minus_b + &a * &c + &one_minus_b * &c; - - // lt = signed * signed_lt + (1 - signed) * unsigned_lt - let expected_lt = &signed * &signed_lt + (&one - &signed) * &unsigned_lt; - - // Constraint: lt - expected_lt = 0 - lt - expected_lt - } - LtConstraintKind::OutXorInvert => { - // out = lt XOR invert = lt + invert - 2*lt*invert - let out = step.get_main_evaluation_element(0, cols::OUT).clone(); - let lt = step.get_main_evaluation_element(0, cols::LT).clone(); - let invert = step.get_main_evaluation_element(0, cols::INVERT).clone(); - let two = FieldElement::::from(2u64); - out - (< + &invert - two * < * &invert) - } - LtConstraintKind::InvertIsBit => { - // invert * (1 - invert) = 0 - let invert = step.get_main_evaluation_element(0, cols::INVERT).clone(); - &invert * (one - &invert) - } - LtConstraintKind::SignedIsBit => { - // signed * (1 - signed) = 0 - let signed = step.get_main_evaluation_element(0, cols::SIGNED).clone(); - &signed * (one - &signed) - } - } - } -} - -impl TransitionConstraint for LtConstraint { - fn degree(&self) -> usize { - match self.kind { - // IS_BIT on virtual carry involves computing carry (degree 1) then X*(1-X) (degree 2) - LtConstraintKind::Carry0IsBit => 2, - LtConstraintKind::Carry1IsBit => 2, - // LT formula involves products like signed * A * (1-B) - LtConstraintKind::LtFormula => 3, - // out - (lt + invert - 2*lt*invert): the lt*invert product is degree 2 - LtConstraintKind::OutXorInvert => 2, - // X*(1-X) - LtConstraintKind::InvertIsBit => 2, - LtConstraintKind::SignedIsBit => 2, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Creates all constraints for the LT table. -/// -/// Returns: (constraints, next_constraint_idx) -pub fn lt_constraints(constraint_idx_start: usize) -> (Vec, usize) { - let mut idx = constraint_idx_start; - let constraints = vec![ - LtConstraint::new(LtConstraintKind::Carry0IsBit, { - let i = idx; - idx += 1; - i - }), - LtConstraint::new(LtConstraintKind::Carry1IsBit, { - let i = idx; - idx += 1; - i - }), - LtConstraint::new(LtConstraintKind::LtFormula, { - let i = idx; - idx += 1; - i - }), - // out = lt XOR invert (binds the ALU-bus-consumed `out` column). - LtConstraint::new(LtConstraintKind::OutXorInvert, { - let i = idx; - idx += 1; - i - }), - // Range-check the boolean flags that drive the formula / bus. - LtConstraint::new(LtConstraintKind::InvertIsBit, { - let i = idx; - idx += 1; - i - }), - LtConstraint::new(LtConstraintKind::SignedIsBit, { - let i = idx; - idx += 1; - i - }), - ]; - (constraints, idx) -} - // ========================================================================= // Single-body constraint set (ConstraintSet front-end) // ========================================================================= // -// Non-destructive twin of `LtConstraint` / `lt_constraints` above, written -// once against the generic `ConstraintBuilder` so one body serves the compiled -// prover folder, the verifier folder and IR capture. The old structs stay for -// now (they are the differential oracle); the final deletion phase removes -// them. Constraint indices 0..6 match `lt_constraints(0)` exactly. +// One body against the generic `ConstraintBuilder` serves the compiled prover +// folder, the verifier folder and IR capture. Constraint indices 0..6. use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; diff --git a/prover/src/tables/memw.rs b/prover/src/tables/memw.rs index 96e458e6c..722d5ef85 100644 --- a/prover/src/tables/memw.rs +++ b/prover/src/tables/memw.rs @@ -29,17 +29,13 @@ //! //! ## Constraints (11 total: 2 custom + 2 IS_BIT for multiplicities + 7 IS_BIT for carry) -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::{IsBitConstraint, emit_is_bit}; +use crate::constraints::templates::emit_is_bit; /// Maximum number of rows per MEMW table chunk. /// If operations exceed this, the trace is split into multiple tables. @@ -839,176 +835,22 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Virtual column computations -// ========================================================================= - -/// Compute virtual w2 = write2 + write4 + write8 -fn compute_w2(step: &TableView) -> FieldElement -where - F: IsSubFieldOf, - E: IsField, -{ - let write2 = step.get_main_evaluation_element(0, cols::WRITE2).clone(); - let write4 = step.get_main_evaluation_element(0, cols::WRITE4).clone(); - let write8 = step.get_main_evaluation_element(0, cols::WRITE8).clone(); - write2 + write4 + write8 -} - -/// Compute virtual μ_sum = μ_read + μ_write -fn compute_mu_sum(step: &TableView) -> FieldElement -where - F: IsSubFieldOf, - E: IsField, -{ - let mu_read = step.get_main_evaluation_element(0, cols::MU_READ).clone(); - let mu_write = step.get_main_evaluation_element(0, cols::MU_WRITE).clone(); - mu_read + mu_write -} - -// ========================================================================= -// Constraints (11 total: 2 custom + 2 IS_BIT for multiplicities + 7 IS_BIT for carry) -// ========================================================================= - -/// MEMW table constraint kinds. -#[derive(Debug, Clone, Copy)] -pub enum MemwConstraintKind { - /// IS_BIT<μ_sum>: multiplicity sum is 0 or 1 - MuSumIsBit, - /// w2 => μ_sum: if accessing 2+ bytes, must be active row - W2ImpliesMuSum, - /// IS_BIT: the width-sum is 0 or 1 (spec assumption). - WidthSumIsBit, -} - -/// MEMW table constraint. -pub struct MemwConstraint { - constraint_idx: usize, - kind: MemwConstraintKind, -} - -impl MemwConstraint { - pub fn new(kind: MemwConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } - - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - - match self.kind { - MemwConstraintKind::MuSumIsBit => { - let mu_sum = compute_mu_sum(step); - &mu_sum * (&one - &mu_sum) - } - MemwConstraintKind::W2ImpliesMuSum => { - let w2 = compute_w2(step); - let mu_sum = compute_mu_sum(step); - &w2 * (&one - &mu_sum) - } - MemwConstraintKind::WidthSumIsBit => { - let w2 = compute_w2(step); - &w2 * (&one - &w2) - } - } - } -} - -impl TransitionConstraint for MemwConstraint { - fn degree(&self) -> usize { - match self.kind { - MemwConstraintKind::MuSumIsBit => 2, - MemwConstraintKind::W2ImpliesMuSum => 2, - MemwConstraintKind::WidthSumIsBit => 2, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Creates all constraints for the MEMW table. -/// -/// 15 constraints total: -/// - IS_BIT<μ_sum> (1) -/// - w2 => μ_sum (1) -/// - IS_BIT<μ_read> (1) -/// - IS_BIT<μ_write> (1) -/// - IS_BIT for carry[0..6] (7) -/// - IS_BIT (3) + IS_BIT (1) [spec assumption] -pub fn constraints() --> Vec>> { - let mut constraints: Vec< - Box>, - > = Vec::new(); - - let mut idx = 0; - - // IS_BIT<μ_sum> - constraints.push(MemwConstraint::new(MemwConstraintKind::MuSumIsBit, idx).boxed()); - idx += 1; - - // w2 => μ_sum - constraints.push(MemwConstraint::new(MemwConstraintKind::W2ImpliesMuSum, idx).boxed()); - idx += 1; - - // IS_BIT<μ_read> - constraints.push(IsBitConstraint::unconditional(cols::MU_READ, idx).boxed()); - idx += 1; - - // IS_BIT<μ_write> - constraints.push(IsBitConstraint::unconditional(cols::MU_WRITE, idx).boxed()); - idx += 1; - - // IS_BIT for carry[0..6] - for &col in &cols::CARRY { - constraints.push(IsBitConstraint::unconditional(col, idx).boxed()); - idx += 1; - } - - // IS_BIT on the width flags + their sum (spec defense-in-depth assumption). - for &col in &[cols::WRITE2, cols::WRITE4, cols::WRITE8] { - constraints.push(IsBitConstraint::unconditional(col, idx).boxed()); - idx += 1; - } - constraints.push(MemwConstraint::new(MemwConstraintKind::WidthSumIsBit, idx).boxed()); - - constraints -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// `μ_sum = μ_read + μ_write` as a builder expression (twin of -/// [`compute_mu_sum`]). +/// `μ_sum = μ_read + μ_write` as a builder expression. fn mu_sum_expr>(b: &B) -> B::Expr { b.main(0, cols::MU_READ) + b.main(0, cols::MU_WRITE) } -/// `w2 = write2 + write4 + write8` as a builder expression (twin of -/// [`compute_w2`]). +/// `w2 = write2 + write4 + write8` as a builder expression. fn w2_expr>(b: &B) -> B::Expr { b.main(0, cols::WRITE2) + b.main(0, cols::WRITE4) + b.main(0, cols::WRITE8) } /// The MEMW table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`constraints`] index-for-index (15 constraints): +/// mirroring `constraints` index-for-index (15 constraints): /// - idx 0: `IS_BIT<μ_sum>`; /// - idx 1: `w2 ⇒ μ_sum` (`w2·(1 − μ_sum)`); /// - idx 2,3: `IS_BIT` on `μ_read`, `μ_write`; diff --git a/prover/src/tables/memw_aligned.rs b/prover/src/tables/memw_aligned.rs index 801deb353..acc3651ae 100644 --- a/prover/src/tables/memw_aligned.rs +++ b/prover/src/tables/memw_aligned.rs @@ -34,18 +34,14 @@ //! - IS_HALF[base_address[i]] for i ∈ [0, 1] //! - IS_WORD[base_address[2]] -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use super::memw::MemwOperation; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::{IsBitConstraint, emit_is_bit}; +use crate::constraints::templates::emit_is_bit; /// Maximum number of rows per MEMW_A table chunk. pub const MAX_ROWS: usize = super::max_rows::MEMW_A; @@ -649,98 +645,6 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints (4 total) -// ========================================================================= - -/// MEMW_A constraint kinds. -#[derive(Debug, Clone, Copy)] -pub enum MemwAlignedConstraintKind { - /// IS_BIT<μ_sum>: multiplicity sum is 0 or 1 - MuSumIsBit, - /// w2 => μ_sum: if accessing 2+ bytes, must be active row - W2ImpliesMuSum, - /// IS_BIT: the width-sum is 0 or 1 (spec assumption). - WidthSumIsBit, -} - -pub struct MemwAlignedConstraint { - constraint_idx: usize, - kind: MemwAlignedConstraintKind, -} - -impl MemwAlignedConstraint { - pub fn new(kind: MemwAlignedConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } - - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let mu_read = step.get_main_evaluation_element(0, cols::MU_READ).clone(); - let mu_write = step.get_main_evaluation_element(0, cols::MU_WRITE).clone(); - let mu_sum = &mu_read + &mu_write; - - match self.kind { - MemwAlignedConstraintKind::MuSumIsBit => &mu_sum * (&one - &mu_sum), - MemwAlignedConstraintKind::W2ImpliesMuSum => { - let write2 = step.get_main_evaluation_element(0, cols::WRITE2).clone(); - let write4 = step.get_main_evaluation_element(0, cols::WRITE4).clone(); - let write8 = step.get_main_evaluation_element(0, cols::WRITE8).clone(); - let w2 = write2 + write4 + write8; - &w2 * (&one - &mu_sum) - } - MemwAlignedConstraintKind::WidthSumIsBit => { - let write2 = step.get_main_evaluation_element(0, cols::WRITE2).clone(); - let write4 = step.get_main_evaluation_element(0, cols::WRITE4).clone(); - let write8 = step.get_main_evaluation_element(0, cols::WRITE8).clone(); - let w2 = write2 + write4 + write8; - &w2 * (&one - &w2) - } - } - } -} - -impl TransitionConstraint for MemwAlignedConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Creates all constraints for the MEMW_A table (8 total). The last four are the -/// spec's defense-in-depth width-flag assumptions. -pub fn constraints() --> Vec>> { - vec![ - MemwAlignedConstraint::new(MemwAlignedConstraintKind::MuSumIsBit, 0).boxed(), - MemwAlignedConstraint::new(MemwAlignedConstraintKind::W2ImpliesMuSum, 1).boxed(), - IsBitConstraint::unconditional(cols::MU_READ, 2).boxed(), - IsBitConstraint::unconditional(cols::MU_WRITE, 3).boxed(), - IsBitConstraint::unconditional(cols::WRITE2, 4).boxed(), - IsBitConstraint::unconditional(cols::WRITE4, 5).boxed(), - IsBitConstraint::unconditional(cols::WRITE8, 6).boxed(), - MemwAlignedConstraint::new(MemwAlignedConstraintKind::WidthSumIsBit, 7).boxed(), - ] -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= @@ -757,7 +661,7 @@ fn w2_expr>(b: &B) -> } /// The MEMW_A table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`constraints`] index-for-index (8 constraints): +/// mirroring `constraints` index-for-index (8 constraints): /// - idx 0: `IS_BIT<μ_sum>`; /// - idx 1: `w2 ⇒ μ_sum` (`w2·(1 − μ_sum)`); /// - idx 2,3: `IS_BIT` on `μ_read`, `μ_write`; diff --git a/prover/src/tables/memw_register.rs b/prover/src/tables/memw_register.rs index 59ba6d2be..fd6ad0ca3 100644 --- a/prover/src/tables/memw_register.rs +++ b/prover/src/tables/memw_register.rs @@ -38,11 +38,7 @@ //! - 4 Memory bus tokens (read-old + write-new, per word) //! - 2 MEMW output interactions (read + write, from CPU) -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; @@ -361,73 +357,12 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints (3 algebraic) -// ========================================================================= - -/// MEMW_R constraint: IS_BIT(mu_sum) = (mu_read + mu_write) * (1 - mu_read - mu_write) = 0 -pub struct MemwRegisterMuSumIsBit { - constraint_idx: usize, -} - -impl MemwRegisterMuSumIsBit { - pub fn new(constraint_idx: usize) -> Self { - Self { constraint_idx } - } - - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let mu_read = step.get_main_evaluation_element(0, cols::MU_READ).clone(); - let mu_write = step.get_main_evaluation_element(0, cols::MU_WRITE).clone(); - let mu_sum = &mu_read + &mu_write; - &mu_sum * (&one - &mu_sum) - } -} - -impl TransitionConstraint for MemwRegisterMuSumIsBit { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Creates all constraints for the MEMW_R table (3 total). -/// -/// - IS_BIT(MU_READ) -- unconditional -/// - IS_BIT(MU_WRITE) -- unconditional -/// - IS_BIT(mu_sum) = (mu_read + mu_write) * (1 - mu_read - mu_write) = 0 -pub fn constraints() --> Vec>> { - use crate::constraints::templates::IsBitConstraint; - - vec![ - IsBitConstraint::unconditional(cols::MU_READ, 0).boxed(), - IsBitConstraint::unconditional(cols::MU_WRITE, 1).boxed(), - MemwRegisterMuSumIsBit::new(2).boxed(), - ] -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= /// The MEMW_R table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`constraints`] index-for-index (3 constraints): +/// mirroring `constraints` index-for-index (3 constraints): /// - idx 0,1: `IS_BIT` on `μ_read`, `μ_write`; /// - idx 2: `IS_BIT<μ_sum>` with `μ_sum = μ_read + μ_write`. pub struct MemwRegisterConstraints; diff --git a/prover/src/tables/mul.rs b/prover/src/tables/mul.rs index 402154910..70adbedd6 100644 --- a/prover/src/tables/mul.rs +++ b/prover/src/tables/mul.rs @@ -30,11 +30,7 @@ //! - Receiver: ALU (×2 for lo and hi results — every MUL lookup, CPU //! MUL/MULH dispatch and dvrm's internal `d*q` consistency) -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::TransitionConstraint; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use std::collections::HashMap; @@ -683,228 +679,6 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// MUL table constraint kinds. -#[derive(Debug, Clone, Copy)] -pub enum MulConstraintKind { - /// SIGN constraint for lhs: (1 - lhs_signed) * lhs_is_negative = 0 - LhsSign, - /// SIGN constraint for rhs: (1 - rhs_signed) * rhs_is_negative = 0 - RhsSign, - /// IS_BIT range check on a sign flag column: `x * (1 - x) = 0`. Required - /// because `lhs_signed`/`rhs_signed` are used as bus multiplicities, so an - /// out-of-range value (e.g. `lhs_signed = 3`) would otherwise be accepted. - SignedIsBit(usize), - /// Raw product convolution formula for index i - RawProduct(usize), -} - -/// MUL table constraint. -pub struct MulConstraint { - constraint_idx: usize, - kind: MulConstraintKind, -} - -impl MulConstraint { - /// Create a new MUL constraint. - pub fn new(kind: MulConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } - - /// Compute the constraint value. - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - match self.kind { - MulConstraintKind::LhsSign => { - // (1 - lhs_signed) * lhs_is_negative = 0 - let lhs_signed = step - .get_main_evaluation_element(0, cols::LHS_SIGNED) - .clone(); - let lhs_is_neg = step - .get_main_evaluation_element(0, cols::LHS_IS_NEGATIVE) - .clone(); - let one = FieldElement::::one(); - (&one - &lhs_signed) * &lhs_is_neg - } - MulConstraintKind::RhsSign => { - // (1 - rhs_signed) * rhs_is_negative = 0 - let rhs_signed = step - .get_main_evaluation_element(0, cols::RHS_SIGNED) - .clone(); - let rhs_is_neg = step - .get_main_evaluation_element(0, cols::RHS_IS_NEGATIVE) - .clone(); - let one = FieldElement::::one(); - (&one - &rhs_signed) * &rhs_is_neg - } - MulConstraintKind::SignedIsBit(col) => { - // x * (1 - x) = 0 - let x = step.get_main_evaluation_element(0, col).clone(); - let one = FieldElement::::one(); - &x * &(&one - &x) - } - MulConstraintKind::RawProduct(i) => { - // raw_product[i] = convolution formula - // This requires computing the sign-extended values and convolution - self.compute_raw_product_constraint(i, step) - } - } - } - - /// Compute raw_product constraint for index i. - /// - /// raw_product[i] = Σ_k=0^1 2^(16k) × Σ_j=0^(2i+k) lhs_ext[j] × rhs_ext[2i+k-j] - fn compute_raw_product_constraint( - &self, - i: usize, - step: &TableView, - ) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - // Get lhs halfwords - let lhs: [FieldElement; 4] = [ - step.get_main_evaluation_element(0, cols::LHS_0).clone(), - step.get_main_evaluation_element(0, cols::LHS_1).clone(), - step.get_main_evaluation_element(0, cols::LHS_2).clone(), - step.get_main_evaluation_element(0, cols::LHS_3).clone(), - ]; - - // Get rhs halfwords - let rhs: [FieldElement; 4] = [ - step.get_main_evaluation_element(0, cols::RHS_0).clone(), - step.get_main_evaluation_element(0, cols::RHS_1).clone(), - step.get_main_evaluation_element(0, cols::RHS_2).clone(), - step.get_main_evaluation_element(0, cols::RHS_3).clone(), - ]; - - // Get sign bits - let lhs_is_neg = step - .get_main_evaluation_element(0, cols::LHS_IS_NEGATIVE) - .clone(); - let rhs_is_neg = step - .get_main_evaluation_element(0, cols::RHS_IS_NEGATIVE) - .clone(); - - // Build sign-extended values - let sign_fill = FieldElement::::from(SIGN_FILL); - let mut lhs_ext: [FieldElement; 8] = std::array::from_fn(|_| FieldElement::zero()); - let mut rhs_ext: [FieldElement; 8] = std::array::from_fn(|_| FieldElement::zero()); - - lhs_ext[..4].clone_from_slice(&lhs); - rhs_ext[..4].clone_from_slice(&rhs); - for j in 4..8 { - lhs_ext[j] = &sign_fill * &lhs_is_neg; - rhs_ext[j] = &sign_fill * &rhs_is_neg; - } - - // Compute convolution sum - let shift_16 = FieldElement::::from(SHIFT_16); - let mut sum = FieldElement::::zero(); - - for k in 0..=1u32 { - let idx = 2 * i + k as usize; - if idx < 8 { - let mut inner_sum = FieldElement::::zero(); - for j in 0..=idx { - if j < 8 && (idx - j) < 8 { - inner_sum = &inner_sum + &(&lhs_ext[j] * &rhs_ext[idx - j]); - } - } - // Multiply by 2^(16*k) - if k == 0 { - sum = &sum + &inner_sum; - } else { - sum = &sum + &(&inner_sum * &shift_16); - } - } - } - - // Constraint: raw_product[i] - sum = 0 - let raw_col = match i { - 0 => cols::RAW_PRODUCT_0, - 1 => cols::RAW_PRODUCT_1, - 2 => cols::RAW_PRODUCT_2, - 3 => cols::RAW_PRODUCT_3, - _ => unreachable!(), - }; - let raw_product = step.get_main_evaluation_element(0, raw_col).clone(); - - raw_product - sum - } -} - -impl TransitionConstraint for MulConstraint { - fn degree(&self) -> usize { - match self.kind { - // (1 - signed) * is_negative is degree 2 - MulConstraintKind::LhsSign | MulConstraintKind::RhsSign => 2, - // x * (1 - x) is degree 2 - MulConstraintKind::SignedIsBit(_) => 2, - // Raw product: lhs_ext[j] * rhs_ext[idx-j] where each may involve - // sign_fill * is_negative (degree 1), so product is degree 2 - // But we're summing many degree-2 terms, still degree 2 - MulConstraintKind::RawProduct(_) => 2, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Creates all constraints for the MUL table. -/// -/// Returns: (constraints, next_constraint_idx) -pub fn mul_constraints(constraint_idx_start: usize) -> (Vec, usize) { - let mut idx = constraint_idx_start; - let mut constraints = Vec::new(); - - // IS_BIT range checks on the sign flags (used as bus multiplicities). - constraints.push(MulConstraint::new( - MulConstraintKind::SignedIsBit(cols::LHS_SIGNED), - idx, - )); - idx += 1; - constraints.push(MulConstraint::new( - MulConstraintKind::SignedIsBit(cols::RHS_SIGNED), - idx, - )); - idx += 1; - - // SIGN constraints - constraints.push(MulConstraint::new(MulConstraintKind::LhsSign, idx)); - idx += 1; - constraints.push(MulConstraint::new(MulConstraintKind::RhsSign, idx)); - idx += 1; - - // Raw product constraints for i in 0..4 - for i in 0..4 { - constraints.push(MulConstraint::new(MulConstraintKind::RawProduct(i), idx)); - idx += 1; - } - - (constraints, idx) -} - // ========================================================================= // Single-body constraint set (ConstraintSet front-end) // ========================================================================= diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index 44b25ec68..dc8165555 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -17,11 +17,7 @@ //! - Senders: MSB16, BYTE_ALU[AND] (×3), ZERO, HWSL (×5), IS_HALFWORD (×4) //! - Receiver: SHIFT (from CPU) -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::TransitionConstraint; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, SHIFT_16, VmTable, alu_op}; @@ -728,266 +724,9 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// Polynomial constraint kinds for the SHIFT table. -#[derive(Debug, Clone, Copy)] -pub enum ShiftConstraintKind { - /// SHIFT-C13: direction * (1 - μ) = 0 - DirectionImpliesMu, - /// SHIFT-C5.i: zbs * (X[i] - in[i] * left) = 0 - ZbsOverrideX(usize), - /// SHIFT-C7: zbs * X[4] = 0 - ZbsOverrideX4, - /// SHIFT-C9.i: zbs * (Y[i] - in[i] * right) = 0 - ZbsOverrideY(usize), - /// SHIFT-C10.i: IS_BIT - LimbShiftIsBit(usize), - /// SHIFT-C12.i: out[i] - (shifted::DWordWL)[i] = 0 - OutputMatchesShifted(usize), - /// `IS_BIT`: `flag * (1 - flag) = 0` for a boolean flag used as a bus - /// multiplicity / shift selector (`shift:c:direction|signed|word_instr`). - /// `usize` is the flag column. - FlagIsBit(usize), -} - -pub struct ShiftConstraint { - constraint_idx: usize, - kind: ShiftConstraintKind, -} - -impl ShiftConstraint { - pub fn new(kind: ShiftConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } - - /// Compute the `shifted` virtual column at index `half_idx` (0..4). - fn compute_shifted_half(half_idx: usize, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let dir: FieldElement = step.get_main_evaluation_element(0, cols::DIRECTION).clone(); - let mu = step.get_main_evaluation_element(0, cols::MU).clone(); - let left = &mu - &dir; // μ - direction - let right = dir; - - // extension = 65535 * is_negative - let is_neg = step.get_main_evaluation_element(0, cols::IS_NEGATIVE); - let extension = is_neg * FieldElement::::from(65535u64); - - // Get X, Y, limb_shift, in columns - let get_x = |i: usize| step.get_main_evaluation_element(0, cols::X[i]).clone(); - let get_y = |i: usize| step.get_main_evaluation_element(0, cols::Y[i]).clone(); - let get_ls = |i: usize| -> FieldElement { - if i < 3 { - step.get_main_evaluation_element(0, cols::LIMB_SHIFT_RAW[i]) - .clone() - } else { - // limb_shift[3] is virtual: 1 - ls_raw[0] - ls_raw[1] - ls_raw[2] - FieldElement::::one() - - step.get_main_evaluation_element(0, cols::LIMB_SHIFT_RAW[0]) - - step.get_main_evaluation_element(0, cols::LIMB_SHIFT_RAW[1]) - - step.get_main_evaluation_element(0, cols::LIMB_SHIFT_RAW[2]) - } - }; - - // intra_limb_left[i]: X[0] for i=0, X[i]+Y[i-1] for i>0 - let intra_left = |i: usize| -> FieldElement { - if i == 0 { - get_x(0) - } else { - get_x(i) + get_y(i - 1) - } - }; - - // intra_limb_right[i]: Y[i]+X[i+1] - let intra_right = |i: usize| -> FieldElement { get_y(i) + get_x(i + 1) }; - - let i = half_idx; - let zero = FieldElement::::zero(); - - // left_part = left * Σ_j=0^i limb_shift[j] * intra_limb_left[i-j] - let mut left_part = zero.clone(); - for j in 0..=i { - left_part += &get_ls(j) * intra_left(i - j); - } - left_part = &left * left_part; - - // right_shift_part = right * Σ_j=0^(3-i) limb_shift[j] * intra_limb_right[i+j] - let mut right_shift_part = zero.clone(); - for j in 0..=(3 - i) { - right_shift_part += &get_ls(j) * intra_right(i + j); - } - - // right_ext_part = right * extension * Σ_j=(4-i)^3 limb_shift[j] - let mut ext_sum = zero.clone(); - if i < 4 { - for j in (4 - i)..4 { - ext_sum += get_ls(j); - } - } - let right_ext_part = &extension * ext_sum; - - let right_part = &right * (right_shift_part + right_ext_part); - - left_part + right_part - } - - fn compute(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let one = FieldElement::::one(); - let shift_16 = FieldElement::::from(SHIFT_16); - - match self.kind { - ShiftConstraintKind::DirectionImpliesMu => { - // direction * (1 - μ) = 0 - let dir = step.get_main_evaluation_element(0, cols::DIRECTION); - let mu = step.get_main_evaluation_element(0, cols::MU); - dir * (&one - mu) - } - ShiftConstraintKind::ZbsOverrideX(i) => { - // zbs * (X[i] - in[i] * left) = 0, where left = μ - direction - let zbs = step.get_main_evaluation_element(0, cols::ZBS); - let x_i = step.get_main_evaluation_element(0, cols::X[i]); - let in_i = step.get_main_evaluation_element(0, cols::IN[i]); - let mu = step.get_main_evaluation_element(0, cols::MU); - let dir = step.get_main_evaluation_element(0, cols::DIRECTION); - let left = mu - dir; - zbs * (x_i - in_i * &left) - } - ShiftConstraintKind::ZbsOverrideX4 => { - // zbs * X[4] = 0 - let zbs = step.get_main_evaluation_element(0, cols::ZBS); - let x4 = step.get_main_evaluation_element(0, cols::X_4); - zbs * x4 - } - ShiftConstraintKind::ZbsOverrideY(i) => { - // zbs * (Y[i] - in[i] * right) = 0 - let zbs = step.get_main_evaluation_element(0, cols::ZBS); - let y_i = step.get_main_evaluation_element(0, cols::Y[i]); - let in_i = step.get_main_evaluation_element(0, cols::IN[i]); - let dir = step.get_main_evaluation_element(0, cols::DIRECTION); - zbs * (y_i - in_i * dir) - } - ShiftConstraintKind::LimbShiftIsBit(i) => { - // limb_shift[i] * (1 - limb_shift[i]) = 0 - // limb_shift[3] is virtual: 1 - ls_raw[0] - ls_raw[1] - ls_raw[2] - let ls = if i < 3 { - step.get_main_evaluation_element(0, cols::LIMB_SHIFT_RAW[i]) - .clone() - } else { - one.clone() - - step.get_main_evaluation_element(0, cols::LIMB_SHIFT_RAW[0]) - - step.get_main_evaluation_element(0, cols::LIMB_SHIFT_RAW[1]) - - step.get_main_evaluation_element(0, cols::LIMB_SHIFT_RAW[2]) - }; - &ls * (&one - &ls) - } - ShiftConstraintKind::OutputMatchesShifted(i) => { - // C12.i: out[i] - (shifted::DWordWL)[i] = 0 - // (shifted::DWordWL)[i] = shifted[2*i] + shifted[2*i+1] * 2^16 - let out_col = if i == 0 { cols::OUT_0 } else { cols::OUT_1 }; - let out = step.get_main_evaluation_element(0, out_col).clone(); - let half_lo = Self::compute_shifted_half(2 * i, step); - let half_hi = Self::compute_shifted_half(2 * i + 1, step); - out - half_lo - half_hi * shift_16 - } - ShiftConstraintKind::FlagIsBit(col) => { - // flag * (1 - flag) = 0 - let flag = step.get_main_evaluation_element(0, col).clone(); - let one = FieldElement::::one(); - &flag * (one - &flag) - } - } - } -} - -impl TransitionConstraint for ShiftConstraint { - fn degree(&self) -> usize { - match self.kind { - ShiftConstraintKind::DirectionImpliesMu => 2, - ShiftConstraintKind::ZbsOverrideX(_) => 3, // zbs * (X - in * left), left = 1 - dir - ShiftConstraintKind::ZbsOverrideX4 => 2, - ShiftConstraintKind::ZbsOverrideY(_) => 3, // zbs * (Y - in * dir) - ShiftConstraintKind::LimbShiftIsBit(_) => 2, - ShiftConstraintKind::OutputMatchesShifted(_) => 3, // out - left*ls*intra (degree 3) - ShiftConstraintKind::FlagIsBit(_) => 2, - } - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} - -/// Number of polynomial constraints in the SHIFT table. -// 1 (DirectionImpliesMu) + 4 (ZbsOverrideX) + 1 (ZbsOverrideX4) + 4 (ZbsOverrideY) -// + 4 (LimbShiftIsBit) + 2 (OutputMatchesShifted) + 3 (FlagIsBit) = 19 +/// Total number of SHIFT transition constraints. pub const NUM_SHIFT_CONSTRAINTS: usize = 19; -/// Creates all polynomial constraints for the SHIFT table. -pub fn shift_constraints(constraint_idx_start: usize) -> (Vec, usize) { - let mut idx = constraint_idx_start; - let mut constraints = Vec::with_capacity(NUM_SHIFT_CONSTRAINTS); - - let mut push = |kind| { - constraints.push(ShiftConstraint::new(kind, idx)); - idx += 1; - }; - - // C13: direction * (1 - μ) = 0 - push(ShiftConstraintKind::DirectionImpliesMu); - - // C5.i: zbs * (X[i] - in[i] * left) = 0 - for i in 0..4 { - push(ShiftConstraintKind::ZbsOverrideX(i)); - } - - // C7: zbs * X[4] = 0 - push(ShiftConstraintKind::ZbsOverrideX4); - - // C9.i: zbs * (Y[i] - in[i] * right) = 0 - for i in 0..4 { - push(ShiftConstraintKind::ZbsOverrideY(i)); - } - - // C10.i: IS_BIT - for i in 0..4 { - push(ShiftConstraintKind::LimbShiftIsBit(i)); - } - - // C12.i: out[i] - (shifted::DWordWL)[i] = 0 - for i in 0..2 { - push(ShiftConstraintKind::OutputMatchesShifted(i)); - } - - // IS_BIT[direction|signed|word_instr] (shift.toml `range` group): these flags - // drive bus multiplicities / shift selectors, so they must be boolean. - for flag_col in [cols::DIRECTION, cols::SIGNED, cols::WORD_INSTR] { - push(ShiftConstraintKind::FlagIsBit(flag_col)); - } - - debug_assert_eq!(constraints.len(), NUM_SHIFT_CONSTRAINTS); - (constraints, idx) -} - // ========================================================================= // Single-body constraint set (ConstraintSet front-end) // ========================================================================= diff --git a/prover/src/tables/store.rs b/prover/src/tables/store.rs index fef2fd223..93ba98332 100644 --- a/prover/src/tables/store.rs +++ b/prover/src/tables/store.rs @@ -19,17 +19,13 @@ //! - `value`: DWordBL (8 bytes) — value to store //! - `μ`: multiplicity -use math::field::element::FieldElement; -use math::field::traits::{IsField, IsSubFieldOf}; -use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; -use crate::constraints::templates::{emit_is_bit, is_bit_meta, new_is_bit_constraints}; +use crate::constraints::templates::{emit_is_bit, is_bit_meta}; // ========================================================================= // Column indices for STORE table @@ -254,96 +250,12 @@ pub fn bus_interactions() -> Vec { interactions } -// ========================================================================= -// Constraints -// ========================================================================= - -/// Width-flag constraints for the STORE table. -pub struct StoreConstraint { - constraint_idx: usize, - kind: StoreConstraintKind, -} - -#[derive(Debug, Clone, Copy)] -pub enum StoreConstraintKind { - /// `write2 + write4 + write8 ∈ {0, 1}` (at most one width bit set). - WidthSumIsBit, - /// `(write2 + write4 + write8) ⇒ μ`, i.e. `(Σ width)·(1 − μ) = 0`. - WidthImpliesMu, -} - -impl StoreConstraint { - pub fn new(kind: StoreConstraintKind, constraint_idx: usize) -> Self { - Self { - constraint_idx, - kind, - } - } -} - -impl TransitionConstraint for StoreConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } - - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - let w2 = step.get_main_evaluation_element(0, cols::WRITE2).clone(); - let w4 = step.get_main_evaluation_element(0, cols::WRITE4).clone(); - let w8 = step.get_main_evaluation_element(0, cols::WRITE8).clone(); - let sum = &w2 + &w4 + &w8; - let one = FieldElement::::one(); - match self.kind { - StoreConstraintKind::WidthSumIsBit => &sum * (&one - &sum), - StoreConstraintKind::WidthImpliesMu => { - let mu = step.get_main_evaluation_element(0, cols::MU).clone(); - &sum * (&one - &mu) - } - } - } -} - -/// Creates all transition constraints for the STORE table: `IS_BIT` on each -/// width flag, the width-sum-is-bit constraint, and width ⇒ μ. -pub fn store_constraints( - constraint_idx_start: usize, -) -> ( - Vec>>, - usize, -) { - let mut constraints: Vec< - Box>, - > = Vec::new(); - - let (is_bit, mut idx) = new_is_bit_constraints( - &[cols::WRITE2, cols::WRITE4, cols::WRITE8, cols::MU], - constraint_idx_start, - ); - for c in is_bit { - constraints.push(c.boxed()); - } - - constraints.push(StoreConstraint::new(StoreConstraintKind::WidthSumIsBit, idx).boxed()); - idx += 1; - constraints.push(StoreConstraint::new(StoreConstraintKind::WidthImpliesMu, idx).boxed()); - idx += 1; - - (constraints, idx) -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= /// The STORE table's transition constraints as a single [`ConstraintSet`], -/// mirroring [`store_constraints`] index-for-index: +/// mirroring `store_constraints` index-for-index: /// - idx 0-3: `IS_BIT` on `write2`, `write4`, `write8`, `μ` (unconditional); /// - idx 4: `(Σ width)·(1 − Σ width) = 0` (width sum is a bit); /// - idx 5: `(Σ width)·(1 − μ) = 0` (width ⇒ μ). diff --git a/prover/src/tests/branch_constraints_tests.rs b/prover/src/tests/branch_constraints_tests.rs index af0b3aadb..950b21cb9 100644 --- a/prover/src/tests/branch_constraints_tests.rs +++ b/prover/src/tests/branch_constraints_tests.rs @@ -5,50 +5,27 @@ //! - Carry computation validity //! - Sign extension handling -use crate::tables::branch::{BranchOperation, branch_constraints, compute_carries}; +use crate::tables::branch::{BranchConstraints, BranchOperation, compute_carries}; use crate::tables::types::FE; -use stark::constraints::transition::TransitionConstraint; +use stark::constraints::builder::ConstraintSet; // ========================================================================= // Basic Constraint Property Tests // ========================================================================= #[test] -fn test_branch_constraint_degree() { - let (constraints, _) = branch_constraints(0); - - // The 4 conditional carry IS_BIT constraints have degree 3: - // cond (degree 1) * carry (degree 1) * (1 - carry) (degree 1) - // and the IS_BIT constraint has degree 2: JALR * (1 - JALR). - for c in &constraints[..4] { - assert_eq!(c.degree(), 3); +fn test_branch_constraint_set_meta() { + // 5 constraints: 4 conditional carry IS_BIT (degree 3) + IS_BIT (degree 2), + // dense and idx-ordered. + let meta = BranchConstraints.meta(); + assert_eq!(meta.len(), 5); + for (i, m) in meta.iter().enumerate() { + assert_eq!(m.constraint_idx, i); } - assert_eq!(constraints[4].degree(), 2); -} - -#[test] -fn test_branch_constraint_indices_unique() { - let (constraints, next_idx) = branch_constraints(0); - - assert_eq!(constraints.len(), 5); - assert_eq!(constraints[0].constraint_idx(), 0); - assert_eq!(constraints[1].constraint_idx(), 1); - assert_eq!(constraints[2].constraint_idx(), 2); - assert_eq!(constraints[3].constraint_idx(), 3); - assert_eq!(constraints[4].constraint_idx(), 4); - assert_eq!(next_idx, 5); -} - -#[test] -fn test_branch_constraint_indices_with_offset() { - let (constraints, next_idx) = branch_constraints(10); - - assert_eq!(constraints[0].constraint_idx(), 10); - assert_eq!(constraints[1].constraint_idx(), 11); - assert_eq!(constraints[2].constraint_idx(), 12); - assert_eq!(constraints[3].constraint_idx(), 13); - assert_eq!(constraints[4].constraint_idx(), 14); - assert_eq!(next_idx, 15); + for m in &meta[..4] { + assert_eq!(m.degree, 3); + } + assert_eq!(meta[4].degree, 2); } // ========================================================================= diff --git a/prover/src/tests/commit_tests.rs b/prover/src/tests/commit_tests.rs index f23405b0e..876bdc04b 100644 --- a/prover/src/tests/commit_tests.rs +++ b/prover/src/tests/commit_tests.rs @@ -434,27 +434,13 @@ fn test_bus_interactions_count() { #[test] fn test_constraints_count_and_indices() { - use crate::tables::commit::create_constraints; - let (constraints, next_idx) = create_constraints(0); - assert_eq!(constraints.len(), 8); - assert_eq!(next_idx, 8); - - // Verify sequential indices - for (i, c) in constraints.iter().enumerate() { - assert_eq!(c.constraint_idx(), i); + use crate::tables::commit::CommitConstraints; + use stark::constraints::builder::ConstraintSet; + let meta = CommitConstraints.meta(); + assert_eq!(meta.len(), 8); + // Dense, idx-ordered, all degree 2 (unconditional). + for (i, m) in meta.iter().enumerate() { + assert_eq!(m.constraint_idx, i); + assert_eq!(m.degree, 2); } - - // All degree 2 (unconditional) - for c in &constraints { - assert_eq!(c.degree(), 2); - } -} - -#[test] -fn test_constraints_with_offset() { - use crate::tables::commit::create_constraints; - let (constraints, next_idx) = create_constraints(10); - assert_eq!(next_idx, 18); - assert_eq!(constraints[0].constraint_idx(), 10); - assert_eq!(constraints[7].constraint_idx(), 17); } diff --git a/prover/src/tests/constraint_emit_tests.rs b/prover/src/tests/constraint_emit_tests.rs index db2d7aa88..b1f7cf133 100644 --- a/prover/src/tests/constraint_emit_tests.rs +++ b/prover/src/tests/constraint_emit_tests.rs @@ -1,19 +1,13 @@ -//! Differential tests for the single-body `emit_*` constraint functions. +//! Folder-vs-capture-interpret regression tests for the single-body `emit_*` +//! constraint functions in `constraints::{templates, cpu}`. //! -//! Each `emit_*` function in `constraints::{templates, cpu}` is checked -//! against the OLD boxed constraint struct it transcribes (the structs stay -//! in-branch as this oracle until the final deletion phase), on -//! [`TRIALS`] random rows — off-trace points, where a weakened or slipped -//! transcription diverges with overwhelming probability: -//! -//! 1. `ProverEvalFolder` output == old `evaluate::`; -//! 2. `VerifierEvalFolder` output == old `evaluate::` (embedded); -//! 3. `CaptureBuilder` → flatten → interpret == old `evaluate::`; -//! -//! plus per constraint: tree-measured degree == declared `meta.degree` == -//! old `degree()`, and `*_meta` zerofier parameters == the old struct's -//! `period`/`offset`/`exemptions_period`/`periodic_exemptions_offset`/ -//! `end_exemptions`. +//! Each `emit_*` body is run three ways — the `ProverEvalFolder` (base), the +//! `VerifierEvalFolder` (extension), and the `CaptureBuilder` → flat IR → +//! `eval_program_base` interpreter — and asserted to agree on [`TRIALS`] random +//! off-trace rows. All three derive from the ONE body, so this pins that +//! capture/interpretation stays faithful to the compiled folder. Per constraint +//! we also assert the meta invariants (dense, idx-ordered, all-base) and that +//! the tree-measured degree equals the declared `meta.degree`. use math::field::element::FieldElement; use stark::constraint_ir::eval_program_base; @@ -21,26 +15,21 @@ use stark::constraints::builder::{ CaptureBuilder, ConstraintBuilder, ConstraintMeta, ProverEvalFolder, RootKind, VerifierEvalFolder, num_base_from_meta, }; -use stark::constraints::transition::TransitionConstraint; use stark::frame::Frame; use stark::lookup::PackingShifts; use stark::table::TableView; use stark::traits::TransitionEvaluationContext; -use crate::constraints::cpu::{ - Arg2Constraint, Arg2ExclusiveConstraint, BranchCondConstraint, BranchRvdConstraint, - MemFlagsBitConstraint, NextPcAddConstraint, ProductZeroConstraint, RegNotReadIsZeroConstraint, - RvdEqResConstraint, emit_arg2, emit_arg2_exclusive, emit_branch_cond, emit_branch_rvd_pair, - emit_mem_flags_bit, emit_next_pc_add_pair, emit_product_zero, emit_reg_not_read_is_zero, - emit_rvd_eq_res, -}; use crate::constraints::cpu::{ arg2_exclusive_meta, arg2_meta, branch_cond_meta, branch_rvd_meta, mem_flags_bit_meta, next_pc_add_meta, product_zero_meta, reg_not_read_is_zero_meta, rvd_eq_res_meta, }; +use crate::constraints::cpu::{ + emit_arg2, emit_arg2_exclusive, emit_branch_cond, emit_branch_rvd_pair, emit_mem_flags_bit, + emit_next_pc_add_pair, emit_product_zero, emit_reg_not_read_is_zero, emit_rvd_eq_res, +}; use crate::constraints::templates::{ - AddConstraint, AddLinearTerm, AddOperand, IsBitConstraint, add_pair_meta, emit_add_pair, - emit_is_bit, is_bit_meta, + AddLinearTerm, AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, }; use crate::tables::cpu::cols; use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; @@ -84,70 +73,24 @@ macro_rules! emit_body { }; } -/// Old-struct evaluator at the prover instantiation ``. -type OldEval<'a> = &'a dyn Fn(&TableView) -> FE; -/// Old-struct evaluator at the verifier instantiation ``. -type OldEvalExt<'a> = &'a dyn Fn(&TableView) -> Fp3; - -/// Zerofier/degree parameters read off an old constraint struct. -struct OldParams { - degree: usize, - period: usize, - offset: usize, - exemptions_period: Option, - periodic_exemptions_offset: Option, - end_exemptions: usize, -} - -fn old_params>(c: &T) -> OldParams { - OldParams { - degree: c.degree(), - period: c.period(), - offset: c.offset(), - exemptions_period: c.exemptions_period(), - periodic_exemptions_offset: c.periodic_exemptions_offset(), - end_exemptions: c.end_exemptions(), - } -} - -/// The full differential check for one emit body against its old structs. -fn check_emit_vs_old( - label: &str, - body: &T, - meta: &[ConstraintMeta], - olds: &[OldEval<'_>], - olds_ext: &[OldEvalExt<'_>], - old_params: &[OldParams], -) { +/// Folder-vs-capture-interpret check for one emit body. The body is run three +/// ways (prover folder, verifier folder, captured-IR interpreter) and asserted +/// to agree on random off-trace rows — all derive from the ONE emit body, so +/// this pins that capture/interpretation stays faithful to the compiled folder. +/// `meta` must be dense, idx-ordered, all-base, with each declared degree equal +/// to the tree-measured degree. +fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { let n = body.n(); assert_eq!(meta.len(), n, "[{label}] meta length"); - assert_eq!(olds.len(), n); - assert_eq!(olds_ext.len(), n); - assert_eq!(old_params.len(), n); - // --- meta parity vs the old structs --- + // --- meta invariants --- assert_eq!(num_base_from_meta(meta), n, "[{label}] all-base num_base"); - for (i, (m, p)) in meta.iter().zip(old_params.iter()).enumerate() { + for (i, m) in meta.iter().enumerate() { assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); - assert_eq!(m.degree, p.degree, "[{label}] degree {i}"); - assert_eq!(m.period, p.period, "[{label}] period {i}"); - assert_eq!(m.offset, p.offset, "[{label}] offset {i}"); - assert_eq!( - m.exemptions_period, p.exemptions_period, - "[{label}] exemptions_period {i}" - ); - assert_eq!( - m.periodic_exemptions_offset, p.periodic_exemptions_offset, - "[{label}] periodic_exemptions_offset {i}" - ); - assert_eq!( - m.end_exemptions, p.end_exemptions, - "[{label}] end_exemptions {i}" - ); } - // --- capture once; tree-measured degree == declared == old --- + // --- capture once; tree-measured degree == declared --- let mut cb = CaptureBuilder::::new(); body.eval(&mut cb); let (prog, degrees) = cb.finish(n); @@ -172,10 +115,7 @@ fn check_emit_vs_old( let row: Vec = (0..NUM_COLS).map(|_| FE::from(rng.next_u64())).collect(); let row_e: Vec = row.iter().map(|x| x.to_extension()).collect(); - let step: TableView = TableView::new(vec![row.clone()], vec![Vec::new()]); - let step_e: TableView = TableView::new(vec![row_e.clone()], vec![Vec::new()]); - - // --- 1. ProverEvalFolder == old evaluate:: --- + // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); let ctx = TransitionEvaluationContext::new_prover( &frame, @@ -190,15 +130,8 @@ fn check_emit_vs_old( let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); body.eval(&mut folder); folder.assert_all_emitted(); - for (i, old) in olds.iter().enumerate() { - assert_eq!( - base_out[i], - old(&step), - "[{label}] prover folder mismatch, constraint {i}, trial {trial}" - ); - } - // --- 2. VerifierEvalFolder == old evaluate:: --- + // --- VerifierEvalFolder (ext) --- let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( @@ -213,19 +146,17 @@ fn check_emit_vs_old( let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); body.eval(&mut vfolder); vfolder.assert_all_emitted(); - for (i, old) in olds_ext.iter().enumerate() { + + // Prover folder (promoted) == verifier folder == interpreter. + for i in 0..n { assert_eq!( + base_out[i].to_extension(), vext_out[i], - old(&step_e), - "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" + "[{label}] prover-vs-verifier folder mismatch, constraint {i}, trial {trial}" ); - } - - // --- 3. capture → flatten → interpret == old evaluate --- - for (i, old) in olds.iter().enumerate() { assert_eq!( eval_program_base(&prog, i, &row), - old(&step), + base_out[i], "[{label}] interpreter mismatch, constraint {i}, trial {trial}" ); } @@ -237,61 +168,25 @@ fn check_emit_vs_old( // ============================================================================= #[test] -fn emit_is_bit_matches_old() { +fn emit_is_bit_folder_capture_agree() { emit_body!(Uncond, 1, |b| { emit_is_bit(b, 0, 7, None) }); - let old = IsBitConstraint::unconditional(7, 0); - check_emit_vs_old( - "is_bit_unconditional", - &Uncond, - &[is_bit_meta(0, false)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], - ); + check_emit("is_bit_unconditional", &Uncond, &[is_bit_meta(0, false)]); emit_body!(Cond, 1, |b| { emit_is_bit(b, 0, 5, Some(3)) }); - let old = IsBitConstraint::new(3, 5, 0); - check_emit_vs_old( - "is_bit_conditional", - &Cond, - &[is_bit_meta(0, true)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], - ); + check_emit("is_bit_conditional", &Cond, &[is_bit_meta(0, true)]); } // ============================================================================= // templates.rs: ADD pair // ============================================================================= -/// Run the pair check for one (cond, lhs, rhs, sum) configuration. -fn check_add_pair_case( - label: &str, - body: &T, - cond_cols: Vec, - lhs: AddOperand, - rhs: AddOperand, - sum: AddOperand, -) { - let conditional = !cond_cols.is_empty(); - let (old0, old1) = AddConstraint::new_pair(cond_cols, lhs, rhs, sum, 0); - check_emit_vs_old( - label, - body, - &add_pair_meta(0, conditional), - &[&|s| old0.evaluate::(s), &|s| { - old1.evaluate::(s) - }], - &[&|s| old0.evaluate::(s), &|s| { - old1.evaluate::(s) - }], - &[old_params(&old0), old_params(&old1)], - ); +/// Run the pair check for one `conditional` flag. +fn check_add_pair_case(label: &str, body: &T, conditional: bool) { + check_emit(label, body, &add_pair_meta(0, conditional)); } #[test] -fn emit_add_pair_matches_old_conditional_dword() { +fn emit_add_pair_conditional_dword() { emit_body!(Body, 2, |b| { emit_add_pair( b, @@ -302,18 +197,11 @@ fn emit_add_pair_matches_old_conditional_dword() { &AddOperand::dword(5), ) }); - check_add_pair_case( - "add_pair_conditional_dword", - &Body, - vec![0], - AddOperand::dword(1), - AddOperand::dword(3), - AddOperand::dword(5), - ); + check_add_pair_case("add_pair_conditional_dword", &Body, true); } #[test] -fn emit_add_pair_matches_old_linear_unconditional() { +fn emit_add_pair_linear_unconditional() { // DWordHL repack lhs; negative-coefficient + constant linear rhs — // exercises const_signed on both signs. fn rhs() -> AddOperand { @@ -338,18 +226,11 @@ fn emit_add_pair_matches_old_linear_unconditional() { &AddOperand::dword(5), ) }); - check_add_pair_case( - "add_pair_linear_unconditional", - &Body, - vec![], - AddOperand::from_dword_hl(8), - rhs(), - AddOperand::dword(5), - ); + check_add_pair_case("add_pair_linear_unconditional", &Body, false); } #[test] -fn emit_add_pair_matches_old_multi_cond_bytes() { +fn emit_add_pair_multi_cond_bytes() { // Multi-column condition (flag sum), Word + Constant operands, and a // DWordBL byte-repacked sum — the remaining AddOperand variants. emit_body!(Body, 2, |b| { @@ -362,14 +243,7 @@ fn emit_add_pair_matches_old_multi_cond_bytes() { &AddOperand::from_dword_bl(20), ) }); - check_add_pair_case( - "add_pair_multi_cond_bytes", - &Body, - vec![0, 2], - AddOperand::from_word(4), - AddOperand::constant(300), - AddOperand::from_dword_bl(20), - ); + check_add_pair_case("add_pair_multi_cond_bytes", &Body, true); } // ============================================================================= @@ -377,88 +251,44 @@ fn emit_add_pair_matches_old_multi_cond_bytes() { // ============================================================================= #[test] -fn emit_product_zero_matches_old() { +fn emit_product_zero_folder_capture_agree() { emit_body!(Body, 1, |b| { emit_product_zero(b, 0, 12, 17) }); - let old = ProductZeroConstraint::new(12, 17, 0); - check_emit_vs_old( - "product_zero", - &Body, - &[product_zero_meta(0)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], - ); + check_emit("product_zero", &Body, &[product_zero_meta(0)]); } #[test] -fn emit_arg2_exclusive_matches_old() { - for imm_col in [cols::IMM_0, cols::IMM_1] { - let old = Arg2ExclusiveConstraint::new(imm_col, 0); - // Body reads `imm_col` from the environment via two fixed cases. - if imm_col == cols::IMM_0 { - emit_body!(Body0, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_0) }); - check_emit_vs_old( - "arg2_exclusive_imm0", - &Body0, - &[arg2_exclusive_meta(0)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], - ); - } else { - emit_body!(Body1, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_1) }); - check_emit_vs_old( - "arg2_exclusive_imm1", - &Body1, - &[arg2_exclusive_meta(0)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], - ); - } - } +fn emit_arg2_exclusive_folder_capture_agree() { + emit_body!(Body0, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_0) }); + check_emit("arg2_exclusive_imm0", &Body0, &[arg2_exclusive_meta(0)]); + + emit_body!(Body1, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_1) }); + check_emit("arg2_exclusive_imm1", &Body1, &[arg2_exclusive_meta(0)]); } #[test] -fn emit_mem_flags_bit_matches_old() { +fn emit_mem_flags_bit_folder_capture_agree() { emit_body!(Body, 1, |b| { emit_mem_flags_bit(b, 0) }); - let old = MemFlagsBitConstraint::new(0); - check_emit_vs_old( - "mem_flags_bit", - &Body, - &[mem_flags_bit_meta(0)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], - ); + check_emit("mem_flags_bit", &Body, &[mem_flags_bit_meta(0)]); } #[test] -fn emit_reg_not_read_is_zero_matches_old() { +fn emit_reg_not_read_is_zero_folder_capture_agree() { emit_body!(Body, 1, |b| { emit_reg_not_read_is_zero(b, 0, cols::READ_REGISTER1, cols::RV1_0) }); - let old = RegNotReadIsZeroConstraint::new(cols::READ_REGISTER1, cols::RV1_0, 0); - check_emit_vs_old( + check_emit( "reg_not_read_is_zero_rv1", &Body, &[reg_not_read_is_zero_meta(0)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], ); emit_body!(Body2, 1, |b| { emit_reg_not_read_is_zero(b, 0, cols::READ_REGISTER2, cols::RV2_1) }); - let old = RegNotReadIsZeroConstraint::new(cols::READ_REGISTER2, cols::RV2_1, 0); - check_emit_vs_old( + check_emit( "reg_not_read_is_zero_rv2", &Body2, &[reg_not_read_is_zero_meta(0)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], ); } @@ -467,99 +297,35 @@ fn emit_reg_not_read_is_zero_matches_old() { // ============================================================================= #[test] -fn emit_arg2_matches_old() { +fn emit_arg2_folder_capture_agree() { emit_body!(Body0, 1, |b| { emit_arg2(b, 0, 0) }); + check_emit("arg2_word0", &Body0, &[arg2_meta(0)]); emit_body!(Body1, 1, |b| { emit_arg2(b, 0, 1) }); - let old0 = Arg2Constraint::new(0, 0); - check_emit_vs_old( - "arg2_word0", - &Body0, - &[arg2_meta(0)], - &[&|s| old0.evaluate::(s)], - &[&|s| old0.evaluate::(s)], - &[old_params(&old0)], - ); - let old1 = Arg2Constraint::new(1, 0); - check_emit_vs_old( - "arg2_word1", - &Body1, - &[arg2_meta(0)], - &[&|s| old1.evaluate::(s)], - &[&|s| old1.evaluate::(s)], - &[old_params(&old1)], - ); + check_emit("arg2_word1", &Body1, &[arg2_meta(0)]); } #[test] -fn emit_rvd_eq_res_matches_old() { +fn emit_rvd_eq_res_folder_capture_agree() { emit_body!(Body0, 1, |b| { emit_rvd_eq_res(b, 0, 0) }); + check_emit("rvd_eq_res_word0", &Body0, &[rvd_eq_res_meta(0)]); emit_body!(Body1, 1, |b| { emit_rvd_eq_res(b, 0, 1) }); - let old0 = RvdEqResConstraint::new(0, 0); - check_emit_vs_old( - "rvd_eq_res_word0", - &Body0, - &[rvd_eq_res_meta(0)], - &[&|s| old0.evaluate::(s)], - &[&|s| old0.evaluate::(s)], - &[old_params(&old0)], - ); - let old1 = RvdEqResConstraint::new(1, 0); - check_emit_vs_old( - "rvd_eq_res_word1", - &Body1, - &[rvd_eq_res_meta(0)], - &[&|s| old1.evaluate::(s)], - &[&|s| old1.evaluate::(s)], - &[old_params(&old1)], - ); + check_emit("rvd_eq_res_word1", &Body1, &[rvd_eq_res_meta(0)]); } #[test] -fn emit_branch_rvd_pair_matches_old() { +fn emit_branch_rvd_pair_folder_capture_agree() { emit_body!(Body, 2, |b| { emit_branch_rvd_pair(b, 0) }); - let (old0, old1) = BranchRvdConstraint::new_pair(0); - check_emit_vs_old( - "branch_rvd_pair", - &Body, - &branch_rvd_meta(0), - &[&|s| old0.evaluate::(s), &|s| { - old1.evaluate::(s) - }], - &[&|s| old0.evaluate::(s), &|s| { - old1.evaluate::(s) - }], - &[old_params(&old0), old_params(&old1)], - ); + check_emit("branch_rvd_pair", &Body, &branch_rvd_meta(0)); } #[test] -fn emit_branch_cond_matches_old() { +fn emit_branch_cond_folder_capture_agree() { emit_body!(Body, 1, |b| { emit_branch_cond(b, 0) }); - let old = BranchCondConstraint::new(0); - check_emit_vs_old( - "branch_cond", - &Body, - &[branch_cond_meta(0)], - &[&|s| old.evaluate::(s)], - &[&|s| old.evaluate::(s)], - &[old_params(&old)], - ); + check_emit("branch_cond", &Body, &[branch_cond_meta(0)]); } #[test] -fn emit_next_pc_add_pair_matches_old() { +fn emit_next_pc_add_pair_folder_capture_agree() { emit_body!(Body, 2, |b| { emit_next_pc_add_pair(b, 0) }); - let (old0, old1) = NextPcAddConstraint::new_pair(0); - check_emit_vs_old( - "next_pc_add_pair", - &Body, - &next_pc_add_meta(0), - &[&|s| old0.evaluate::(s), &|s| { - old1.evaluate::(s) - }], - &[&|s| old0.evaluate::(s), &|s| { - old1.evaluate::(s) - }], - &[old_params(&old0), old_params(&old1)], - ); + check_emit("next_pc_add_pair", &Body, &next_pc_add_meta(0)); } diff --git a/prover/src/tests/constraint_set_tests_a.rs b/prover/src/tests/constraint_set_tests_a.rs index d500fbe10..172d8b720 100644 --- a/prover/src/tests/constraint_set_tests_a.rs +++ b/prover/src/tests/constraint_set_tests_a.rs @@ -1,20 +1,14 @@ -//! Differential tests for the single-source `ConstraintSet` table conversions -//! (group A: dvrm, shift, mul, lt, load, ecsm, ecdas, ec_scalar). +//! Folder-vs-capture-interpret regression tests for the single-source +//! `ConstraintSet` table bodies (group A: dvrm, shift, mul, lt, load, ecsm, +//! ecdas, ec_scalar). //! -//! Each table's new `XxxConstraints` implementing [`ConstraintSet`] is checked -//! against the OLD boxed constraint builder it transcribes (the old structs + -//! `*_constraints` builders stay in-branch as this oracle until the final -//! deletion phase), on [`TRIALS`] random rows — off-trace points, where a -//! weakened or slipped transcription diverges with overwhelming probability: -//! -//! 1. `ProverEvalFolder` output == old `evaluate_prover`; -//! 2. `VerifierEvalFolder` output == old `evaluate_verifier`; -//! 3. `CaptureBuilder` → flatten → interpret == old `evaluate_prover`; -//! -//! plus meta parity vs the old boxed objects (count, `num_base`, and per-idx -//! degree / period / offset / exemptions_period / periodic_exemptions_offset / -//! end_exemptions), and tree-measured degree (`CaptureBuilder::finish`) == -//! declared `meta.degree`. +//! Each table's single `eval` body is run three ways — the `ProverEvalFolder` +//! (base), the `VerifierEvalFolder` (extension), and the `CaptureBuilder` → flat +//! IR → `eval_program_base` interpreter — and asserted to agree on [`TRIALS`] +//! random off-trace rows. All three derive from the ONE body, so this pins that +//! capture/interpretation stays faithful to the compiled folder. We also assert +//! the meta invariants (dense, idx-ordered, all-base) and that each root's +//! tree-measured degree does not EXCEED its declared `meta.degree`. use math::field::element::FieldElement; use stark::constraint_ir::eval_program_base; @@ -22,7 +16,6 @@ use stark::constraints::builder::{ CaptureBuilder, ConstraintSet, ProverEvalFolder, RootKind, VerifierEvalFolder, num_base_from_meta, }; -use stark::constraints::transition::TransitionConstraintEvaluator; use stark::frame::Frame; use stark::lookup::PackingShifts; use stark::table::TableView; @@ -48,72 +41,41 @@ impl SplitMix64 { } } -/// The full differential check for one table's `ConstraintSet` against the -/// OLD boxed constraint list (built via the old builder at `idx_start = 0`). +/// Folder-vs-capture-interpret regression check for one table's +/// [`ConstraintSet`]. The single body is run three ways (prover folder, verifier +/// folder, captured-IR interpreter) and asserted to agree on random off-trace +/// rows — all three derive from the ONE body, so this pins that +/// capture/interpretation stays faithful to the compiled folder. /// /// `num_cols` is the table's column count; frames are single-step (none of /// these tables read next-row cells). -fn check_set_vs_old( - label: &str, - set: &CS, - old: &[Box>], - num_cols: usize, -) where +fn check_set(label: &str, set: &CS, num_cols: usize) +where CS: ConstraintSet, { let meta = set.meta(); let n = meta.len(); - assert_eq!(old.len(), n, "[{label}] constraint count"); - // --- meta parity vs the old boxed objects --- + // --- meta invariants: dense, idx-ordered, all-base (group-A tables). --- let num_base = num_base_from_meta(&meta); assert_eq!(num_base, n, "[{label}] all-base num_base"); - // Build a lookup from the old objects by constraint_idx. - let mut old_by_idx: Vec>>> = - vec![None; n]; - for c in old.iter() { - let i = c.constraint_idx(); - assert!(i < n, "[{label}] old constraint idx {i} out of range"); - assert!(old_by_idx[i].is_none(), "[{label}] duplicate old idx {i}"); - old_by_idx[i] = Some(c); - } for (i, m) in meta.iter().enumerate() { - let c = old_by_idx[i].expect("dense old idx"); assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); - assert_eq!(m.degree, c.degree(), "[{label}] degree {i}"); - assert_eq!(m.period, c.period(), "[{label}] period {i}"); - assert_eq!(m.offset, c.offset(), "[{label}] offset {i}"); - assert_eq!( - m.exemptions_period, - c.exemptions_period(), - "[{label}] exemptions_period {i}" - ); - assert_eq!( - m.periodic_exemptions_offset, - c.periodic_exemptions_offset(), - "[{label}] periodic_exemptions_offset {i}" - ); - assert_eq!( - m.end_exemptions, - c.end_exemptions(), - "[{label}] end_exemptions {i}" - ); } - // --- capture once; tree-measured degree <= declared (== old degree()) --- + // --- capture once; tree-measured degree <= declared --- // - // The declared `meta.degree` reproduces the OLD struct's `degree()` exactly - // (asserted above at the meta-parity loop), which the engine uses as the - // composition-poly degree bound. For most tables that bound is tight, so - // tree-measured == declared. A few constraints (the ecsm/ecdas convolution - // TAILS — `ConvCarry` at large `i`) legitimately have a lower EXACT degree - // than their uniform declared bound: at limb `i` near the top, every - // remaining product has a zeroed (constant) factor, so the surviving - // expression is degree 1 while the struct declares 2 (resp. 3). The - // soundness-relevant invariant is therefore `measured <= declared` (the - // real degree must never EXCEED the bound the composition polynomial is - // sized for) — an over-declaration is safe, an under-declaration is not. + // The declared `meta.degree` is what the engine uses as the composition-poly + // degree bound. For most tables that bound is tight, so tree-measured == + // declared. A few constraints (the ecsm/ecdas convolution TAILS — `ConvCarry` + // at large `i`) legitimately have a lower EXACT degree than their uniform + // declared bound: at limb `i` near the top, every remaining product has a + // zeroed (constant) factor, so the surviving expression is degree 1 while the + // meta declares 2 (resp. 3). The soundness-relevant invariant is therefore + // `measured <= declared` (the real degree must never EXCEED the bound the + // composition polynomial is sized for) — over-declaration is safe, + // under-declaration is not. let mut cb = CaptureBuilder::::new(); set.eval(&mut cb); let (prog, degrees) = cb.finish(num_base); @@ -138,7 +100,7 @@ fn check_set_vs_old( let row: Vec = (0..num_cols).map(|_| FE::from(rng.next_u64())).collect(); let row_e: Vec = row.iter().map(|x| x.to_extension()).collect(); - // --- old prover-side reference: evaluate_prover into base_evals --- + // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); let ctx = TransitionEvaluationContext::new_prover( &frame, @@ -148,13 +110,13 @@ fn check_set_vs_old( &offset_e, &shifts, ); - let mut old_base = vec![FE::zero(); n]; - let mut old_ext_scratch = vec![Fp3::zero(); n]; - for c in old.iter() { - c.evaluate_prover(&ctx, &mut old_base, &mut old_ext_scratch); - } + let mut base_out = vec![FE::zero(); n]; + let mut ext_out = vec![Fp3::zero(); n]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); + set.eval(&mut folder); + folder.assert_all_emitted(); - // --- old verifier-side reference: evaluate_verifier into ext_evals --- + // --- VerifierEvalFolder (ext) --- let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( @@ -165,38 +127,22 @@ fn check_set_vs_old( &offset_e, &vshifts, ); - let mut old_vext = vec![Fp3::zero(); n]; - for c in old.iter() { - c.evaluate_verifier(&vctx, &mut old_vext); - } - - // --- 1. ProverEvalFolder == old evaluate_prover --- - let mut base_out = vec![FE::zero(); n]; - let mut ext_out = vec![Fp3::zero(); n]; - let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); - set.eval(&mut folder); - folder.assert_all_emitted(); - for i in 0..n { - assert_eq!( - base_out[i], old_base[i], - "[{label}] prover folder mismatch, constraint {i}, trial {trial}" - ); - } - - // --- 2. VerifierEvalFolder == old evaluate_verifier --- let mut vext_out = vec![Fp3::zero(); n]; let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); set.eval(&mut vfolder); vfolder.assert_all_emitted(); + + // Prover folder (promoted) == verifier folder. for i in 0..n { assert_eq!( - vext_out[i], old_vext[i], - "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" + base_out[i].to_extension(), + vext_out[i], + "[{label}] prover-vs-verifier folder mismatch, constraint {i}, trial {trial}" ); } - // --- 3. capture → flatten → interpret == old evaluate_prover --- - for (i, expected) in old_base.iter().enumerate() { + // --- capture → flatten → interpret == ProverEvalFolder (base) --- + for (i, expected) in base_out.iter().enumerate() { assert_eq!( &eval_program_base(&prog, i, &row), expected, @@ -212,15 +158,11 @@ fn check_set_vs_old( mod lt { use super::*; - use crate::tables::lt::{LtConstraints, cols, lt_constraints}; - use stark::constraints::transition::TransitionConstraint; + use crate::tables::lt::{LtConstraints, cols}; #[test] - fn lt_constraint_set_matches_old() { - let (old, next) = lt_constraints(0); - assert_eq!(next, old.len()); - let boxed: Vec<_> = old.into_iter().map(|c| c.boxed()).collect(); - check_set_vs_old("lt", &LtConstraints, &boxed, cols::NUM_COLUMNS); + fn lt_constraint_set_folder_capture_agree() { + check_set("lt", &LtConstraints, cols::NUM_COLUMNS); } } @@ -230,15 +172,11 @@ mod lt { mod dvrm { use super::*; - use crate::tables::dvrm::{DvrmConstraints, cols, dvrm_constraints}; - use stark::constraints::transition::TransitionConstraint; + use crate::tables::dvrm::{DvrmConstraints, cols}; #[test] - fn dvrm_constraint_set_matches_old() { - let (old, next) = dvrm_constraints(0); - assert_eq!(next, old.len()); - let boxed: Vec<_> = old.into_iter().map(|c| c.boxed()).collect(); - check_set_vs_old("dvrm", &DvrmConstraints, &boxed, cols::NUM_COLUMNS); + fn dvrm_constraint_set_folder_capture_agree() { + check_set("dvrm", &DvrmConstraints, cols::NUM_COLUMNS); } } @@ -248,15 +186,11 @@ mod dvrm { mod shift { use super::*; - use crate::tables::shift::{ShiftConstraints, cols, shift_constraints}; - use stark::constraints::transition::TransitionConstraint; + use crate::tables::shift::{ShiftConstraints, cols}; #[test] - fn shift_constraint_set_matches_old() { - let (old, next) = shift_constraints(0); - assert_eq!(next, old.len()); - let boxed: Vec<_> = old.into_iter().map(|c| c.boxed()).collect(); - check_set_vs_old("shift", &ShiftConstraints, &boxed, cols::NUM_COLUMNS); + fn shift_constraint_set_folder_capture_agree() { + check_set("shift", &ShiftConstraints, cols::NUM_COLUMNS); } } @@ -266,15 +200,11 @@ mod shift { mod mul { use super::*; - use crate::tables::mul::{MulConstraints, cols, mul_constraints}; - use stark::constraints::transition::TransitionConstraint; + use crate::tables::mul::{MulConstraints, cols}; #[test] - fn mul_constraint_set_matches_old() { - let (old, next) = mul_constraints(0); - assert_eq!(next, old.len()); - let boxed: Vec<_> = old.into_iter().map(|c| c.boxed()).collect(); - check_set_vs_old("mul", &MulConstraints, &boxed, cols::NUM_COLUMNS); + fn mul_constraint_set_folder_capture_agree() { + check_set("mul", &MulConstraints, cols::NUM_COLUMNS); } } @@ -284,13 +214,11 @@ mod mul { mod load { use super::*; - use crate::tables::load::{LoadConstraints, cols, constraints as load_constraints}; + use crate::tables::load::{LoadConstraints, cols}; #[test] - fn load_constraint_set_matches_old() { - // `constraints()` already returns boxed evaluators (idx_start = 0). - let boxed = load_constraints(); - check_set_vs_old("load", &LoadConstraints, &boxed, cols::NUM_COLUMNS); + fn load_constraint_set_folder_capture_agree() { + check_set("load", &LoadConstraints, cols::NUM_COLUMNS); } } @@ -300,13 +228,11 @@ mod load { mod ecsm { use super::*; - use crate::tables::ecsm::{EcsmConstraints, cols, create_constraints}; + use crate::tables::ecsm::{EcsmConstraints, cols}; #[test] - fn ecsm_constraint_set_matches_old() { - let (boxed, next) = create_constraints(0); - assert_eq!(next, boxed.len()); - check_set_vs_old("ecsm", &EcsmConstraints, &boxed, cols::NUM_COLUMNS); + fn ecsm_constraint_set_folder_capture_agree() { + check_set("ecsm", &EcsmConstraints, cols::NUM_COLUMNS); } } @@ -316,13 +242,11 @@ mod ecsm { mod ecdas { use super::*; - use crate::tables::ecdas::{EcdasConstraints, cols, create_constraints}; + use crate::tables::ecdas::{EcdasConstraints, cols}; #[test] - fn ecdas_constraint_set_matches_old() { - let (boxed, next) = create_constraints(0); - assert_eq!(next, boxed.len()); - check_set_vs_old("ecdas", &EcdasConstraints, &boxed, cols::NUM_COLUMNS); + fn ecdas_constraint_set_folder_capture_agree() { + check_set("ecdas", &EcdasConstraints, cols::NUM_COLUMNS); } } @@ -332,12 +256,10 @@ mod ecdas { mod ec_scalar { use super::*; - use crate::tables::ec_scalar::{EcScalarConstraints, cols, create_constraints}; + use crate::tables::ec_scalar::{EcScalarConstraints, cols}; #[test] - fn ec_scalar_constraint_set_matches_old() { - let (boxed, next) = create_constraints(0); - assert_eq!(next, boxed.len()); - check_set_vs_old("ec_scalar", &EcScalarConstraints, &boxed, cols::NUM_COLUMNS); + fn ec_scalar_constraint_set_folder_capture_agree() { + check_set("ec_scalar", &EcScalarConstraints, cols::NUM_COLUMNS); } } diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index 0989601f9..a7d0af6be 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -1,25 +1,18 @@ -//! Differential tests for the per-table [`ConstraintSet`] single-body -//! conversions (PR B, group B tables). +//! Folder-vs-capture-interpret regression tests for the per-table +//! [`ConstraintSet`] single bodies (group B tables). //! -//! Each converted table exposes a new `XxxConstraints: ConstraintSet` whose -//! `meta()`/`eval()` must reproduce, index-for-index, the OLD boxed builder -//! function (`eq_constraints`, `store_constraints`, …) that still lives in the -//! same file as the differential oracle. For every table we assert, on -//! [`TRIALS`] random rows (off-trace points, where a weakened or slipped -//! transcription diverges with overwhelming probability): -//! -//! 1. `ProverEvalFolder` output == old `evaluate_prover` (base field); -//! 2. `VerifierEvalFolder` output == old `evaluate_verifier` (extension); -//! 3. `CaptureBuilder` → flatten → `eval_program_base` == old `evaluate_prover`; -//! -//! plus meta parity vs the old boxed objects (count, `num_base`, and per-idx -//! degree / period / offset / exemptions_period / periodic_exemptions_offset / -//! end_exemptions), and tree-measured degree (`CaptureBuilder::finish`) == -//! declared `meta.degree`. +//! Each table's single `eval` body is exercised three ways — the +//! `ProverEvalFolder` (base), the `VerifierEvalFolder` (extension), and the +//! `CaptureBuilder` → flat IR → `eval_program_base` interpreter — and we assert +//! they agree on [`TRIALS`] random off-trace rows. All three derive from the +//! ONE body, so this pins that capture/interpretation stays faithful to the +//! compiled folder (the GPU/interpreter path a divergence would silently break). +//! We also assert the meta invariants (dense, idx-ordered, all-base) and that +//! each root's tree-measured degree equals its declared `meta.degree`. //! //! All group-B tables read the current row only (offset 0) and are entirely //! base-field, so `eval_program_base` (single `main_row`, row 0) is the -//! interpreter oracle. +//! interpreter entry point. use math::field::element::FieldElement; use stark::constraint_ir::eval_program_base; @@ -27,7 +20,6 @@ use stark::constraints::builder::{ CaptureBuilder, ConstraintSet, ProverEvalFolder, RootKind, VerifierEvalFolder, num_base_from_meta, }; -use stark::constraints::transition::TransitionConstraintEvaluator; use stark::frame::Frame; use stark::lookup::PackingShifts; use stark::table::TableView; @@ -53,20 +45,19 @@ impl SplitMix64 { } } -/// The OLD boxed constraint builder result, used as the differential oracle. -type OldVec = Vec>>; - -/// Run the full three-way differential + meta-parity check for one table. +/// Run the folder-vs-capture-interpret differential + meta invariants for one +/// table's [`ConstraintSet`]. All three interpretations derive from the ONE +/// single-source body, so agreement across them (on random off-trace rows) is a +/// permanent regression guard that capture/interpretation stays faithful to the +/// compiled folder. /// -/// * `old` — the OLD boxed constraints built at `idx_start = 0`. -/// * `set` — the NEW [`ConstraintSet`]. +/// * `set` — the table's [`ConstraintSet`]. /// * `num_cols` — the table's `cols::NUM_COLUMNS`. -fn check_table>(label: &str, old: &OldVec, set: &CS, num_cols: usize) { - let n = old.len(); +fn check_table>(label: &str, set: &CS, num_cols: usize) { let meta = set.meta(); + let n = meta.len(); - // --- count / meta parity vs the old boxed objects --- - assert_eq!(meta.len(), n, "[{label}] constraint count"); + // --- meta invariants: dense, idx-ordered, all-base (group-B tables). --- assert_eq!( num_base_from_meta(&meta), n, @@ -75,27 +66,6 @@ fn check_table>(label: &str, old: &OldVec, set: &CS, for (i, m) in meta.iter().enumerate() { assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); - // The old boxed objects expose their idx and zerofier params directly. - let o = &old[i]; - assert_eq!(o.constraint_idx(), i, "[{label}] old idx {i} out of order"); - assert_eq!(m.degree, o.degree(), "[{label}] degree {i}"); - assert_eq!(m.period, o.period(), "[{label}] period {i}"); - assert_eq!(m.offset, o.offset(), "[{label}] offset {i}"); - assert_eq!( - m.exemptions_period, - o.exemptions_period(), - "[{label}] exemptions_period {i}" - ); - assert_eq!( - m.periodic_exemptions_offset, - o.periodic_exemptions_offset(), - "[{label}] periodic_exemptions_offset {i}" - ); - assert_eq!( - m.end_exemptions, - o.end_exemptions(), - "[{label}] end_exemptions {i}" - ); } // --- capture once; tree-measured degree == declared --- @@ -123,39 +93,7 @@ fn check_table>(label: &str, old: &OldVec, set: &CS, let row: Vec = (0..num_cols).map(|_| FE::from(rng.next_u64())).collect(); let row_e: Vec = row.iter().map(|x| x.to_extension()).collect(); - // --- OLD oracle: prover (base) + verifier (ext) evaluations --- - let old_frame = - Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); - let old_ctx = TransitionEvaluationContext::new_prover( - &old_frame, - &no_periodic, - &no_ch, - &no_ch, - &offset_e, - &shifts, - ); - let mut old_base = vec![FE::zero(); n]; - let mut old_ext_p = vec![Fp3::zero(); n]; - for c in old.iter() { - c.evaluate_prover(&old_ctx, &mut old_base, &mut old_ext_p); - } - - let old_frame_e = - Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); - let old_vctx = TransitionEvaluationContext::::new_verifier( - &old_frame_e, - &no_periodic_e, - &no_ch, - &no_ch, - &offset_e, - &vshifts, - ); - let mut old_ext_v = vec![Fp3::zero(); n]; - for c in old.iter() { - c.evaluate_verifier(&old_vctx, &mut old_ext_v); - } - - // --- 1. ProverEvalFolder == old evaluate_prover (base) --- + // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); let ctx = TransitionEvaluationContext::new_prover( &frame, @@ -170,14 +108,8 @@ fn check_table>(label: &str, old: &OldVec, set: &CS, let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); set.eval(&mut folder); folder.assert_all_emitted(); - for (i, (got, want)) in base_out.iter().zip(old_base.iter()).enumerate() { - assert_eq!( - got, want, - "[{label}] prover folder mismatch, constraint {i}, trial {trial}" - ); - } - // --- 2. VerifierEvalFolder == old evaluate_verifier (ext) --- + // --- VerifierEvalFolder (ext) --- let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( @@ -192,15 +124,19 @@ fn check_table>(label: &str, old: &OldVec, set: &CS, let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); set.eval(&mut vfolder); vfolder.assert_all_emitted(); - for (i, (got, want)) in vext_out.iter().zip(old_ext_v.iter()).enumerate() { + + // Prover folder (promoted) == verifier folder: the same body over the + // same row in base vs extension must agree. + for (i, (b, v)) in base_out.iter().zip(vext_out.iter()).enumerate() { assert_eq!( - got, want, - "[{label}] verifier folder mismatch, constraint {i}, trial {trial}" + &b.to_extension(), + v, + "[{label}] prover-vs-verifier folder mismatch, constraint {i}, trial {trial}" ); } - // --- 3. capture → flatten → interpret == old evaluate_prover (base) --- - for (i, want) in old_base.iter().enumerate() { + // --- capture → flatten → interpret == ProverEvalFolder (base) --- + for (i, want) in base_out.iter().enumerate() { assert_eq!( &eval_program_base(&prog, i, &row), want, @@ -216,12 +152,11 @@ fn check_table>(label: &str, old: &OldVec, set: &CS, mod eq { use super::*; - use crate::tables::eq::{EqConstraints, cols, eq_constraints}; + use crate::tables::eq::{EqConstraints, cols}; #[test] - fn eq_constraint_set_matches_old() { - let (old, _) = eq_constraints(0); - check_table("eq", &old, &EqConstraints, cols::NUM_COLUMNS); + fn eq_constraint_set_folder_capture_agree() { + check_table("eq", &EqConstraints, cols::NUM_COLUMNS); } } @@ -231,12 +166,11 @@ mod eq { mod store { use super::*; - use crate::tables::store::{StoreConstraints, cols, store_constraints}; + use crate::tables::store::{StoreConstraints, cols}; #[test] - fn store_constraint_set_matches_old() { - let (old, _) = store_constraints(0); - check_table("store", &old, &StoreConstraints, cols::NUM_COLUMNS); + fn store_constraint_set_folder_capture_agree() { + check_table("store", &StoreConstraints, cols::NUM_COLUMNS); } } @@ -246,12 +180,11 @@ mod store { mod memw { use super::*; - use crate::tables::memw::{MemwConstraints, cols, constraints}; + use crate::tables::memw::{MemwConstraints, cols}; #[test] - fn memw_constraint_set_matches_old() { - let old = constraints(); - check_table("memw", &old, &MemwConstraints, cols::NUM_COLUMNS); + fn memw_constraint_set_folder_capture_agree() { + check_table("memw", &MemwConstraints, cols::NUM_COLUMNS); } } @@ -261,17 +194,11 @@ mod memw { mod memw_aligned { use super::*; - use crate::tables::memw_aligned::{MemwAlignedConstraints, cols, constraints}; + use crate::tables::memw_aligned::{MemwAlignedConstraints, cols}; #[test] - fn memw_aligned_constraint_set_matches_old() { - let old = constraints(); - check_table( - "memw_aligned", - &old, - &MemwAlignedConstraints, - cols::NUM_COLUMNS, - ); + fn memw_aligned_constraint_set_folder_capture_agree() { + check_table("memw_aligned", &MemwAlignedConstraints, cols::NUM_COLUMNS); } } @@ -281,17 +208,11 @@ mod memw_aligned { mod memw_register { use super::*; - use crate::tables::memw_register::{MemwRegisterConstraints, cols, constraints}; + use crate::tables::memw_register::{MemwRegisterConstraints, cols}; #[test] - fn memw_register_constraint_set_matches_old() { - let old = constraints(); - check_table( - "memw_register", - &old, - &MemwRegisterConstraints, - cols::NUM_COLUMNS, - ); + fn memw_register_constraint_set_folder_capture_agree() { + check_table("memw_register", &MemwRegisterConstraints, cols::NUM_COLUMNS); } } @@ -301,16 +222,11 @@ mod memw_register { mod branch { use super::*; - use crate::tables::branch::{BranchConstraints, branch_constraints, cols}; - use stark::constraints::transition::TransitionConstraint; + use crate::tables::branch::{BranchConstraints, cols}; #[test] - fn branch_constraint_set_matches_old() { - // `branch_constraints` returns unboxed `BranchConstraint`s; box them - // for the differential oracle. - let (old_structs, _) = branch_constraints(0); - let old: OldVec = old_structs.into_iter().map(|c| c.boxed()).collect(); - check_table("branch", &old, &BranchConstraints, cols::NUM_COLUMNS); + fn branch_constraint_set_folder_capture_agree() { + check_table("branch", &BranchConstraints, cols::NUM_COLUMNS); } } @@ -320,12 +236,11 @@ mod branch { mod commit { use super::*; - use crate::tables::commit::{CommitConstraints, cols, create_constraints}; + use crate::tables::commit::{CommitConstraints, cols}; #[test] - fn commit_constraint_set_matches_old() { - let (old, _) = create_constraints(0); - check_table("commit", &old, &CommitConstraints, cols::NUM_COLUMNS); + fn commit_constraint_set_folder_capture_agree() { + check_table("commit", &CommitConstraints, cols::NUM_COLUMNS); } } @@ -335,12 +250,11 @@ mod commit { mod keccak { use super::*; - use crate::tables::keccak::{KeccakConstraints, cols, create_constraints}; + use crate::tables::keccak::{KeccakConstraints, cols}; #[test] - fn keccak_constraint_set_matches_old() { - let (old, _) = create_constraints(0); - check_table("keccak", &old, &KeccakConstraints, cols::NUM_COLUMNS); + fn keccak_constraint_set_folder_capture_agree() { + check_table("keccak", &KeccakConstraints, cols::NUM_COLUMNS); } } @@ -350,12 +264,11 @@ mod keccak { mod keccak_rnd { use super::*; - use crate::tables::keccak_rnd::{KeccakRndConstraints, cols, create_constraints}; + use crate::tables::keccak_rnd::{KeccakRndConstraints, cols}; #[test] - fn keccak_rnd_constraint_set_matches_old() { - let (old, _) = create_constraints(0); - check_table("keccak_rnd", &old, &KeccakRndConstraints, cols::NUM_COLUMNS); + fn keccak_rnd_constraint_set_folder_capture_agree() { + check_table("keccak_rnd", &KeccakRndConstraints, cols::NUM_COLUMNS); } } @@ -365,43 +278,27 @@ mod keccak_rnd { mod cpu32 { use super::*; - use crate::tables::cpu32::{Cpu32Constraints, cols, cpu32_constraints}; + use crate::tables::cpu32::{Cpu32Constraints, cols}; #[test] - fn cpu32_constraint_set_matches_old() { - let (old, _) = cpu32_constraints(0); - check_table("cpu32", &old, &Cpu32Constraints, cols::NUM_COLUMNS); + fn cpu32_constraint_set_folder_capture_agree() { + check_table("cpu32", &Cpu32Constraints, cols::NUM_COLUMNS); } } // ============================================================================= -// cpu.rs (constraints/cpu.rs — assembled by create_all_cpu_constraints, never -// a prover/src/tables/*.rs conversion) +// cpu.rs (CpuConstraints lives in constraints/cpu.rs, not a +// prover/src/tables/*.rs conversion) // ============================================================================= mod cpu { use super::*; - use crate::constraints::cpu::{ - CpuConstraints, NUM_CPU_CONSTRAINTS, create_all_cpu_constraints, - }; + use crate::constraints::cpu::{CpuConstraints, NUM_CPU_CONSTRAINTS}; use crate::tables::cpu::cols; - use stark::constraints::transition::TransitionConstraint; #[test] - fn cpu_constraint_set_matches_old() { - // create_all_cpu_constraints returns (is_bit, add, other, next) with - // idx 0..11, 12..15, 16..38 respectively — concatenate in that order so - // the boxed oracle is dense and idx-ordered. - let (is_bit, add, other, next) = create_all_cpu_constraints(); - assert_eq!(next, NUM_CPU_CONSTRAINTS); - let mut old: OldVec = Vec::with_capacity(NUM_CPU_CONSTRAINTS); - for c in is_bit { - old.push(c.boxed()); - } - for c in add { - old.push(c.boxed()); - } - old.extend(other); - check_table("cpu", &old, &CpuConstraints, cols::NUM_COLUMNS); + fn cpu_constraint_set_folder_capture_agree() { + assert_eq!(CpuConstraints.meta().len(), NUM_CPU_CONSTRAINTS); + check_table("cpu", &CpuConstraints, cols::NUM_COLUMNS); } } diff --git a/prover/src/tests/constraints_tests.rs b/prover/src/tests/constraints_tests.rs index e52cc6c0e..51f41c314 100644 --- a/prover/src/tests/constraints_tests.rs +++ b/prover/src/tests/constraints_tests.rs @@ -1,10 +1,7 @@ //! Tests for the 64-bit VM constraint templates. -use crate::constraints::templates::{ - AddConstraint, AddLinearTerm, AddOperand, IsBitConstraint, SHIFT_32, new_is_bit_constraints, -}; +use crate::constraints::templates::{AddLinearTerm, AddOperand, SHIFT_32}; use crate::tables::types::FE; -use stark::constraints::transition::TransitionConstraint; // ========================================================================= // Basic tests @@ -19,43 +16,6 @@ fn test_inv_2_32() { assert_eq!(product, FE::one()); } -#[test] -fn test_is_bit_constraint_degree() { - // Conditional: degree 3 - let conditional = IsBitConstraint::new(0, 1, 0); - assert_eq!(conditional.degree(), 3); - - // Unconditional: degree 2 - let unconditional = IsBitConstraint::unconditional(1, 0); - assert_eq!(unconditional.degree(), 2); -} - -#[test] -fn test_add_constraint_degree() { - let (c0, c1) = AddConstraint::new_pair( - vec![0], - AddOperand::dword(1), - AddOperand::dword(3), - AddOperand::dword(5), - 0, - ); - assert_eq!(c0.degree(), 3); - assert_eq!(c1.degree(), 3); -} - -#[test] -fn test_add_constraint_indices() { - let (c0, c1) = AddConstraint::new_pair( - vec![0], - AddOperand::dword(1), - AddOperand::dword(3), - AddOperand::dword(5), - 10, - ); - assert_eq!(c0.constraint_idx(), 10); - assert_eq!(c1.constraint_idx(), 11); -} - // ========================================================================= // IS_BIT formula verification tests // ========================================================================= @@ -186,25 +146,6 @@ fn test_carry_max_values() { assert_eq!(carry, FE::one()); } -// ========================================================================= -// Helper function tests -// ========================================================================= - -#[test] -fn test_new_is_bit_constraints_count() { - let (constraints, next_idx) = new_is_bit_constraints(&[1, 2, 3, 4], 10); - assert_eq!(constraints.len(), 4); - assert_eq!(next_idx, 14); -} - -#[test] -fn test_new_is_bit_constraints_indices() { - let (constraints, _) = new_is_bit_constraints(&[5, 6, 7], 100); - assert_eq!(constraints[0].constraint_idx(), 100); - assert_eq!(constraints[1].constraint_idx(), 101); - assert_eq!(constraints[2].constraint_idx(), 102); -} - // ========================================================================= // AddOperand tests // ========================================================================= @@ -512,13 +453,9 @@ fn test_dword_bl_repack_formula() { // CPU Constraints tests // ========================================================================= -use crate::constraints::cpu::{ - Arg2Constraint, BIT_FLAG_COLUMNS, BranchCondConstraint, NUM_CPU_CONSTRAINTS, - NextPcAddConstraint, ProductZeroConstraint, RegNotReadIsZeroConstraint, RvdEqResConstraint, - create_add_constraints, create_all_cpu_constraints, create_is_bit_constraints, - create_sub_constraints, -}; +use crate::constraints::cpu::{BIT_FLAG_COLUMNS, CpuConstraints, NUM_CPU_CONSTRAINTS}; use crate::tables::cpu::cols as cpu_cols; +use stark::constraints::builder::{ConstraintSet, num_base_from_meta}; #[test] fn test_cpu_bit_flag_columns_count() { @@ -534,95 +471,17 @@ fn test_cpu_bit_flag_columns_valid() { } #[test] -fn test_create_is_bit_constraints_count() { - let (cs, next) = create_is_bit_constraints(0); - assert_eq!(cs.len(), BIT_FLAG_COLUMNS.len()); - assert_eq!(next, BIT_FLAG_COLUMNS.len()); -} - -#[test] -fn test_add_sub_constraint_pairs() { - let (add, next) = create_add_constraints(0); - assert_eq!(add.len(), 2, "ADD carry pair"); - let (sub, next2) = create_sub_constraints(next); - assert_eq!(sub.len(), 2, "SUB carry pair"); - assert_eq!(next2, next + 2, "constraint indices are contiguous"); -} - -#[test] -fn test_product_zero_constraint_degree() { - // word_instr · MEMORY = 0 (decode mutex): degree 2. - let c = ProductZeroConstraint::new(cpu_cols::WORD_INSTR, cpu_cols::MEMORY, 0); - assert_eq!(c.degree(), 2); -} - -#[test] -fn test_arg2_constraint_degree() { - // (1 - MEMORY - BRANCH)·(rv2 + imm): degree 2 (relies on the live - // MEMORY·BRANCH = 0 mutex). - assert_eq!(Arg2Constraint::new(0, 0).degree(), 2); - assert_eq!(Arg2Constraint::new(1, 0).degree(), 2); -} - -#[test] -fn test_rvd_eq_res_constraint_degree() { - // (1 - MEMORY - BRANCH)·(rvd[i] - cast(res, WL)[i]): degree 2. - // BRANCH rows are exempt — their rvd (`pc + len`) is pinned by - // BranchRvdConstraint instead. Well within the blowup=2 budget. - assert_eq!(RvdEqResConstraint::new(0, 0).degree(), 2); - assert_eq!(RvdEqResConstraint::new(1, 0).degree(), 2); -} - -#[test] -fn test_branch_cond_constraint_degree() { - // branch_cond = BRANCH·JALR + BRANCH·(1-JALR)·res[0]: degree 3. - assert_eq!(BranchCondConstraint::new(0).degree(), 3); -} - -#[test] -fn test_reg_not_read_is_zero_degree() { - let c = RegNotReadIsZeroConstraint::new(cpu_cols::READ_REGISTER1, cpu_cols::RV1_0, 0); - assert_eq!(c.degree(), 2); -} - -#[test] -fn test_next_pc_add_constraint() { - let (c0, c1) = NextPcAddConstraint::new_pair(5); - assert_eq!(c0.degree(), 3); - assert_eq!(c1.degree(), 3); - assert_eq!(c0.constraint_idx(), 5); - assert_eq!(c1.constraint_idx(), 6); -} - -#[test] -fn test_create_all_cpu_constraints_count() { - let (is_bit, add, other, total) = create_all_cpu_constraints(); - // IS_BIT: 12, ADD+SUB pairs: 4, other (mutex 6 + arg2 2 + reg-zero 4 + rvd 2 - // + branch rvd 2 + branch_cond 1 + next_pc 2 + assumptions 4): 23. - assert_eq!(is_bit.len(), 12); - assert_eq!(add.len(), 4); - assert_eq!(other.len(), 23); - assert_eq!(total, NUM_CPU_CONSTRAINTS); - assert_eq!(is_bit.len() + add.len() + other.len(), NUM_CPU_CONSTRAINTS); -} - -#[test] -fn test_cpu_constraint_indices_are_unique_and_sequential() { - let (is_bit, add, other, _) = create_all_cpu_constraints(); - - let mut indices: Vec = Vec::new(); - for c in &is_bit { - indices.push(c.constraint_idx()); - } - for c in &add { - indices.push(c.constraint_idx()); - } - for c in &other { - indices.push(c.constraint_idx()); - } - - indices.sort_unstable(); - for (i, &idx) in indices.iter().enumerate() { - assert_eq!(idx, i, "constraint indices must be unique and cover 0..N"); +fn test_cpu_constraint_set_meta_is_dense_all_base() { + // The CPU single-source set declares exactly NUM_CPU_CONSTRAINTS base + // constraints, dense and idx-ordered (per-constraint degrees and the + // folder-vs-capture faithfulness are covered by constraint_set_tests_b). + let meta = CpuConstraints.meta(); + assert_eq!(meta.len(), NUM_CPU_CONSTRAINTS); + assert_eq!(num_base_from_meta(&meta), NUM_CPU_CONSTRAINTS); + for (i, m) in meta.iter().enumerate() { + assert_eq!( + m.constraint_idx, i, + "constraint indices cover 0..N in order" + ); } } diff --git a/prover/src/tests/cpu32_tests.rs b/prover/src/tests/cpu32_tests.rs index 3ef1468a8..835da27d9 100644 --- a/prover/src/tests/cpu32_tests.rs +++ b/prover/src/tests/cpu32_tests.rs @@ -2,14 +2,37 @@ //! sign-extension / register-zero constraints. use crate::tables::cpu32::{ - Cpu32Constraint, Cpu32ConstraintKind, Cpu32Operation, bus_interactions, cols, - generate_cpu32_trace, + Cpu32Constraints, Cpu32Operation, bus_interactions, cols, generate_cpu32_trace, }; use crate::tables::types::{ BusId, FE, GoldilocksExtension, GoldilocksField, alu_op, build_alu_flags, }; -use stark::constraints::transition::TransitionConstraint; +use math::field::element::FieldElement; +use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; +use stark::frame::Frame; +use stark::lookup::PackingShifts; use stark::table::TableView; +use stark::traits::TransitionEvaluationContext; + +/// Evaluate the CPU32 [`ConstraintSet`] on one main row, returning every +/// base-field constraint value (the compiled prover folder path). +fn eval_cpu32(row: &[FE]) -> Vec { + let n = Cpu32Constraints.meta().len(); + let frame = Frame::::new(vec![TableView::new( + vec![row.to_vec()], + vec![vec![]], + )]); + let shifts = PackingShifts::::new(); + let no_e: Vec> = vec![]; + let offset_e = FieldElement::::zero(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &[], &no_e, &no_e, &offset_e, &shifts); + let mut base = vec![FE::zero(); n]; + let mut ext = vec![FieldElement::::zero(); n]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); + Cpu32Constraints.eval(&mut folder); + base +} #[test] fn test_aux_signed_input_extension() { @@ -124,13 +147,6 @@ fn test_trace_layout() { assert_eq!(row[cols::MU], FE::from(1u64)); } -/// Build a single-row `TableView` from a CPU32 trace generated for `op`. -fn view_for(op: Cpu32Operation) -> TableView { - let trace = generate_cpu32_trace(&[op]); - let row = trace.main_table.get_row(0).to_vec(); - TableView::new(vec![row], vec![vec![]]) -} - #[test] fn test_ext_and_regzero_constraints_hold_on_valid_row() { // A signed word op via the immediate path (read_register2 = 0, rv2 = 0). @@ -148,36 +164,13 @@ fn test_ext_and_regzero_constraints_hold_on_valid_row() { half_instruction_length: 2, ..Default::default() }; - let view = view_for(op); - - // All sign-extension arithmetic constraints evaluate to zero. - for kind in [ - Cpu32ConstraintKind::Arg1Lo, - Cpu32ConstraintKind::Arg1Hi, - Cpu32ConstraintKind::Arg2Lo, - Cpu32ConstraintKind::Arg2Hi, - Cpu32ConstraintKind::RvdLo, - Cpu32ConstraintKind::RvdHi, - ] { - let c = Cpu32Constraint::new(kind, 0); - assert_eq!(c.evaluate(&view), FE::zero(), "{kind:?} must hold"); - } + let trace = generate_cpu32_trace(&[op]); + let row = trace.main_table.get_row(0).to_vec(); - // Register-zero checks: read_register1=1 ⇒ trivially 0; read_register2=0 with rv2=0 ⇒ 0. - for (read_col, value_col) in [ - (cols::READ_REGISTER1, cols::RV1_0), - (cols::READ_REGISTER1, cols::RV1_1), - (cols::READ_REGISTER2, cols::RV2_0), - (cols::READ_REGISTER2, cols::RV2_1), - ] { - let c = Cpu32Constraint::new( - Cpu32ConstraintKind::RegZero { - read_col, - value_col, - }, - 0, - ); - assert_eq!(c.evaluate(&view), FE::zero()); + // Every CPU32 constraint (sign-extension arithmetic + register-zero checks) + // holds on the valid row. + for (i, v) in eval_cpu32(&row).iter().enumerate() { + assert_eq!(*v, FE::zero(), "constraint {i} must hold on a valid row"); } } @@ -195,37 +188,26 @@ fn test_constraints_catch_corruption() { }; let trace = generate_cpu32_trace(&[op]); - // Corrupt arg1[1] (the sign-extended high word) → Arg1Hi must fire. + // Corrupt arg1[1] (the sign-extended high word) → some constraint must fire. let mut row = trace.main_table.get_row(0).to_vec(); row[cols::ARG1_1] += FE::one(); - let bad: TableView = - TableView::new(vec![row], vec![vec![]]); - let c = Cpu32Constraint::new(Cpu32ConstraintKind::Arg1Hi, 0); - assert_ne!( - c.evaluate(&bad), - FE::zero(), - "Arg1Hi should catch a bad arg1[1]" + assert!( + eval_cpu32(&row).iter().any(|v| *v != FE::zero()), + "a corrupted arg1[1] must break some constraint" ); - // read_register1 = 1 but a non-zero unread half would only matter when 0; - // instead corrupt with read=0 case: a value present while read flag cleared. + // A non-zero unread register value (read_register2 = 0, rv2 ≠ 0) must fire + // the register-zero check. let op2 = Cpu32Operation { rv2: 0x1234, // non-zero read_register2: false, // but flagged unread ..Default::default() }; - let view2 = view_for(op2); - let c2 = Cpu32Constraint::new( - Cpu32ConstraintKind::RegZero { - read_col: cols::READ_REGISTER2, - value_col: cols::RV2_0, - }, - 0, - ); - assert_ne!( - c2.evaluate(&view2), - FE::zero(), - "RegZero should catch rv2≠0 when unread" + let trace2 = generate_cpu32_trace(&[op2]); + let row2 = trace2.main_table.get_row(0).to_vec(); + assert!( + eval_cpu32(&row2).iter().any(|v| *v != FE::zero()), + "rv2≠0 while unread must break some constraint" ); } diff --git a/prover/src/tests/dvrm_tests.rs b/prover/src/tests/dvrm_tests.rs index 8a8d8c31b..2b5abe3b9 100644 --- a/prover/src/tests/dvrm_tests.rs +++ b/prover/src/tests/dvrm_tests.rs @@ -4,7 +4,7 @@ use stark::proof::options::ProofOptions; use stark::traits::AIR; use crate::tables::dvrm::{ - DvrmConstraints, DvrmOperation, bus_interactions, cols, dvrm_constraints, generate_dvrm_trace, + DvrmConstraints, DvrmOperation, bus_interactions, cols, generate_dvrm_trace, }; use crate::tables::types::FE; use crate::test_utils::{ @@ -461,7 +461,8 @@ fn test_dvrm_air_wires_in_chip_constraints() { cols::NUM_COLUMNS, bus_interactions(), ); - assert_eq!(in_chip, dvrm_constraints(0).0.len()); + use stark::constraints::builder::ConstraintSet; + assert_eq!(in_chip, DvrmConstraints.meta().len()); } /// Regression test for the `Msb16` LogUp over-send bug. diff --git a/prover/src/tests/ec_scalar_tests.rs b/prover/src/tests/ec_scalar_tests.rs index 462443843..2a7cc1f1e 100644 --- a/prover/src/tests/ec_scalar_tests.rs +++ b/prover/src/tests/ec_scalar_tests.rs @@ -1,24 +1,39 @@ //! Tests for the EC_SCALAR table — constraint satisfaction on generated traces, -//! the `last_limb` schedule, and the constraint count. +//! the `last_limb` schedule, and the single-source constraint count. -use crate::constraints::templates::IsBitConstraint; use crate::tables::ec_scalar::{ - MulZeroConstraint, cols, create_constraints, generate_ec_scalar_trace, rows_for_scalar, + EcScalarConstraints, cols, generate_ec_scalar_trace, rows_for_scalar, }; use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; -use stark::constraints::transition::TransitionConstraint; +use math::field::element::FieldElement; +use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; +use stark::frame::Frame; +use stark::lookup::PackingShifts; use stark::table::TableView; use stark::trace::TraceTable; +use stark::traits::TransitionEvaluationContext; -/// Builds a one-row `TableView` for `row` of the trace (constraints only read row 0). -fn row_view( - trace: &TraceTable, - row: usize, -) -> TableView { +/// Evaluate the EC_SCALAR [`ConstraintSet`] on one trace row (the compiled +/// prover folder path), returning every base-field constraint value. +fn eval_row(trace: &TraceTable, row: usize) -> Vec { let main: Vec = (0..cols::NUM_COLUMNS) .map(|c| *trace.main_table.get(row, c)) .collect(); - TableView::new(vec![main], vec![]) + let n = EcScalarConstraints.meta().len(); + let frame = Frame::::new(vec![TableView::new( + vec![main], + vec![vec![]], + )]); + let shifts = PackingShifts::::new(); + let no_e: Vec> = vec![]; + let offset_e = FieldElement::::zero(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &[], &no_e, &no_e, &offset_e, &shifts); + let mut base = vec![FE::zero(); n]; + let mut ext = vec![FieldElement::::zero(); n]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); + EcScalarConstraints.eval(&mut folder); + base } #[test] @@ -32,41 +47,10 @@ fn constraints_hold_on_generated_trace() { let ops = rows_for_scalar(444, 0x3000, &k); let trace = generate_ec_scalar_trace(&ops); - // IS_BIT columns - let mut bit_cols = vec![cols::MU]; - bit_cols.extend((0..8).map(cols::limb_bit)); - bit_cols.push(cols::LAST_LIMB); - for row in 0..trace.num_rows() { - let view = row_view(&trace, row); - for &col in &bit_cols { - let v = IsBitConstraint::unconditional(col, 0).evaluate(&view); - assert_eq!(v, FE::zero(), "IS_BIT col {col} row {row}"); - } - // implication constraints - for i in 0..8 { - let c = MulZeroConstraint { - a: cols::limb_bit(i), - b: cols::MU, - b_complement: true, - constraint_idx: 0, - }; - assert_eq!(c.evaluate(&view), FE::zero(), "limb_bit{i}=>mu row {row}"); + for (i, v) in eval_row(&trace, row).iter().enumerate() { + assert_eq!(*v, FE::zero(), "constraint {i} must hold at row {row}"); } - let c = MulZeroConstraint { - a: cols::LAST_LIMB, - b: cols::MU, - b_complement: true, - constraint_idx: 0, - }; - assert_eq!(c.evaluate(&view), FE::zero(), "last_limb=>mu row {row}"); - let c = MulZeroConstraint { - a: cols::LAST_LIMB, - b: cols::OFFSET, - b_complement: false, - constraint_idx: 0, - }; - assert_eq!(c.evaluate(&view), FE::zero(), "last_limb=>offset row {row}"); } } @@ -84,8 +68,6 @@ fn last_limb_set_only_at_offset_zero() { } #[test] -fn create_constraints_count() { - let (constraints, next) = create_constraints(0); - assert_eq!(constraints.len(), 20); - assert_eq!(next, 20); +fn constraint_set_count() { + assert_eq!(EcScalarConstraints.meta().len(), 20); } diff --git a/prover/src/tests/ecdas_tests.rs b/prover/src/tests/ecdas_tests.rs index 38a413ab0..ccb69f5d9 100644 --- a/prover/src/tests/ecdas_tests.rs +++ b/prover/src/tests/ecdas_tests.rs @@ -1,16 +1,17 @@ -//! Tests for the ECDAS double/add table — the `R_BYTES` offset constant, constraint -//! satisfaction on generated traces across many scalars, and the constraint count. +//! Tests for the ECDAS double/add table — the `R_BYTES` offset constant, +//! constraint satisfaction on generated traces across many scalars, and the +//! single-source constraint count. -use crate::constraints::templates::IsBitConstraint; -use crate::tables::ecdas::{ - ColIsZero, ConvCarry, EcdasOperation, MulZero, R_BYTES, Relation, cols, create_constraints, - generate_ecdas_trace, -}; +use crate::tables::ecdas::{EcdasConstraints, EcdasOperation, R_BYTES, cols, generate_ecdas_trace}; use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; use ecsm::compute_witness; -use stark::constraints::transition::TransitionConstraint; +use math::field::element::FieldElement; +use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; +use stark::frame::Frame; +use stark::lookup::PackingShifts; use stark::table::TableView; use stark::trace::TraceTable; +use stark::traits::TransitionEvaluationContext; fn gx_le() -> [u8; 32] { let mut be = [ @@ -43,14 +44,39 @@ fn ops_for(k: u64) -> Vec { ops_for_bytes(&k_le(k)) } -fn row_view( - trace: &TraceTable, - row: usize, -) -> TableView { +/// Evaluate the ECDAS [`ConstraintSet`] on one trace row (the compiled prover +/// folder path), returning every base-field constraint value. +fn eval_row(trace: &TraceTable, row: usize) -> Vec { let main: Vec = (0..cols::NUM_COLUMNS) .map(|c| *trace.main_table.get(row, c)) .collect(); - TableView::new(vec![main], vec![]) + let n = EcdasConstraints.meta().len(); + let frame = Frame::::new(vec![TableView::new( + vec![main], + vec![vec![]], + )]); + let shifts = PackingShifts::::new(); + let no_e: Vec> = vec![]; + let offset_e = FieldElement::::zero(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &[], &no_e, &no_e, &offset_e, &shifts); + let mut base = vec![FE::zero(); n]; + let mut ext = vec![FieldElement::::zero(); n]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); + EcdasConstraints.eval(&mut folder); + base +} + +fn assert_trace_holds(trace: &TraceTable, label: &str) { + for row in 0..trace.num_rows() { + for (i, v) in eval_row(trace, row).iter().enumerate() { + assert_eq!( + *v, + FE::zero(), + "{label}: constraint {i} must hold at row {row}" + ); + } + } } #[test] @@ -63,73 +89,15 @@ fn r_bytes_is_three_p() { assert_eq!(&bytes[..], &R_BYTES[..]); } -/// Every ECDAS constraint evaluates to zero on a generated trace across many scalars -/// (which exercise both double and add steps), including padding rows. +/// Every ECDAS constraint evaluates to zero on a generated trace across many +/// scalars (exercising both double and add steps), including padding rows. #[test] fn constraints_hold_on_generated_trace() { for k in [2u64, 3, 5, 7, 0xFF, 0xABCD, 1_000_003] { let ops = ops_for(k); assert!(!ops.is_empty(), "k={k} should have steps"); let trace = generate_ecdas_trace(&ops); - - for row in 0..trace.num_rows() { - let view = row_view(&trace, row); - assert_eq!( - IsBitConstraint::unconditional(cols::MU, 0).evaluate(&view), - FE::zero(), - "is_bit(mu) k={k} row {row}" - ); - assert_eq!( - IsBitConstraint::unconditional(cols::NEXT_OP, 0).evaluate(&view), - FE::zero() - ); - assert_eq!( - IsBitConstraint::unconditional(cols::OP, 0).evaluate(&view), - FE::zero() - ); - assert_eq!( - MulZero { - a: cols::OP, - b: cols::NEXT_OP, - b_complement: false, - constraint_idx: 0 - } - .evaluate(&view), - FE::zero(), - "op·next_op k={k} row {row}" - ); - assert_eq!( - MulZero { - a: cols::NEXT_OP, - b: cols::MU, - b_complement: true, - constraint_idx: 0 - } - .evaluate(&view), - FE::zero() - ); - for relation in [Relation::Lambda, Relation::Xr, Relation::Yr] { - for i in 0..64 { - let v = ConvCarry { - relation, - i, - constraint_idx: 0, - } - .evaluate(&view); - assert_eq!(v, FE::zero(), "conv k={k} i={i} row {row}"); - } - } - for c_base in [cols::C0, cols::C1, cols::C2] { - assert_eq!( - ColIsZero { - col: c_base + 63, - constraint_idx: 0 - } - .evaluate(&view), - FE::zero() - ); - } - } + assert_trace_holds(&trace, &format!("k={k}")); } } @@ -141,28 +109,10 @@ fn constraints_hold_for_near_order_scalar() { let ops = ops_for_bytes(&k); assert!(!ops.is_empty()); let trace = generate_ecdas_trace(&ops); - for row in 0..trace.num_rows() { - let view = row_view(&trace, row); - for relation in [Relation::Lambda, Relation::Xr, Relation::Yr] { - for i in 0..64 { - assert_eq!( - ConvCarry { - relation, - i, - constraint_idx: 0 - } - .evaluate(&view), - FE::zero(), - "conv N-1 i={i} row {row}" - ); - } - } - } + assert_trace_holds(&trace, "N-1"); } #[test] -fn create_constraints_count() { - let (constraints, next) = create_constraints(0); - assert_eq!(constraints.len(), 200); - assert_eq!(next, 200); +fn constraint_set_count() { + assert_eq!(EcdasConstraints.meta().len(), 200); } diff --git a/prover/src/tests/ecsm_tests.rs b/prover/src/tests/ecsm_tests.rs index bc92c4596..18607f9ac 100644 --- a/prover/src/tests/ecsm_tests.rs +++ b/prover/src/tests/ecsm_tests.rs @@ -1,16 +1,16 @@ -//! Tests for the ECSM core table — constraint satisfaction on generated traces, -//! constraint count, and the yG padding-closure argument. +//! Tests for the ECSM core table — constraint satisfaction on generated traces +//! and the single-source constraint count. -use crate::constraints::templates::IsBitConstraint; -use crate::tables::ecsm::{ - CarryBit, ColIsZero, ConvCarry, EcsmOperation, OverflowKind, OverflowRequired, Relation, cols, - create_constraints, generate_ecsm_trace, -}; +use crate::tables::ecsm::{EcsmConstraints, EcsmOperation, cols, generate_ecsm_trace}; use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; -use ecsm::{P_BYTES, compute_witness}; -use stark::constraints::transition::TransitionConstraint; +use ecsm::compute_witness; +use math::field::element::FieldElement; +use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; +use stark::frame::Frame; +use stark::lookup::PackingShifts; use stark::table::TableView; use stark::trace::TraceTable; +use stark::traits::TransitionEvaluationContext; fn gx_le() -> [u8; 32] { // secp256k1 Gx, little-endian. @@ -40,17 +40,31 @@ fn op_for(k: u64) -> EcsmOperation { } } -fn row_view( - trace: &TraceTable, - row: usize, -) -> TableView { +/// Evaluate the ECSM [`ConstraintSet`] on one trace row (the compiled prover +/// folder path), returning every base-field constraint value. +fn eval_row(trace: &TraceTable, row: usize) -> Vec { let main: Vec = (0..cols::NUM_COLUMNS) .map(|c| *trace.main_table.get(row, c)) .collect(); - TableView::new(vec![main], vec![]) + let n = EcsmConstraints.meta().len(); + let frame = Frame::::new(vec![TableView::new( + vec![main], + vec![vec![]], + )]); + let shifts = PackingShifts::::new(); + let no_e: Vec> = vec![]; + let offset_e = FieldElement::::zero(); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &[], &no_e, &no_e, &offset_e, &shifts); + let mut base = vec![FE::zero(); n]; + let mut ext = vec![FieldElement::::zero(); n]; + let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); + EcsmConstraints.eval(&mut folder); + base } -/// Every ECSM constraint evaluates to zero on a generated trace (real + padding rows). +/// Every ECSM constraint evaluates to zero on a generated trace (real + padding +/// rows). This exercises the padding closure (`q1 = p`, µ-gated `b`) end to end. #[test] fn constraints_hold_on_generated_trace() { let ops: Vec = [1u64, 2, 5, 0xFFFF, 1_000_003] @@ -60,135 +74,13 @@ fn constraints_hold_on_generated_trace() { let trace = generate_ecsm_trace(&ops); for row in 0..trace.num_rows() { - let view = row_view(&trace, row); - // Re-evaluate concrete constraints (mirror create_constraints) at this row. - assert_eq!( - IsBitConstraint::unconditional(cols::MU, 0).evaluate(&view), - FE::zero(), - "is_bit(mu) row {row}" - ); - for i in 0..64 { - for relation in [Relation::X2, Relation::Yg] { - let v = ConvCarry { - relation, - i, - constraint_idx: 0, - } - .evaluate(&view); - assert_eq!(v, FE::zero(), "conv carry i={i} row {row}"); - } - } - assert_eq!( - ColIsZero { - col: cols::c0(63), - constraint_idx: 0 - } - .evaluate(&view), - FE::zero() - ); - assert_eq!( - ColIsZero { - col: cols::c1(63), - constraint_idx: 0 - } - .evaluate(&view), - FE::zero() - ); - for kind in [OverflowKind::KLtN, OverflowKind::XrLtP] { - for i in 0..7 { - assert_eq!( - CarryBit { - kind, - i, - constraint_idx: 0 - } - .evaluate(&view), - FE::zero(), - "carry bit kind i={i} row {row}" - ); - } - assert_eq!( - OverflowRequired { - kind, - constraint_idx: 0 - } - .evaluate(&view), - FE::zero(), - "overflow required row {row}" - ); + for (i, v) in eval_row(&trace, row).iter().enumerate() { + assert_eq!(*v, FE::zero(), "constraint {i} must hold at row {row}"); } } } #[test] -fn create_constraints_count() { - let (constraints, next) = create_constraints(0); - assert_eq!(constraints.len(), 148); - assert_eq!(next, 148); -} - -/// The yG carry recurrence is unsatisfiable on a padding row unless two ingredients hold, -/// and this test locks both: -/// (a) `q1` pads to `p`, so the `p² − q1·p` offset cancels; -/// (b) the curve constant `b` is multiplied by `µ`, so it drops when `µ = 0`. -/// Removing either ingredient leaves a nonzero residual on the yG limb-0 relation. -/// The x² relation has no standalone constant, so it closes on all-zero padding and is -/// left fully unconditional. -#[test] -fn yg_padding_closes_via_q1_eq_p_and_mu_gated_b() { - // yG limb-0 ConvCarry residual on a one-off row with the given `µ` and `q1`. - let yg_residual = |mu: u64, q1_is_p: bool| { - let mut main = vec![FE::zero(); cols::NUM_COLUMNS]; - main[cols::MU] = FE::from(mu); - if q1_is_p { - for (i, &b) in P_BYTES.iter().enumerate() { - main[cols::Q1 + i] = FE::from(b as u64); - } - } - let view: TableView = - TableView::new(vec![main], vec![]); - ConvCarry { - relation: Relation::Yg, - i: 0, - constraint_idx: 0, - } - .evaluate(&view) - }; - - // The padding row this chip emits (µ = 0, q1 = p): both ingredients present → closes. - assert_eq!( - yg_residual(0, true), - FE::zero(), - "padding row (µ=0, q1=p) must close" - ); - - // Drop ingredient (a): q1 = 0 instead of p → the p² offset is uncancelled. - assert_eq!( - yg_residual(0, false), - FE::zero() - FE::from(2209u64), - "without q1=p the residual is −P_0² = −47²" - ); - - // Drop ingredient (b): force the row active (µ = 1) so the curve constant `b` - // survives even with q1 = p. Residual = b = 7. - assert_eq!( - yg_residual(1, true), - FE::from(7u64), - "with µ=1 (b ungated) the leftover residual is the curve constant b=7" - ); - - // x² has no standalone constant → closes on an all-zero padding row regardless. - let mut zero = vec![FE::zero(); cols::NUM_COLUMNS]; - zero[cols::MU] = FE::zero(); - let zview: TableView = TableView::new(vec![zero], vec![]); - assert_eq!( - ConvCarry { - relation: Relation::X2, - i: 0, - constraint_idx: 0, - } - .evaluate(&zview), - FE::zero(), - "x² closes on all-zero padding (no standalone constant)" - ); +fn constraint_set_count() { + assert_eq!(EcsmConstraints.meta().len(), 148); } diff --git a/prover/src/tests/lt_tests.rs b/prover/src/tests/lt_tests.rs index 92068b67b..d7f707a13 100644 --- a/prover/src/tests/lt_tests.rs +++ b/prover/src/tests/lt_tests.rs @@ -3,9 +3,7 @@ use stark::proof::options::ProofOptions; use stark::traits::AIR; -use crate::tables::lt::{ - LtConstraints, LtOperation, bus_interactions, cols, generate_lt_trace, lt_constraints, -}; +use crate::tables::lt::{LtConstraints, LtOperation, bus_interactions, cols, generate_lt_trace}; use crate::tables::types::FE; use crate::test_utils::{busless_air, create_lt_air, in_chip_constraint_count, validate_busless}; @@ -208,9 +206,10 @@ fn test_lt_air_wires_in_chip_constraints() { cols::NUM_COLUMNS, bus_interactions(), ); - assert_eq!(in_chip, lt_constraints(0).0.len()); + use stark::constraints::builder::ConstraintSet; + assert_eq!(in_chip, LtConstraints.meta().len()); // Carry0IsBit, Carry1IsBit, LtFormula, OutXorInvert, InvertIsBit, SignedIsBit. - assert_eq!(lt_constraints(0).0.len(), 6); + assert_eq!(LtConstraints.meta().len(), 6); } /// Enforcement (this branch's unified-ALU-bus layout): the bus consumes `out`, diff --git a/prover/src/tests/mul_tests.rs b/prover/src/tests/mul_tests.rs index f47b9c12a..1e2fe5b5b 100644 --- a/prover/src/tests/mul_tests.rs +++ b/prover/src/tests/mul_tests.rs @@ -4,7 +4,7 @@ use stark::proof::options::ProofOptions; use stark::traits::AIR; use crate::tables::mul::{ - MulConstraints, MulOperation, bus_interactions, cols, generate_mul_trace, mul_constraints, + MulConstraints, MulOperation, bus_interactions, cols, generate_mul_trace, }; use crate::tables::types::FE; use crate::test_utils::{ @@ -339,10 +339,11 @@ fn test_mul_air_wires_in_chip_constraints() { cols::NUM_COLUMNS, bus_interactions(), ); - assert_eq!(in_chip, mul_constraints(0).0.len()); + use stark::constraints::builder::ConstraintSet; + assert_eq!(in_chip, MulConstraints.meta().len()); // 2x SignedIsBit + LhsSign + RhsSign + 4x RawProduct (#644 added the two // SignedIsBit constraints that #652's count of 6 predated). - assert_eq!(mul_constraints(0).0.len(), 8); + assert_eq!(MulConstraints.meta().len(), 8); } /// Presence: every input halfword is range-checked via IS_HALFWORD senders, so a diff --git a/prover/src/tests/trace_builder_tests.rs b/prover/src/tests/trace_builder_tests.rs index b23da43bf..8540b2926 100644 --- a/prover/src/tests/trace_builder_tests.rs +++ b/prover/src/tests/trace_builder_tests.rs @@ -757,16 +757,14 @@ mod keccak_tests { #[test] fn test_keccak_constraint_counts() { - let (core_constraints, _) = keccak::create_constraints(0); + use stark::constraints::builder::ConstraintSet; assert_eq!( - core_constraints.len(), + keccak::KeccakConstraints.meta().len(), 51, "KECCAK core: 25 ADD pairs + no-overflow" ); - - let (rnd_constraints, _) = keccak_rnd::create_constraints(0); assert_eq!( - rnd_constraints.len(), + keccak_rnd::KeccakRndConstraints.meta().len(), 20, "KECCAK_RND: 20 IS_BIT(μ; Cxz_right_bit) per spec d75944ee" ); diff --git a/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md b/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md index 95ae65f96..3bbdeae28 100644 --- a/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md +++ b/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md @@ -527,3 +527,36 @@ Notes: | Goldilocks residue constants | `prover/src/tables/types.rs:387-423` | | Example AIRs (to migrate) | `crypto/stark/src/examples/*.rs` (13 files) | | Bench harness | `scripts/bench_abba.sh` (runs on the bench server only) | + +## 9. Completion notes (engine switch, PR 2) + +The engine switch (§5.6) landed as: single-source LogUp (`LogUpLayout` + +`emit_logup_constraints` + `logup_meta` in `lookup.rs`), `CpuConstraints` +(`prover/src/constraints/cpu.rs`), the `AirWithBuses` + +`AIR`-trait rewiring, VM cross-verification (`scripts/cross_verify_vm.sh`, +both directions green pre- and post-deletion), then the deletions. + +- **`AIR::constraint_program()` (Phase-4 GPU access path).** The flat + `ConstraintProgram` is produced by `AirWithBuses::constraint_program()`, + lazily captured once and cached in a `OnceLock` (built by running the table's + `ConstraintSet::eval` + `emit_logup_constraints` through `CaptureBuilder`, + matching the folders' emission order/indexing exactly). The trait default + panics, so only capture-capable AIRs expose it; the verify/recursion path + never calls it (guest-safety, §5.7). +- **`Op::Var.row` is provably 0.** Every capture leaf constructs + `Op::Var { row: 0, .. }` (both `IrBuilder::main/aux` and `CaptureBuilder`), + and nothing else writes `row`. The Phase-4 device encoding can drop the + `row` field entirely. +- **No `stark/constraint-ir` feature on this branch.** Unlike the spike (which + gated an interpreter-as-prover hook behind that feature), this branch has no + such feature — the compiled folder is the only prover path and the + interpreter is exercised directly by the folder-vs-capture differential + tests. The §5.9.4 gate `--features stark/constraint-ir` is therefore N/A; + the default suites cover both the folder and interpreter paths. +- **`VmAir` is now `Box`.** Each table's `AirWithBuses<..,CS>` is a + distinct concrete type once `CS` is a type parameter, so `VmAirs` stores the + heterogeneous per-table AIRs behind a trait object; `create_*_air` return the + concrete `AirWithBuses` (so `.with_name` / `.with_preprocessed` still chain) + and are boxed at `VmAirs` assembly. One virtual call per row per table into + the monomorphized folder — same shape as before, minus the 33 per-row inner + virtual calls. From 3fe62aa7fc9896c72b3134504776b3c427a97d77 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 11:20:19 -0300 Subject: [PATCH 33/52] reports: post-deletion cross-verification evidence scripts/cross_verify_vm.sh 2499b2a8 734faae0: all 3 ELFs (sub, add, arith_8) cross-verify in both directions after the old-machinery deletion. scripts/cross_verify_examples.sh 88adbfa6 734faae0: all 11 example AIRs cross-verify in both directions (22/22). --- .../cross_verify_examples_post-deletion.log | 32 +++++++++++++++++++ .../sscs/cross_verify_vm_post-deletion.log | 17 ++++++++++ 2 files changed, 49 insertions(+) create mode 100644 reports/sscs/cross_verify_examples_post-deletion.log create mode 100644 reports/sscs/cross_verify_vm_post-deletion.log diff --git a/reports/sscs/cross_verify_examples_post-deletion.log b/reports/sscs/cross_verify_examples_post-deletion.log new file mode 100644 index 000000000..068964958 --- /dev/null +++ b/reports/sscs/cross_verify_examples_post-deletion.log @@ -0,0 +1,32 @@ +==> Refs + OLD 88adbfa6 -> 88adbfa64c + NEW 734faae0 -> 734faae04c +Preparing worktree (detached HEAD 88adbfa6) +==> Building examples_cli @ 88adbfa64c -> cli_old +==> Building examples_cli @ 734faae04c -> cli_new +==> Cross-verifying 11 examples, both directions +PASS prove-NEW-verify-OLD : simple_fibonacci +PASS prove-OLD-verify-NEW : simple_fibonacci +PASS prove-NEW-verify-OLD : fibonacci_2_columns +PASS prove-OLD-verify-NEW : fibonacci_2_columns +PASS prove-NEW-verify-OLD : fibonacci_2_cols_shifted +PASS prove-OLD-verify-NEW : fibonacci_2_cols_shifted +PASS prove-NEW-verify-OLD : fibonacci_multi_column +PASS prove-OLD-verify-NEW : fibonacci_multi_column +PASS prove-NEW-verify-OLD : quadratic_air +PASS prove-OLD-verify-NEW : quadratic_air +PASS prove-NEW-verify-OLD : fibonacci_rap +PASS prove-OLD-verify-NEW : fibonacci_rap +PASS prove-NEW-verify-OLD : dummy_air +PASS prove-OLD-verify-NEW : dummy_air +PASS prove-NEW-verify-OLD : simple_addition +PASS prove-OLD-verify-NEW : simple_addition +PASS prove-NEW-verify-OLD : read_only_memory +PASS prove-OLD-verify-NEW : read_only_memory +PASS prove-NEW-verify-OLD : read_only_memory_logup +PASS prove-OLD-verify-NEW : read_only_memory_logup +PASS prove-NEW-verify-OLD : multi_table_lookup +PASS prove-OLD-verify-NEW : multi_table_lookup + +==> RESULT: all 11 examples cross-verify in both directions. +EXIT=0 diff --git a/reports/sscs/cross_verify_vm_post-deletion.log b/reports/sscs/cross_verify_vm_post-deletion.log new file mode 100644 index 000000000..fd98fb7c9 --- /dev/null +++ b/reports/sscs/cross_verify_vm_post-deletion.log @@ -0,0 +1,17 @@ +==> Refs + OLD 2499b2a8 -> 2499b2a821 + NEW 734faae0 -> 734faae04c +==> ELFs: /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/sub.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/add.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/arith_8.elf +Preparing worktree (detached HEAD 2499b2a8) +==> Building cli @ 2499b2a821 -> cli_old +==> Building cli @ 734faae04c -> cli_new +==> Cross-verifying 3 ELFs, both directions +PASS prove-NEW-verify-OLD : sub +PASS prove-OLD-verify-NEW : sub +PASS prove-NEW-verify-OLD : add +PASS prove-OLD-verify-NEW : add +PASS prove-NEW-verify-OLD : arith_8 +PASS prove-OLD-verify-NEW : arith_8 + +==> RESULT: all 3 ELFs cross-verify in both directions. +EXIT=0 From 5d7259041a6cd79805e5e2267cddd0a95062cf1b Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 11:36:34 -0300 Subject: [PATCH 34/52] stark: rip the dead periodic machinery + trim ConstraintMeta to the every-row shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No production constraint (and, since the bit_flags/simple_periodic_cols example deletions, no AIR at all) uses periodic columns or a period/offset/periodic- exemptions zerofier shape. Rip the dead generality, value-neutrally: - Periodic columns: ConstraintBuilder::periodic + both folders' plumbing + CaptureBuilder leaf, Op::Periodic + IrBuilder::periodic + the interpreter's resolve_periodic, AIR::get_periodic_column_values/get_periodic_column_polynomials, the evaluator's lde_periodic_columns/periodic_buf plumbing, and the periodic_values field of both TransitionEvaluationContext variants (new_prover/ new_verifier lose the parameter; all call sites updated). - ConstraintMeta trims to { constraint_idx, kind, degree, end_exemptions } (period/offset/exemptions_period/periodic_exemptions_offset and their with_* builders deleted; with_end_exemptions kept — fibonacci fixtures use it). ZerofierGroupKey is now keyed on end_exemptions alone. - constraints/zerofier.rs specializes to the every-row shape via the exact pow simplification (offset+N-period = N-1; root^(0*N) = 1; z^(N/1) = z^N): the zerofier is 1/(x^N - 1) times the end-exemptions correction. Batch inversion, loop order and operand types unchanged - bit-identical values for the shape every constraint uses. Re-add path for any of it: git history — zerofier metadata is orthogonal to the constraint bodies, so restoring costs no body rewrites. Both crates build 0 errors / 0 warnings; clippy clean; cargo test -p stark 164 passed; cargo test --release -p lambda-vm-prover 491 passed + the same 5 pre-existing missing-rust-ELF failures as the baseline. --- crypto/stark/src/constraint_ir/builder.rs | 5 - crypto/stark/src/constraint_ir/interp.rs | 15 +- crypto/stark/src/constraint_ir/ir.rs | 3 - crypto/stark/src/constraint_ir/tests.rs | 49 ++--- crypto/stark/src/constraints/builder.rs | 52 +----- crypto/stark/src/constraints/builder_tests.rs | 46 +---- crypto/stark/src/constraints/evaluator.rs | 47 +---- crypto/stark/src/constraints/zerofier.rs | 176 +++++------------- crypto/stark/src/debug.rs | 27 +-- crypto/stark/src/lookup.rs | 4 - crypto/stark/src/traits.rs | 51 +---- crypto/stark/src/verifier.rs | 7 - prover/src/tests/constraint_emit_tests.rs | 19 +- prover/src/tests/constraint_set_tests_a.rs | 19 +- prover/src/tests/constraint_set_tests_b.rs | 19 +- prover/src/tests/cpu32_tests.rs | 3 +- prover/src/tests/ec_scalar_tests.rs | 3 +- prover/src/tests/ecdas_tests.rs | 3 +- prover/src/tests/ecsm_tests.rs | 3 +- 19 files changed, 106 insertions(+), 445 deletions(-) diff --git a/crypto/stark/src/constraint_ir/builder.rs b/crypto/stark/src/constraint_ir/builder.rs index 3b9d2d6eb..ff1365e96 100644 --- a/crypto/stark/src/constraint_ir/builder.rs +++ b/crypto/stark/src/constraint_ir/builder.rs @@ -136,11 +136,6 @@ impl IrBuilder { ) } - /// A periodic column read at the current row ([`Dim::Base`]). - pub fn periodic(&mut self, idx: usize) -> Expr { - self.push(Op::Periodic { idx: idx as u16 }, Dim::Base) - } - /// A LogUp RAP challenge, uniform per proof ([`Dim::Ext`]). pub fn challenge(&mut self, idx: usize) -> Expr { self.push(Op::RapChallenge { idx: idx as u16 }, Dim::Ext) diff --git a/crypto/stark/src/constraint_ir/interp.rs b/crypto/stark/src/constraint_ir/interp.rs index ee8cb3ba5..9bb049c27 100644 --- a/crypto/stark/src/constraint_ir/interp.rs +++ b/crypto/stark/src/constraint_ir/interp.rs @@ -54,14 +54,12 @@ impl, E: IsField> Value { } /// Shared forward pass: evaluate every node, then return the value array. -/// `resolve_var` resolves `Op::Var` leaves; `resolve_periodic` resolves -/// `Op::Periodic`; the remaining uniforms are read from field-agnostic closures -/// so prover/verifier share this one walk. +/// `resolve_var` resolves `Op::Var` leaves; the remaining uniforms are read +/// from field-agnostic closures so prover/verifier share this one walk. #[allow(clippy::too_many_arguments)] -fn run( +fn run( prog: &ConstraintProgram, resolve_var: FVar, - resolve_periodic: FPeriodic, resolve_challenge: FChallenge, resolve_alpha: FAlpha, resolve_offset: FOffset, @@ -70,7 +68,6 @@ where F: IsSubFieldOf, E: IsField, FVar: Fn(bool, u8, u8, u16) -> Value, - FPeriodic: Fn(u16) -> Value, FChallenge: Fn(u16) -> FieldElement, FAlpha: Fn(u16) -> FieldElement, FOffset: Fn() -> FieldElement, @@ -87,7 +84,6 @@ where row, col, } => resolve_var(main, offset, row, col), - Op::Periodic { idx } => resolve_periodic(idx), Op::RapChallenge { idx } => Value::Ext(resolve_challenge(idx)), Op::AlphaPow { idx } => Value::Ext(resolve_alpha(idx)), Op::TableOffset => Value::Ext(resolve_offset()), @@ -156,7 +152,6 @@ where assert_eq!(row, 0, "minimal set reads row 0 only"); Value::Base(main_row[col as usize].clone()) }, - |_idx| panic!("periodic leaves are not part of the minimal algebraic set"), |_idx| panic!("challenge leaves are not part of the minimal algebraic set"), |_idx| panic!("alpha_power leaves are not part of the minimal algebraic set"), || panic!("table_offset leaves are not part of the minimal algebraic set"), @@ -181,7 +176,6 @@ pub fn eval_program( { let TransitionEvaluationContext::Prover { frame, - periodic_values, rap_challenges, logup_alpha_powers, logup_table_offset, @@ -202,7 +196,6 @@ pub fn eval_program( Value::Ext(step.get_aux_evaluation_element(0, col as usize).clone()) } }, - |idx| Value::Base(periodic_values[idx as usize].clone()), |idx| rap_challenges[idx as usize].clone(), |idx| logup_alpha_powers[idx as usize].clone(), || (*logup_table_offset).clone(), @@ -234,7 +227,6 @@ pub fn eval_program_verifier( { let TransitionEvaluationContext::Verifier { frame, - periodic_values, rap_challenges, logup_alpha_powers, logup_table_offset, @@ -255,7 +247,6 @@ pub fn eval_program_verifier( Value::Ext(step.get_aux_evaluation_element(0, col as usize).clone()) } }, - |idx| Value::Ext(periodic_values[idx as usize].clone()), |idx| rap_challenges[idx as usize].clone(), |idx| logup_alpha_powers[idx as usize].clone(), || (*logup_table_offset).clone(), diff --git a/crypto/stark/src/constraint_ir/ir.rs b/crypto/stark/src/constraint_ir/ir.rs index c4159589a..ba4c2c935 100644 --- a/crypto/stark/src/constraint_ir/ir.rs +++ b/crypto/stark/src/constraint_ir/ir.rs @@ -56,9 +56,6 @@ pub enum Op { /// Column index. col: u16, }, - /// A periodic column read: `periodic_values[idx]` at the current row - /// ([`Dim::Base`]). - Periodic { idx: u16 }, /// A LogUp RAP challenge: `rap_challenges[idx]` ([`Dim::Ext`], uniform per /// proof). RapChallenge { idx: u16 }, diff --git a/crypto/stark/src/constraint_ir/tests.rs b/crypto/stark/src/constraint_ir/tests.rs index 61e3d8dbb..57a2bef39 100644 --- a/crypto/stark/src/constraint_ir/tests.rs +++ b/crypto/stark/src/constraint_ir/tests.rs @@ -196,13 +196,11 @@ fn frame_offset_reads_next_step() { let step0 = TableView::::new(vec![vec![fp(10)]], vec![vec![]]); let step1 = TableView::::new(vec![vec![fp(17)]], vec![vec![]]); let frame = Frame::::new(vec![step0, step1]); - let periodic: Vec = vec![]; let rap: Vec = vec![]; let alpha: Vec = vec![]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); let mut base_evals = vec![FpE::zero()]; let mut ext_evals: Vec = vec![]; @@ -232,13 +230,11 @@ fn mixed_base_ext_auto_embeds() { let aux_val = ext3(2, 3, 4); let step = TableView::::new(vec![vec![main_val]], vec![vec![aux_val]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let rap: Vec = vec![]; let alpha: Vec = vec![]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; @@ -267,13 +263,11 @@ fn explicit_embed_and_ext_neg() { let aux_val = ext3(1, 2, 3); let step = TableView::::new(vec![vec![fp(9)]], vec![vec![aux_val]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let rap: Vec = vec![]; let alpha: Vec = vec![]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero()]; @@ -282,25 +276,25 @@ fn explicit_embed_and_ext_neg() { } // ------------------------------------------------------------------------ -// Every leaf kind: periodic, challenge, alpha_power, table_offset, aux. +// Every leaf kind: main, challenge, alpha_power, table_offset, aux. // ------------------------------------------------------------------------ #[test] fn all_leaf_kinds_logup_shaped() { // A LogUp-shaped expression touching every leaf variety: - // periodic(0) * challenge(0) + alpha_pow(1) * aux(0,3) - table_offset() + // main(0,0) * challenge(0) + alpha_pow(1) * aux(0,3) - table_offset() let mut b = IrBuilder::::new(); - let p = b.periodic(0); // Base + let m = b.main(0, 0); // Base let ch = b.challenge(0); // Ext let ap = b.alpha_power(1); // Ext let au = b.aux(0, 3); // Ext let off = b.table_offset(); // Ext - assert_eq!(p.dim(), Dim::Base); + assert_eq!(m.dim(), Dim::Base); assert_eq!(ch.dim(), Dim::Ext); assert_eq!(ap.dim(), Dim::Ext); assert_eq!(au.dim(), Dim::Ext); assert_eq!(off.dim(), Dim::Ext); - let t1 = b.mul(p, ch); // Base×Ext → Ext + let t1 = b.mul(m, ch); // Base×Ext → Ext let t2 = b.mul(ap, au); // Ext×Ext → Ext let s = b.add(t1, t2); let res = b.sub(s, off); @@ -308,23 +302,22 @@ fn all_leaf_kinds_logup_shaped() { b.emit(0, res); let prog = b.finish(0); - let periodic = vec![fp(6)]; + let main_row = vec![fp(6)]; let rap = vec![ext3(1, 0, 0), ext3(2, 2, 2)]; let alpha = vec![ext3(9, 9, 9), ext3(3, 1, 4)]; let offset = ext3(7, 7, 7); let aux_row = vec![ext3(0, 0, 0), ext3(0, 0, 0), ext3(0, 0, 0), ext3(5, 5, 5)]; let expected = { - let t1 = periodic[0] * rap[0]; // periodic(0) * challenge(0) + let t1 = main_row[0] * rap[0]; // main(0,0) * challenge(0) let t2 = alpha[1] * aux_row[3]; (t1 + t2) - offset }; - let step = TableView::::new(vec![vec![]], vec![aux_row]); + let step = TableView::::new(vec![main_row], vec![aux_row]); let frame = Frame::::new(vec![step]); let shifts = PackingShifts::::new(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero()]; @@ -357,13 +350,11 @@ fn prover_entry_point_splits_base_and_ext() { let aux_val = ext3(2, 0, 1); let step = TableView::::new(vec![vec![fp(30), fp(12)]], vec![vec![aux_val]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let rap: Vec = vec![]; let alpha = vec![ext3(3, 3, 3)]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &periodic, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); let mut base_evals = vec![FpE::zero()]; let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; @@ -384,13 +375,12 @@ fn verifier_entry_point_promotes_base_roots() { vec![vec![aux_val]], ); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let rap: Vec = vec![]; let alpha = vec![ext3(3, 3, 3)]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); let ctx = TransitionEvaluationContext::::new_verifier( - &frame, &periodic, &rap, &alpha, &offset, &shifts, + &frame, &rap, &alpha, &offset, &shifts, ); let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; @@ -491,14 +481,12 @@ fn non_goldilocks_reflexive_tower_builds_and_interprets() { // Full prover entry point with F = E = G. let step = TableView::::new(vec![vec![g(6), g(7)]], vec![vec![g(4)]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let rap: Vec = vec![]; let alpha: Vec = vec![]; let offset = g(0); let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::::new_prover( - &frame, &periodic, &rap, &alpha, &offset, &shifts, - ); + let ctx = + TransitionEvaluationContext::::new_prover(&frame, &rap, &alpha, &offset, &shifts); let mut base_evals = vec![GE::zero()]; let mut ext_evals = vec![GE::zero(), GE::zero()]; eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); @@ -506,9 +494,8 @@ fn non_goldilocks_reflexive_tower_builds_and_interprets() { assert_eq!(ext_evals[1], g(4) + g(10)); // Verifier entry point too (the frame is Frame either way here). - let vctx = TransitionEvaluationContext::::new_verifier( - &frame, &periodic, &rap, &alpha, &offset, &shifts, - ); + let vctx = + TransitionEvaluationContext::::new_verifier(&frame, &rap, &alpha, &offset, &shifts); let mut v_evals = vec![GE::zero(), GE::zero()]; eval_program_verifier(&prog, &vctx, &mut v_evals); assert_eq!(v_evals[0], g(6) * g(7) + g(3)); diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index 9c43a12ce..4863da229 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -103,7 +103,6 @@ pub trait ConstraintBuilder { // ---- leaves --------------------------------------------------------- fn main(&self, offset: usize, col: usize) -> Self::Expr; fn aux(&self, offset: usize, col: usize) -> Self::ExprE; - fn periodic(&self, idx: usize) -> Self::Expr; /// `rap_challenges[idx]`. fn challenge(&self, idx: usize) -> Self::ExprE; /// `logup_alpha_powers[idx]`. @@ -142,6 +141,10 @@ pub enum RootKind { /// Per-constraint metadata: plain data replacing the per-constraint trait /// objects. `Base` entries MUST form a prefix of an idx-ordered, dense list — /// see [`num_base_from_meta`]. +/// +/// Every constraint applies to every row (up to `end_exemptions` rows at the +/// end of the trace); there is no per-constraint period/offset/periodic +/// exemption machinery. #[derive(Clone, Debug)] pub struct ConstraintMeta { pub constraint_idx: usize, @@ -149,14 +152,6 @@ pub struct ConstraintMeta { pub kind: RootKind, /// Declared degree; asserted == tree-measured degree (host-side test). pub degree: usize, - /// Periodicity of application over the trace (default 1: every row). - pub period: usize, - /// Offset for periodic application (default 0). - pub offset: usize, - /// Periodicity of rows exempted within the applied rows (default None). - pub exemptions_period: Option, - /// Offset for the periodic exemptions (default None). - pub periodic_exemptions_offset: Option, /// Number of exempted rows at the end of the trace (default 0). pub end_exemptions: usize, } @@ -169,10 +164,6 @@ impl ConstraintMeta { constraint_idx, kind: RootKind::Base, degree, - period: 1, - offset: 0, - exemptions_period: None, - periodic_exemptions_offset: None, end_exemptions: 0, } } @@ -185,22 +176,6 @@ impl ConstraintMeta { } } - pub fn with_period(mut self, period: usize) -> Self { - self.period = period; - self - } - - pub fn with_offset(mut self, offset: usize) -> Self { - self.offset = offset; - self - } - - pub fn with_exemptions(mut self, exemptions_period: usize, exemptions_offset: usize) -> Self { - self.exemptions_period = Some(exemptions_period); - self.periodic_exemptions_offset = Some(exemptions_offset); - self - } - pub fn with_end_exemptions(mut self, end_exemptions: usize) -> Self { self.end_exemptions = end_exemptions; self @@ -377,7 +352,6 @@ where E: IsField, { frame: &'a Frame, - periodic: &'a [FieldElement], challenges: &'a [FieldElement], alphas: &'a [FieldElement], logup_table_offset: &'a FieldElement, @@ -403,7 +377,6 @@ where ) -> Self { let TransitionEvaluationContext::Prover { frame, - periodic_values, rap_challenges, logup_alpha_powers, logup_table_offset, @@ -415,7 +388,6 @@ where let num_constraints = base_out.len().max(ext_out.len()); Self { frame, - periodic: periodic_values, challenges: rap_challenges, alphas: logup_alpha_powers, logup_table_offset, @@ -452,9 +424,6 @@ where .get_aux_evaluation_element(0, col) .clone() } - fn periodic(&self, idx: usize) -> FieldElement { - self.periodic[idx].clone() - } fn challenge(&self, idx: usize) -> FieldElement { self.challenges[idx].clone() } @@ -503,7 +472,6 @@ where E: IsField, { frame: &'a Frame, - periodic: &'a [FieldElement], challenges: &'a [FieldElement], alphas: &'a [FieldElement], logup_table_offset: &'a FieldElement, @@ -528,7 +496,6 @@ where ) -> Self { let TransitionEvaluationContext::Verifier { frame, - periodic_values, rap_challenges, logup_alpha_powers, logup_table_offset, @@ -540,7 +507,6 @@ where let num_constraints = ext_out.len(); Self { frame, - periodic: periodic_values, challenges: rap_challenges, alphas: logup_alpha_powers, logup_table_offset, @@ -577,9 +543,6 @@ where .get_aux_evaluation_element(0, col) .clone() } - fn periodic(&self, idx: usize) -> FieldElement { - self.periodic[idx].clone() - } fn challenge(&self, idx: usize) -> FieldElement { self.challenges[idx].clone() } @@ -610,7 +573,7 @@ where // 3. CaptureBuilder — owned expression tree, flattened into the flat IR // ============================================================================= -/// One node of the capture tree. `degree` is eager (leaf var/periodic = 1, +/// One node of the capture tree. `degree` is eager (leaf var = 1, /// constants/uniforms = 0, mul sums, add/sub max, neg passthrough — p3's /// `degree_multiple`). struct TreeNode { @@ -628,7 +591,6 @@ enum TreeKind { offset: u8, col: u16, }, - Periodic(u16), Challenge(u16), AlphaPow(u16), TableOffset, @@ -742,7 +704,6 @@ impl CaptureBuilder { match &e.0.kind { TreeKind::Main { offset, col } => self.ir.main(*offset, *col as usize), TreeKind::Aux { offset, col } => self.ir.aux(*offset, *col as usize), - TreeKind::Periodic(idx) => self.ir.periodic(*idx as usize), TreeKind::Challenge(idx) => self.ir.challenge(*idx as usize), TreeKind::AlphaPow(idx) => self.ir.alpha_power(*idx as usize), TreeKind::TableOffset => self.ir.table_offset(), @@ -797,9 +758,6 @@ impl ConstraintBuilder for CaptureBuilder { 1, ) } - fn periodic(&self, idx: usize) -> IrExpr { - IrExpr::leaf(TreeKind::Periodic(idx as u16), Dim::Base, 1) - } fn challenge(&self, idx: usize) -> IrExpr { IrExpr::leaf(TreeKind::Challenge(idx as u16), Dim::Ext, 0) } diff --git a/crypto/stark/src/constraints/builder_tests.rs b/crypto/stark/src/constraints/builder_tests.rs index 8cec6d46d..e48776422 100644 --- a/crypto/stark/src/constraints/builder_tests.rs +++ b/crypto/stark/src/constraints/builder_tests.rs @@ -185,12 +185,10 @@ fn prover_folder_matches_direct_arithmetic() { let t = random_trial(&mut rng); let step = TableView::::new(vec![t.row.clone()], vec![vec![t.aux0]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let challenges = vec![t.challenge0]; let alphas = vec![t.alpha0]; let ctx = TransitionEvaluationContext::new_prover( &frame, - &periodic, &challenges, &alphas, &t.offset, @@ -225,12 +223,10 @@ fn prover_folder_matches_interpreted_capture() { let t = random_trial(&mut rng); let step = TableView::::new(vec![t.row.clone()], vec![vec![t.aux0]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let challenges = vec![t.challenge0]; let alphas = vec![t.alpha0]; let ctx = TransitionEvaluationContext::new_prover( &frame, - &periodic, &challenges, &alphas, &t.offset, @@ -266,12 +262,10 @@ fn verifier_folder_matches_interpreted_capture() { let row_e: Vec = t.row.iter().map(|x| x.to_extension()).collect(); let step = TableView::::new(vec![row_e], vec![vec![t.aux0]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let challenges = vec![t.challenge0]; let alphas = vec![t.alpha0]; let ctx = TransitionEvaluationContext::::new_verifier( &frame, - &periodic, &challenges, &alphas, &t.offset, @@ -363,18 +357,11 @@ fn prover_folder_missing_emit_asserts() { let shifts = PackingShifts::::new(); let step = TableView::::new(vec![vec![FpE::zero(); cols::NUM_COLS]], vec![vec![]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let challenges: Vec = vec![]; let alphas: Vec = vec![]; let offset = ExtE::zero(); - let ctx = TransitionEvaluationContext::new_prover( - &frame, - &periodic, - &challenges, - &alphas, - &offset, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &challenges, &alphas, &offset, &shifts); let mut base_out = vec![FpE::zero(); 2]; let mut ext_out = vec![ExtE::zero(); 2]; @@ -391,18 +378,11 @@ fn prover_folder_double_emit_asserts() { let shifts = PackingShifts::::new(); let step = TableView::::new(vec![vec![FpE::zero(); cols::NUM_COLS]], vec![vec![]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let challenges: Vec = vec![]; let alphas: Vec = vec![]; let offset = ExtE::zero(); - let ctx = TransitionEvaluationContext::new_prover( - &frame, - &periodic, - &challenges, - &alphas, - &offset, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &challenges, &alphas, &offset, &shifts); let mut base_out = vec![FpE::zero(); 2]; let mut ext_out = vec![ExtE::zero(); 2]; @@ -420,13 +400,11 @@ fn verifier_folder_missing_emit_asserts() { let shifts = PackingShifts::::new(); let step = TableView::::new(vec![vec![ExtE::zero(); cols::NUM_COLS]], vec![vec![]]); let frame = Frame::::new(vec![step]); - let periodic: Vec = vec![]; let challenges: Vec = vec![]; let alphas: Vec = vec![]; let offset = ExtE::zero(); let ctx = TransitionEvaluationContext::::new_verifier( &frame, - &periodic, &challenges, &alphas, &offset, @@ -464,9 +442,6 @@ impl ConstraintBuilder for CountingCapture { fn aux(&self, offset: usize, col: usize) -> Self::ExprE { self.inner.aux(offset, col) } - fn periodic(&self, idx: usize) -> Self::Expr { - self.inner.periodic(idx) - } fn challenge(&self, idx: usize) -> Self::ExprE { self.inner.challenge(idx) } @@ -619,15 +594,8 @@ fn next_row_aux_and_multi_alpha_folder_matches_capture() { .map(|s| TableView::new(vec![rows[s].clone()], vec![auxs[s].clone()])) .collect(); let frame = Frame::::new(steps); - let periodic: Vec = vec![]; - let ctx = TransitionEvaluationContext::new_prover( - &frame, - &periodic, - &challenges, - &alphas, - &offset, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &challenges, &alphas, &offset, &shifts); let mut folder_base = vec![FpE::zero(); num_base]; let mut folder_ext = vec![ExtE::zero(); meta.len()]; @@ -659,10 +627,8 @@ fn next_row_aux_and_multi_alpha_folder_matches_capture() { }) .collect(); let frame_e = Frame::::new(steps_e); - let periodic_e: Vec = vec![]; let vctx = TransitionEvaluationContext::::new_verifier( &frame_e, - &periodic_e, &challenges, &alphas, &offset, diff --git a/crypto/stark/src/constraints/evaluator.rs b/crypto/stark/src/constraints/evaluator.rs index 6e94473b7..17b42b058 100644 --- a/crypto/stark/src/constraints/evaluator.rs +++ b/crypto/stark/src/constraints/evaluator.rs @@ -1,11 +1,11 @@ use super::boundary::BoundaryConstraints; use crate::domain::Domain; +use crate::frame::Frame; use crate::lookup::{BusPublicInputs, LOGUP_CHALLENGE_ALPHA, PackingShifts, compute_alpha_powers}; use crate::trace::LDETraceTable; use crate::traits::{AIR, TransitionEvaluationContext, ZerofierEvaluations}; -use crate::{frame::Frame, prover::evaluate_polynomial_on_lde_domain}; +use math::field::element::FieldElement; use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; -use math::{fft::errors::FFTError, field::element::FieldElement}; #[cfg(feature = "parallel")] use rayon::{ iter::IndexedParallelIterator, @@ -30,19 +30,17 @@ where { /// Evaluate transition + boundary constraints across the entire LDE domain. /// - /// Uses `map_init` for per-thread buffer reuse (transition evaluations + periodic values) + /// Uses `map_init` for per-thread buffer reuse (transition evaluations) /// and `ZerofierEvaluations` for deduplicated zerofier access. #[allow(clippy::too_many_arguments)] fn evaluate_transitions( air: &dyn AIR, lde_trace: &LDETraceTable, - lde_periodic_columns: &[Vec>], rap_challenges: &[FieldElement], zerofier_data: &ZerofierEvaluations, transition_coefficients: &[FieldElement], boundary_evaluation: Vec>, num_transition: usize, - num_periodic: usize, offsets: &[usize], logup_table_offset: &FieldElement, ) -> Vec> { @@ -80,18 +78,12 @@ where boundary: FieldElement, transition_buf: &mut [FieldElement], base_buf: &mut [FieldElement], - periodic_buf: &mut [FieldElement], frame: &mut Frame| -> FieldElement { frame.fill_from_lde(lde_trace, i, offsets); - for (j, col) in lde_periodic_columns.iter().enumerate() { - periodic_buf[j] = col[i].clone(); - } - let ctx = TransitionEvaluationContext::new_prover( frame, - periodic_buf, rap_challenges, &logup_alpha_powers, logup_table_offset, @@ -144,7 +136,6 @@ where ( vec![FieldElement::::zero(); num_transition], vec![FieldElement::::zero(); num_base], - vec![FieldElement::::zero(); num_periodic], Frame::preallocate( num_offsets, rows_per_step, @@ -153,8 +144,8 @@ where ), ) }, - |(transition_buf, base_buf, periodic_buf, frame), (i, boundary)| { - eval_row(i, boundary, transition_buf, base_buf, periodic_buf, frame) + |(transition_buf, base_buf, frame), (i, boundary)| { + eval_row(i, boundary, transition_buf, base_buf, frame) }, ) .collect() @@ -164,7 +155,6 @@ where { let mut transition_buf = vec![FieldElement::::zero(); num_transition]; let mut base_buf = vec![FieldElement::::zero(); num_base]; - let mut periodic_buf = vec![FieldElement::::zero(); num_periodic]; let mut frame = Frame::preallocate(num_offsets, rows_per_step, num_main_cols, num_aux_cols); @@ -172,14 +162,7 @@ where .into_iter() .enumerate() .map(|(i, boundary)| { - eval_row( - i, - boundary, - &mut transition_buf, - &mut base_buf, - &mut periodic_buf, - &mut frame, - ) + eval_row(i, boundary, &mut transition_buf, &mut base_buf, &mut frame) }) .collect() } @@ -247,21 +230,6 @@ where }) .collect::>>>(); - let trace_length = domain.interpolation_domain_size; - let lde_periodic_columns = air - .get_periodic_column_polynomials(trace_length) - .iter() - .map(|poly| { - evaluate_polynomial_on_lde_domain( - poly, - domain.blowup_factor, - domain.interpolation_domain_size, - &domain.coset_offset, - ) - }) - .collect::>>, FFTError>>() - .unwrap(); - // Fused boundary evaluation: compute (trace[col] - value) on-the-fly // instead of pre-computing all boundary_polys_evaluations. // This eliminates N_constraints × LDE_size intermediate allocations. @@ -298,19 +266,16 @@ where // boundary constraints. let num_transition = air.num_transition_constraints(); - let num_periodic = lde_periodic_columns.len(); let offsets = &air.context().transition_offsets; Self::evaluate_transitions( air, lde_trace, - &lde_periodic_columns, rap_challenges, &zerofier_data, transition_coefficients, boundary_evaluation, num_transition, - num_periodic, offsets, &self.logup_table_offset, ) diff --git a/crypto/stark/src/constraints/zerofier.rs b/crypto/stark/src/constraints/zerofier.rs index f9ec83a6e..9308c9674 100644 --- a/crypto/stark/src/constraints/zerofier.rs +++ b/crypto/stark/src/constraints/zerofier.rs @@ -2,12 +2,12 @@ //! //! The production zerofier path: `AIR::transition_zerofier_evaluations_grouped` //! (prover) and the verifier's OOD zerofier denominators both evaluate these -//! over each constraint's plain metadata (`period` / `offset` / -//! `exemptions_period` / `periodic_exemptions_offset` / `end_exemptions`). -//! The bodies were relocated verbatim from the deleted boxed-constraint trait's -//! default methods (equivalence was asserted by migration-time tests). - -use core::ops::Div; +//! over each constraint's plain metadata. Every constraint applies to every +//! row of the trace, so the zerofier is `x^N − 1` corrected by the constraint's +//! `end_exemptions` (the last rows it must skip). +//! The bodies were relocated from the deleted boxed-constraint trait's default +//! methods, specialized to the every-row shape (equivalence was asserted by +//! migration-time tests). use math::field::element::FieldElement; use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; @@ -29,11 +29,10 @@ pub fn end_exemptions_roots( if end_exemptions == 0 { return Vec::new(); } - // Last row in the constraint's evaluation domain is g^(offset + N - period); - // walking backward by g^period gives the remaining end-exemption roots. - let period = meta.period; - let decrement = trace_primitive_root.pow(trace_length - period); - let mut current = trace_primitive_root.pow(meta.offset + trace_length - period); + // The last row of the trace is g^(N-1); walking backward by g^-1 = g^(N-1) + // gives the remaining end-exemption roots. + let decrement = trace_primitive_root.pow(trace_length - 1); + let mut current = trace_primitive_root.pow(trace_length - 1); let mut roots = Vec::with_capacity(end_exemptions); for _ in 0..end_exemptions { roots.push(current.clone()); @@ -70,122 +69,57 @@ pub fn end_exemptions_lde_evaluations( /// Compute evaluations of the constraint's zerofier over a LDE domain. /// -/// With no end exemptions the zerofier is cyclic, so a short period-length -/// vector is returned and the consumer cycles it (same contract as the trait -/// default this body was moved from). -#[allow(unstable_name_collisions)] +/// With no end exemptions the zerofier `1/(x^N − 1)` is cyclic over the LDE +/// coset, so a short blowup-length vector is returned and the consumer cycles +/// it (same contract as the trait default this body was moved from). pub fn zerofier_evaluations_on_extended_domain( meta: &ConstraintMeta, domain: &Domain, ) -> Vec> { let blowup_factor = domain.blowup_factor; let trace_length = domain.trace_roots_of_unity.len(); - let trace_primitive_root = &domain.trace_primitive_root; let coset_offset = &domain.coset_offset; let lde_root_order = u64::from((blowup_factor * trace_length).trailing_zeros()); let lde_root = F::get_primitive_root_of_unity(lde_root_order).unwrap(); - // If there is an exemptions period defined for this constraint, the evaluations are calculated directly - // by computing P_exemptions(x) / Zerofier(x) - if let Some(exemptions_period) = meta.exemptions_period { - debug_assert!(exemptions_period.is_multiple_of(meta.period)); - debug_assert!(meta.periodic_exemptions_offset.is_some()); - - // The elements of the domain have order `trace_length * blowup_factor`, so the zerofier evaluations - // without the end exemptions, repeat their values after `blowup_factor * exemptions_period` iterations, - // so we only need to compute those. - let last_exponent = blowup_factor * exemptions_period; - let numerator_power = trace_length / exemptions_period; - let denominator_power = trace_length / meta.period; - let offset_exponent = - trace_length * meta.periodic_exemptions_offset.unwrap() / exemptions_period; - let numerator_offset = trace_primitive_root.pow(offset_exponent); - let denominator_offset = trace_primitive_root.pow(meta.offset * denominator_power); - let numerator_step = lde_root.pow(numerator_power); - let denominator_step = lde_root.pow(denominator_power); - let mut numerator_eval = coset_offset.pow(numerator_power); - let mut denominator_eval = coset_offset.pow(denominator_power); - - let mut numerators = Vec::with_capacity(last_exponent); - let mut denominators = Vec::with_capacity(last_exponent); - for _ in 0..last_exponent { - numerators.push(&numerator_eval - &numerator_offset); - denominators.push(&denominator_eval - &denominator_offset); - numerator_eval = &numerator_eval * &numerator_step; - denominator_eval = &denominator_eval * &denominator_step; - } - - // Batch inversion: O(3N) muls + 1 inversion instead of N individual inversions. - // Denominators are guaranteed non-zero because the sets of powers of - // `offset_times_x` and `trace_primitive_root` are disjoint, provided that the - // offset is neither an element of the interpolation domain nor part of a - // subgroup with order less than n. - FieldElement::inplace_batch_inverse(&mut denominators).unwrap(); - - let evaluations: Vec<_> = numerators - .iter() - .zip(denominators.iter()) - .map(|(num, denom_inv)| num * denom_inv) - .collect(); - - // Mirror the else-branch fast path: with no end exemptions the zerofier stays - // cyclic, so return the short period-length vector and let the consumer cycle. - if meta.end_exemptions == 0 { - return evaluations; - } - - let end_exemption_evaluations = end_exemptions_lde_evaluations(meta, domain); - - let cycled_evaluations = evaluations - .iter() - .cycle() - .take(end_exemption_evaluations.len()); - - core::iter::zip(cycled_evaluations, end_exemption_evaluations) - .map(|(eval, exemption_eval)| eval * exemption_eval) - .collect() - - // In this else branch, the zerofiers are computed as the numerator, then inverted - // using batch inverse and then multiplied by P_exemptions(x). This way we don't do - // useless divisions. - } else { - let last_exponent = blowup_factor * meta.period; - let denominator_power = trace_length / meta.period; - let denominator_offset = trace_primitive_root.pow(meta.offset * denominator_power); - let denominator_step = lde_root.pow(denominator_power); - let mut denominator_eval = coset_offset.pow(denominator_power); - - let mut evaluations = Vec::with_capacity(last_exponent); - for _ in 0..last_exponent { - evaluations.push(&denominator_eval - &denominator_offset); - denominator_eval = &denominator_eval * &denominator_step; - } + // The zerofiers are computed as the numerator, then inverted using batch + // inverse and then multiplied by P_exemptions(x). This way we don't do + // useless divisions. x^N over the LDE coset repeats after blowup_factor + // points, so only those are computed. + let last_exponent = blowup_factor; + let denominator_offset = FieldElement::::one(); + let denominator_step = lde_root.pow(trace_length); + let mut denominator_eval = coset_offset.pow(trace_length); + + let mut evaluations = Vec::with_capacity(last_exponent); + for _ in 0..last_exponent { + evaluations.push(&denominator_eval - &denominator_offset); + denominator_eval = &denominator_eval * &denominator_step; + } - FieldElement::inplace_batch_inverse(&mut evaluations).unwrap(); + FieldElement::inplace_batch_inverse(&mut evaluations).unwrap(); - // Fast path: when end_exemptions == 0 there are no exemption roots, so - // the zerofier stays cyclic — return the short period-length vector - // directly instead of expanding it over the full LDE domain. - if meta.end_exemptions == 0 { - return evaluations; - } + // Fast path: when end_exemptions == 0 there are no exemption roots, so + // the zerofier stays cyclic — return the short blowup-length vector + // directly instead of expanding it over the full LDE domain. + if meta.end_exemptions == 0 { + return evaluations; + } - let end_exemption_evaluations = end_exemptions_lde_evaluations(meta, domain); + let end_exemption_evaluations = end_exemptions_lde_evaluations(meta, domain); - let cycled_evaluations = evaluations - .iter() - .cycle() - .take(end_exemption_evaluations.len()); + let cycled_evaluations = evaluations + .iter() + .cycle() + .take(end_exemption_evaluations.len()); - core::iter::zip(cycled_evaluations, end_exemption_evaluations) - .map(|(eval, exemption_eval)| eval * exemption_eval) - .collect() - } + core::iter::zip(cycled_evaluations, end_exemption_evaluations) + .map(|(eval, exemption_eval)| eval * exemption_eval) + .collect() } /// Evaluation of the constraint's zerofier at some point `z`, which may be in /// a field extension. -#[allow(unstable_name_collisions)] pub fn evaluate_zerofier( meta: &ConstraintMeta, z: &FieldElement, @@ -203,27 +137,9 @@ where acc * -(root.clone() - z.clone()) }); - if let Some(exemptions_period) = meta.exemptions_period { - debug_assert!(exemptions_period.is_multiple_of(meta.period)); - debug_assert!(meta.periodic_exemptions_offset.is_some()); - - let periodic_exemptions_offset = meta.periodic_exemptions_offset.unwrap(); - let offset_exponent = trace_length * periodic_exemptions_offset / exemptions_period; - - let numerator = - -trace_primitive_root.pow(offset_exponent) + z.pow(trace_length / exemptions_period); - let denominator = -trace_primitive_root.pow(meta.offset * trace_length / meta.period) - + z.pow(trace_length / meta.period); - // The denominator is non-zero: z is sampled outside the set of primitive roots. - return numerator - .div(denominator) - .expect("zerofier denominator is non-zero: z is sampled out-of-domain") - * &end_exemptions_eval; - } - - (-trace_primitive_root.pow(meta.offset * trace_length / meta.period) - + z.pow(trace_length / meta.period)) - .inv() - .unwrap() + // 1/(z^N − 1), times the end-exemptions correction. + (-FieldElement::::one() + z.pow(trace_length)) + .inv() + .unwrap() * &end_exemptions_eval } diff --git a/crypto/stark/src/debug.rs b/crypto/stark/src/debug.rs index d97d0e8d3..04ce1cf62 100644 --- a/crypto/stark/src/debug.rs +++ b/crypto/stark/src/debug.rs @@ -6,12 +6,9 @@ use crate::lookup::{LOGUP_CHALLENGE_ALPHA, PackingShifts, compute_alpha_powers}; use crate::{frame::Frame, trace::LDETraceTable}; use log::{error, info}; use math::field::traits::IsSubFieldOf; -use math::{ - field::{ - element::FieldElement, - traits::{IsFFTField, IsField}, - }, - polynomial::Polynomial, +use math::field::{ + element::FieldElement, + traits::{IsFFTField, IsField}, }; /// Validates that the trace is valid with respect to the supplied AIR constraints. @@ -53,19 +50,6 @@ pub fn validate_trace< let lde_trace = LDETraceTable::from_columns(main_trace_columns, aux_trace_columns, air.step_size(), 1); - let periodic_columns: Vec<_> = air - .get_periodic_column_polynomials(domain.interpolation_domain_size) - .iter() - .map(|poly| { - Polynomial::>::evaluate_fft::( - poly, - 1, - Some(domain.interpolation_domain_size), - ) - .unwrap() - }) - .collect(); - // --------- VALIDATE BOUNDARY CONSTRAINTS ------------ let trace_length = domain.interpolation_domain_size; air.boundary_constraints(pub_inputs, rap_challenges, bus_public_inputs, trace_length) @@ -119,13 +103,8 @@ pub fn validate_trace< let packing_shifts = PackingShifts::::new(); for step in 0..lde_trace.num_steps() { let frame = Frame::read_step_from_lde(&lde_trace, step, &air.context().transition_offsets); - let periodic_values: Vec<_> = periodic_columns - .iter() - .map(|col| col[step].clone()) - .collect(); let transition_evaluation_context = TransitionEvaluationContext::new_prover( &frame, - &periodic_values, rap_challenges, &logup_alpha_powers, &logup_table_offset, diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 94b73a200..0057bdcd0 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -2321,8 +2321,6 @@ mod logup_single_source_tests { let n_aux = num_aux_cols(layout); let shifts = PackingShifts::::new(); let vshifts = PackingShifts::::new(); - let no_periodic: Vec = vec![]; - let no_periodic_e: Vec = vec![]; for trial in 0..TRIALS { let mut rng = SplitMix64::new(0xC0FF_EE00_u64 ^ (label.len() as u64) ^ trial as u64); @@ -2342,7 +2340,6 @@ mod logup_single_source_tests { let prover_ctx = TransitionEvaluationContext::new_prover( &frame, - &no_periodic, &rap_challenges, &alpha_powers, &table_offset, @@ -2382,7 +2379,6 @@ mod logup_single_source_tests { ]); let vctx = TransitionEvaluationContext::::new_verifier( &vframe, - &no_periodic_e, &rap_challenges, &alpha_powers, &table_offset, diff --git a/crypto/stark/src/traits.rs b/crypto/stark/src/traits.rs index ca9e82758..4818d8be4 100644 --- a/crypto/stark/src/traits.rs +++ b/crypto/stark/src/traits.rs @@ -1,12 +1,9 @@ use std::collections::HashMap; use crypto::fiat_shamir::is_transcript::IsStarkTranscript; -use math::{ - field::{ - element::FieldElement, - traits::{IsFFTField, IsField, IsSubFieldOf}, - }, - polynomial::Polynomial, +use math::field::{ + element::FieldElement, + traits::{IsFFTField, IsField, IsSubFieldOf}, }; use crate::{ @@ -54,13 +51,11 @@ impl ZerofierEvaluations { } /// Key identifying a unique zerofier shape — constraints with the same key share -/// the same zerofier evaluations on the extended domain. +/// the same zerofier evaluations on the extended domain. Every constraint +/// applies to every row, so the shape is fully determined by its end +/// exemptions. #[derive(Clone, Copy, Hash, Eq, PartialEq)] struct ZerofierGroupKey { - period: usize, - offset: usize, - exemptions_period: Option, - periodic_exemptions_offset: Option, end_exemptions: usize, } @@ -77,7 +72,6 @@ where { Prover { frame: &'a Frame, - periodic_values: &'a [FieldElement], rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, @@ -85,7 +79,6 @@ where }, Verifier { frame: &'a Frame, - periodic_values: &'a [FieldElement], rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, @@ -100,7 +93,6 @@ where { pub fn new_prover( frame: &'a Frame, - periodic_values: &'a [FieldElement], rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, @@ -108,7 +100,6 @@ where ) -> Self { Self::Prover { frame, - periodic_values, rap_challenges, logup_alpha_powers, logup_table_offset, @@ -118,7 +109,6 @@ where pub fn new_verifier( frame: &'a Frame, - periodic_values: &'a [FieldElement], rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, @@ -126,7 +116,6 @@ where ) -> Self { Self::Verifier { frame, - periodic_values, rap_challenges, logup_alpha_powers, logup_table_offset, @@ -295,30 +284,6 @@ pub trait AIR: Send + Sync { self.context().num_transition_constraints } - fn get_periodic_column_values(&self) -> Vec>> { - vec![] - } - - fn get_periodic_column_polynomials( - &self, - trace_length: usize, - ) -> Vec>> { - let mut result = Vec::new(); - for periodic_column in self.get_periodic_column_values() { - let values: Vec<_> = periodic_column - .iter() - .cycle() - .take(trace_length) - .cloned() - .collect(); - let poly = - Polynomial::>::interpolate_fft::(&values) - .unwrap(); - result.push(poly); - } - result - } - /// Compute zerofier evaluations as deduplicated groups with index mapping. /// /// Each unique zerofier (keyed by period/offset/exemption parameters) is @@ -336,10 +301,6 @@ pub trait AIR: Send + Sync { meta.iter().for_each(|m| { let key = ZerofierGroupKey { - period: m.period, - offset: m.offset, - exemptions_period: m.exemptions_period, - periodic_exemptions_offset: m.periodic_exemptions_offset, end_exemptions: m.end_exemptions, }; let group_idx = *zerofier_groups_map.entry(key).or_insert_with(|| { diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index ba3dfd518..ff0a7b024 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -164,12 +164,6 @@ pub trait IsStarkVerifier< .map(|((num, den), beta)| num * den * beta) .fold(FieldElement::::zero(), |acc, x| acc + x); - let periodic_values = air - .get_periodic_column_polynomials(trace_length) - .iter() - .map(|poly| poly.evaluate(&challenges.z)) - .collect::>>(); - let num_main_trace_columns = proof.trace_ood_evaluations.width - air.num_auxiliary_rap_columns(); @@ -199,7 +193,6 @@ pub trait IsStarkVerifier< let packing_shifts = PackingShifts::::new(); let transition_evaluation_context = TransitionEvaluationContext::new_verifier( &ood_frame, - &periodic_values, &challenges.rap_challenges, &logup_alpha_powers, &logup_table_offset, diff --git a/prover/src/tests/constraint_emit_tests.rs b/prover/src/tests/constraint_emit_tests.rs index b1f7cf133..0cfa8ac84 100644 --- a/prover/src/tests/constraint_emit_tests.rs +++ b/prover/src/tests/constraint_emit_tests.rs @@ -105,8 +105,6 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { let shifts = PackingShifts::::new(); let vshifts = PackingShifts::::new(); - let no_periodic: Vec = vec![]; - let no_periodic_e: Vec = vec![]; let no_ch: Vec = vec![]; let offset_e = Fp3::zero(); @@ -117,14 +115,8 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); - let ctx = TransitionEvaluationContext::new_prover( - &frame, - &no_periodic, - &no_ch, - &no_ch, - &offset_e, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &no_ch, &no_ch, &offset_e, &shifts); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); @@ -135,12 +127,7 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( - &frame_e, - &no_periodic_e, - &no_ch, - &no_ch, - &offset_e, - &vshifts, + &frame_e, &no_ch, &no_ch, &offset_e, &vshifts, ); let mut vext_out = vec![Fp3::zero(); n]; let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); diff --git a/prover/src/tests/constraint_set_tests_a.rs b/prover/src/tests/constraint_set_tests_a.rs index 172d8b720..2387f0cad 100644 --- a/prover/src/tests/constraint_set_tests_a.rs +++ b/prover/src/tests/constraint_set_tests_a.rs @@ -90,8 +90,6 @@ where let shifts = PackingShifts::::new(); let vshifts = PackingShifts::::new(); - let no_periodic: Vec = vec![]; - let no_periodic_e: Vec = vec![]; let no_ch: Vec = vec![]; let offset_e = Fp3::zero(); @@ -102,14 +100,8 @@ where // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); - let ctx = TransitionEvaluationContext::new_prover( - &frame, - &no_periodic, - &no_ch, - &no_ch, - &offset_e, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &no_ch, &no_ch, &offset_e, &shifts); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); @@ -120,12 +112,7 @@ where let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( - &frame_e, - &no_periodic_e, - &no_ch, - &no_ch, - &offset_e, - &vshifts, + &frame_e, &no_ch, &no_ch, &offset_e, &vshifts, ); let mut vext_out = vec![Fp3::zero(); n]; let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index a7d0af6be..c81a03008 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -83,8 +83,6 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz let shifts = PackingShifts::::new(); let vshifts = PackingShifts::::new(); - let no_periodic: Vec = vec![]; - let no_periodic_e: Vec = vec![]; let no_ch: Vec = vec![]; let offset_e = Fp3::zero(); @@ -95,14 +93,8 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); - let ctx = TransitionEvaluationContext::new_prover( - &frame, - &no_periodic, - &no_ch, - &no_ch, - &offset_e, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(&frame, &no_ch, &no_ch, &offset_e, &shifts); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); @@ -113,12 +105,7 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( - &frame_e, - &no_periodic_e, - &no_ch, - &no_ch, - &offset_e, - &vshifts, + &frame_e, &no_ch, &no_ch, &offset_e, &vshifts, ); let mut vext_out = vec![Fp3::zero(); n]; let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); diff --git a/prover/src/tests/cpu32_tests.rs b/prover/src/tests/cpu32_tests.rs index 835da27d9..8627e107a 100644 --- a/prover/src/tests/cpu32_tests.rs +++ b/prover/src/tests/cpu32_tests.rs @@ -25,8 +25,7 @@ fn eval_cpu32(row: &[FE]) -> Vec { let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &[], &no_e, &no_e, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &no_e, &no_e, &offset_e, &shifts); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ec_scalar_tests.rs b/prover/src/tests/ec_scalar_tests.rs index 2a7cc1f1e..01afec94e 100644 --- a/prover/src/tests/ec_scalar_tests.rs +++ b/prover/src/tests/ec_scalar_tests.rs @@ -27,8 +27,7 @@ fn eval_row(trace: &TraceTable, row: usize let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &[], &no_e, &no_e, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &no_e, &no_e, &offset_e, &shifts); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ecdas_tests.rs b/prover/src/tests/ecdas_tests.rs index ccb69f5d9..bb5083323 100644 --- a/prover/src/tests/ecdas_tests.rs +++ b/prover/src/tests/ecdas_tests.rs @@ -58,8 +58,7 @@ fn eval_row(trace: &TraceTable, row: usize let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &[], &no_e, &no_e, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &no_e, &no_e, &offset_e, &shifts); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ecsm_tests.rs b/prover/src/tests/ecsm_tests.rs index 18607f9ac..7ba04dc7d 100644 --- a/prover/src/tests/ecsm_tests.rs +++ b/prover/src/tests/ecsm_tests.rs @@ -54,8 +54,7 @@ fn eval_row(trace: &TraceTable, row: usize let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &[], &no_e, &no_e, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover(&frame, &no_e, &no_e, &offset_e, &shifts); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); From e76807ee04b21bcf59be1b8bc59f4277fc83ac04 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 11:38:14 -0300 Subject: [PATCH 35/52] reports: final-tip cross-verification evidence (post periodic rip + meta trim) scripts/cross_verify_vm.sh 2499b2a8 5d725904: all 3 ELFs, both directions. scripts/cross_verify_examples.sh 88adbfa6 5d725904: all 11 examples, both directions (22/22). --- .../sscs/cross_verify_examples_final-tip.log | 32 +++++++++++++++++++ reports/sscs/cross_verify_vm_final-tip.log | 17 ++++++++++ 2 files changed, 49 insertions(+) create mode 100644 reports/sscs/cross_verify_examples_final-tip.log create mode 100644 reports/sscs/cross_verify_vm_final-tip.log diff --git a/reports/sscs/cross_verify_examples_final-tip.log b/reports/sscs/cross_verify_examples_final-tip.log new file mode 100644 index 000000000..4b2c38dc5 --- /dev/null +++ b/reports/sscs/cross_verify_examples_final-tip.log @@ -0,0 +1,32 @@ +==> Refs + OLD 88adbfa6 -> 88adbfa64c + NEW 5d725904 -> 5d7259041a +Preparing worktree (detached HEAD 88adbfa6) +==> Building examples_cli @ 88adbfa64c -> cli_old +==> Building examples_cli @ 5d7259041a -> cli_new +==> Cross-verifying 11 examples, both directions +PASS prove-NEW-verify-OLD : simple_fibonacci +PASS prove-OLD-verify-NEW : simple_fibonacci +PASS prove-NEW-verify-OLD : fibonacci_2_columns +PASS prove-OLD-verify-NEW : fibonacci_2_columns +PASS prove-NEW-verify-OLD : fibonacci_2_cols_shifted +PASS prove-OLD-verify-NEW : fibonacci_2_cols_shifted +PASS prove-NEW-verify-OLD : fibonacci_multi_column +PASS prove-OLD-verify-NEW : fibonacci_multi_column +PASS prove-NEW-verify-OLD : quadratic_air +PASS prove-OLD-verify-NEW : quadratic_air +PASS prove-NEW-verify-OLD : fibonacci_rap +PASS prove-OLD-verify-NEW : fibonacci_rap +PASS prove-NEW-verify-OLD : dummy_air +PASS prove-OLD-verify-NEW : dummy_air +PASS prove-NEW-verify-OLD : simple_addition +PASS prove-OLD-verify-NEW : simple_addition +PASS prove-NEW-verify-OLD : read_only_memory +PASS prove-OLD-verify-NEW : read_only_memory +PASS prove-NEW-verify-OLD : read_only_memory_logup +PASS prove-OLD-verify-NEW : read_only_memory_logup +PASS prove-NEW-verify-OLD : multi_table_lookup +PASS prove-OLD-verify-NEW : multi_table_lookup + +==> RESULT: all 11 examples cross-verify in both directions. +EXIT=0 diff --git a/reports/sscs/cross_verify_vm_final-tip.log b/reports/sscs/cross_verify_vm_final-tip.log new file mode 100644 index 000000000..cf86bd3bd --- /dev/null +++ b/reports/sscs/cross_verify_vm_final-tip.log @@ -0,0 +1,17 @@ +==> Refs + OLD 2499b2a8 -> 2499b2a821 + NEW 5d725904 -> 5d7259041a +==> ELFs: /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/sub.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/add.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/arith_8.elf +Preparing worktree (detached HEAD 2499b2a8) +==> Building cli @ 2499b2a821 -> cli_old +==> Building cli @ 5d7259041a -> cli_new +==> Cross-verifying 3 ELFs, both directions +PASS prove-NEW-verify-OLD : sub +PASS prove-OLD-verify-NEW : sub +PASS prove-NEW-verify-OLD : add +PASS prove-OLD-verify-NEW : add +PASS prove-NEW-verify-OLD : arith_8 +PASS prove-OLD-verify-NEW : arith_8 + +==> RESULT: all 3 ELFs cross-verify in both directions. +EXIT=0 From 0d8e5d108d658728521a0d206d08c6441feb679c Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 11:50:45 -0300 Subject: [PATCH 36/52] stark: pass StorageMode to multi_prove in examples-cli under disk-spill --- crypto/stark/examples/examples_cli.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crypto/stark/examples/examples_cli.rs b/crypto/stark/examples/examples_cli.rs index 7be1d673c..ad455b1ba 100644 --- a/crypto/stark/examples/examples_cli.rs +++ b/crypto/stark/examples/examples_cli.rs @@ -573,6 +573,8 @@ fn prove_multi_table_lookup() -> Result, String> { let multi_proof = Prover::::multi_prove( air_trace_pairs, &mut DefaultTranscript::::new(&[]), + #[cfg(feature = "disk-spill")] + stark::storage_mode::StorageMode::Ram, ) .map_err(|e| format!("prove failed: {e:?}"))?; ser(&multi_proof) From b6f02711fadea9b8615209f8014bb15383eb6ee5 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 13:58:49 -0300 Subject: [PATCH 37/52] stark: fold LogUp fingerprint terms straight into the accumulator emit_fingerprint collected the alpha-value terms into a Vec before summing. The ProverEvalFolder runs the body once per LDE row, so that Vec cost a heap allocation per fingerprint per row (~20 interactions on CPU x millions of LDE rows) -- the old boxed path accumulated in place with zero allocations. Start the fingerprint at z - bus_id and subtract each alpha-value term as it is emitted instead. Field addition is associative and commutative, so the values are unchanged; the folder/capture/runtime differential tests (every Packing variant, both absorbed branches) stay green. --- crypto/stark/src/lookup.rs | 114 ++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 64 deletions(-) diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 0057bdcd0..cf10b780e 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -1845,20 +1845,22 @@ where result } -/// Capture a [`Packing`]'s fingerprint contribution as a sum of extension -/// terms, mirroring [`Packing::accumulate_fingerprint_with`]. Terms are pushed -/// to `acc` (`col_expr * alpha_power`, base operand LEFT); returns the number -/// of alpha powers consumed (`= packing.num_bus_elements()`). Field addition is -/// associative and commutative, so this row-agnostic accumulation is -/// value-identical to the runtime body regardless of grouping. +/// Fold a [`Packing`]'s fingerprint contribution into the running fingerprint +/// `fp`, mirroring [`Packing::accumulate_fingerprint_with`]. Each bus element +/// subtracts one `col_expr * alpha_power` term (base operand LEFT) from `fp` — +/// see [`emit_fingerprint`] for why terms are subtracted rather than summed. +/// Returns the updated fingerprint and the number of alpha powers consumed +/// (`= packing.num_bus_elements()`). Field addition is associative and +/// commutative, so this row-agnostic accumulation is value-identical to the +/// runtime body regardless of grouping. fn emit_packing_fingerprint( b: &B, packing: Packing, start_col: usize, offset: usize, alpha_offset: usize, - acc: &mut Vec, -) -> usize + mut fp: B::ExprE, +) -> (B::ExprE, usize) where F: IsField, E: IsField, @@ -1871,87 +1873,77 @@ where let shift_24 = || b.const_base(SHIFT_8 * SHIFT_16); match packing { - Packing::Direct => { - acc.push(col(start_col) * alpha(0)); - 1 - } + Packing::Direct => (fp - col(start_col) * alpha(0), 1), Packing::Word2L => { let combined = col(start_col) + col(start_col + 1) * shift_16(); - acc.push(combined * alpha(0)); - 1 + (fp - combined * alpha(0), 1) } Packing::Word4L => { let combined = col(start_col) + col(start_col + 1) * shift_8() + col(start_col + 2) * shift_16() + col(start_col + 3) * shift_24(); - acc.push(combined * alpha(0)); - 1 + (fp - combined * alpha(0), 1) } Packing::DWordWL => { - acc.push(col(start_col) * alpha(0)); - acc.push(col(start_col + 1) * alpha(1)); - 2 + fp = fp - col(start_col) * alpha(0); + (fp - col(start_col + 1) * alpha(1), 2) } Packing::DWordHHW => { - acc.push(col(start_col) * alpha(0)); + fp = fp - col(start_col) * alpha(0); let w = col(start_col + 1) + col(start_col + 2) * shift_16(); - acc.push(w * alpha(1)); - 2 + (fp - w * alpha(1), 2) } Packing::DWordWHH => { let w = col(start_col) + col(start_col + 1) * shift_16(); - acc.push(w * alpha(0)); - acc.push(col(start_col + 2) * alpha(1)); - 2 + fp = fp - w * alpha(0); + (fp - col(start_col + 2) * alpha(1), 2) } Packing::DWordHL => { let w0 = col(start_col) + col(start_col + 1) * shift_16(); - acc.push(w0 * alpha(0)); + fp = fp - w0 * alpha(0); let w1 = col(start_col + 2) + col(start_col + 3) * shift_16(); - acc.push(w1 * alpha(1)); - 2 + (fp - w1 * alpha(1), 2) } Packing::DWordBL => { let w0 = col(start_col) + col(start_col + 1) * shift_8() + col(start_col + 2) * shift_16() + col(start_col + 3) * shift_24(); - acc.push(w0 * alpha(0)); + fp = fp - w0 * alpha(0); let w1 = col(start_col + 4) + col(start_col + 5) * shift_8() + col(start_col + 6) * shift_16() + col(start_col + 7) * shift_24(); - acc.push(w1 * alpha(1)); - 2 + (fp - w1 * alpha(1), 2) } Packing::QuadHL => { for i in 0..4 { let c = start_col + i * 2; let w = col(c) + col(c + 1) * shift_16(); - acc.push(w * alpha(i)); + fp = fp - w * alpha(i); } - 4 + (fp, 4) } Packing::QuadWL => { for i in 0..4 { - acc.push(col(start_col + i) * alpha(i)); + fp = fp - col(start_col + i) * alpha(i); } - 4 + (fp, 4) } } } -/// Capture a [`BusValue`]'s fingerprint contribution into `acc`, mirroring -/// [`BusValue::accumulate_fingerprint_from_step`]. Returns the number of alpha -/// powers consumed. +/// Fold a [`BusValue`]'s fingerprint contribution into the running fingerprint +/// `fp`, mirroring [`BusValue::accumulate_fingerprint_from_step`]. Returns the +/// updated fingerprint and the number of alpha powers consumed. fn emit_busvalue_fingerprint( b: &B, bv: &BusValue, offset: usize, alpha_offset: usize, - acc: &mut Vec, -) -> usize + fp: B::ExprE, +) -> (B::ExprE, usize) where F: IsField, E: IsField, @@ -1967,13 +1959,12 @@ where *start_column, offset, alpha_offset, - acc, + fp, ), BusValue::Linear(terms) => { // Value-preserving: always emit the multiply (see the module note). let result = emit_linear_terms(b, terms, offset); - acc.push(result * b.alpha_pow(alpha_offset)); - 1 + (fp - result * b.alpha_pow(alpha_offset), 1) } } } @@ -1982,6 +1973,14 @@ where /// `z - (bus_id + α·v[0] + α²·v[1] + ...)`. /// /// `α⁰ = 1`: the bus-id term needs no multiply and is added as a base constant. +/// +/// The subtraction is distributed: the fingerprint starts at `z − bus_id` and +/// each α·value term is subtracted as it is emitted. Field addition is +/// associative and commutative, so this is value-identical to +/// `z − (bus + Σ terms)` — and it keeps the running value in ONE extension +/// accumulator. The prover folder runs this body once per LDE row, where +/// collecting the terms in a `Vec` costs a heap allocation per fingerprint +/// per row. fn emit_fingerprint(b: &B, interaction: &BusInteraction, offset: usize) -> B::ExprE where F: IsField, @@ -1990,29 +1989,16 @@ where { let z = b.challenge(0); let bus = b.const_base(interaction.bus_id); - // Collect the α·value terms, then fold. Field addition is associative and - // commutative, so the grouping does not change the value. - let mut terms: Vec = Vec::new(); + // `bus` is base and `z` ext; the tower only implements base − ext (base + // operand LEFT), so z − bus is written −(bus − z). + let mut fp = -(bus - z); let mut alpha_idx = 1; for bv in &interaction.values { - alpha_idx += emit_busvalue_fingerprint::(b, bv, offset, alpha_idx, &mut terms); - } - // lc = bus_id + Σ terms (base + ext = ext, base operand LEFT). - let mut iter = terms.into_iter(); - let lc = match iter.next() { - Some(first) => { - let mut lc = bus + first; - for t in iter { - lc = lc + t; - } - lc - } - // No values: fingerprint is z - bus_id. `bus` is base, `z` is ext, and - // the tower only implements base − ext (base operand LEFT), so write - // z − bus as −(bus − z). - None => return -(bus - z), - }; - z - lc + let (next, consumed) = emit_busvalue_fingerprint::(b, bv, offset, alpha_idx, fp); + fp = next; + alpha_idx += consumed; + } + fp } /// Emit the batched-term constraint for committed pair `pair_idx`: From 1d0f0ae2276b761175da0342d5efaf1b6bc209cf Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 14:23:45 -0300 Subject: [PATCH 38/52] stark: restore the LogUp Linear zero-skip via a builder fold hook The old runtime body skipped the FxE multiply when a Linear bus element evaluated to zero on a row -- covering the constant-0 bus-width padding plus any variable element that is zero on that row. The single-source port dropped the skip because data-dependent control flow cannot live in the shared body (capture has no branches). Restore it one level down instead: ConstraintBuilder gains fold_fingerprint_term (fp - v*alpha) with an unconditional default used by capture and the verifier folder, and ProverEvalFolder overrides it with the zero-skip -- value-identical (0*alpha = 0), per-row hot path only. The captured IR is unchanged, so GPU parity is unaffected. New differential test drives always-zero Linear shapes (Constant(0) padding and a column-minus-itself combination) through the folder vs the skip-free captured program, bit-for-bit. --- crypto/stark/src/constraints/builder.rs | 35 +++++++++++++++++ crypto/stark/src/lookup.rs | 51 ++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index 4863da229..b772353ad 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -123,6 +123,27 @@ pub trait ConstraintBuilder { fn emit_base(&mut self, constraint_idx: usize, e: Self::Expr); /// Record extension-field (LogUp) constraint `constraint_idx`'s value. fn emit_ext(&mut self, constraint_idx: usize, e: Self::ExprE); + + // ---- folds ---------------------------------------------------------- + /// Fold one α·value term into a running LogUp fingerprint: + /// `fp − v·α[alpha_idx]`. + /// + /// This default emits the multiply unconditionally — the only option for + /// capture (the IR has no data-dependent control flow) and correct for + /// every builder. [`ProverEvalFolder`] overrides it with a zero-skip: a + /// bus element that is zero on this row contributes nothing (`0·α = 0`), + /// so the F×E multiply is skipped. That covers the constant-0 bus-width + /// padding plus any variable element that is zero on the row, and it runs + /// once per fingerprint element per LDE row — the hot path where the old + /// runtime body had the same skip. + fn fold_fingerprint_term( + &self, + fp: Self::ExprE, + v: Self::Expr, + alpha_idx: usize, + ) -> Self::ExprE { + fp - v * self.alpha_pow(alpha_idx) + } } // ============================================================================= @@ -452,6 +473,20 @@ where self.tracker.mark(constraint_idx); self.ext_out[constraint_idx] = e; } + + fn fold_fingerprint_term( + &self, + fp: FieldElement, + v: FieldElement, + alpha_idx: usize, + ) -> FieldElement { + // Zero bus elements contribute nothing — skip the F×E multiply. + if v == FieldElement::zero() { + fp + } else { + fp - v * &self.alphas[alpha_idx] + } + } } // ============================================================================= diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index cf10b780e..6499ad1e2 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -1962,9 +1962,11 @@ where fp, ), BusValue::Linear(terms) => { - // Value-preserving: always emit the multiply (see the module note). + // Routed through the builder so the prover folder can zero-skip + // the multiply, as the old runtime body did (Linear is where the + // constant-0 bus-width padding lives). Value-identical either way. let result = emit_linear_terms(b, terms, offset); - (fp - result * b.alpha_pow(alpha_offset), 1) + (b.fold_fingerprint_term(fp, result, alpha_offset), 1) } } } @@ -2494,4 +2496,49 @@ mod logup_single_source_tests { ); } } + + #[test] + fn logup_linear_zero_skip() { + // The prover folder zero-skips the F×E multiply for Linear bus + // elements ([`ConstraintBuilder::fold_fingerprint_term`]); the random + // frames above never produce a zero element, so drive both always-zero + // shapes explicitly — the constant-0 bus-width padding and a + // column-minus-itself combination — next to a nonzero element, and + // assert the folder still matches the (skip-free) captured program + // bit-for-bit. + let zero_padded = |bus: u64, sender: bool| { + let values = vec![ + BusValue::column(1), + BusValue::linear(vec![LinearTerm::Constant(0)]), + BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: 2, + }, + LinearTerm::Column { + coefficient: -1, + column: 2, + }, + ]), + BusValue::linear(vec![LinearTerm::Column { + coefficient: 3, + column: 3, + }]), + ]; + if sender { + BusInteraction::sender(bus, Multiplicity::Column(0), values) + } else { + BusInteraction::receiver(bus, Multiplicity::Column(0), values) + } + }; + let interactions = vec![ + zero_padded(3, true), + zero_padded(5, false), + zero_padded(7, true), + ]; + let layout = LogUpLayout::from_interactions(interactions); + assert_eq!(layout.num_committed_pairs, 1); + assert_eq!(layout.absorbed().len(), 1); + check_layout("linear_zero_skip", &layout, 8); + } } From cfc4f1de7f695c17018df78caffbd649d5f625fa Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 15:03:15 -0300 Subject: [PATCH 39/52] prover: stop heap-allocating AddOperands inside the per-row bodies The boxed->ConstraintSet migration moved AddOperand construction from table setup into ConstraintSet::eval, which the prover folder runs once per LDE row. Every Linear operand allocated two Vecs, so the CPU table paid 4 heap allocations per row, CPU32 4, COMMIT 6, EQ 2, and KECCAK ~150 (three operands per lane, 25 lanes) -- the same setup-to-per-row migration class as the LogUp fingerprint Vec. Give AddOperand::Linear inline term storage instead: AddTerms is a fixed [AddLinearTerm; 4] + len (from_dword_bl's byte-packed limb is the widest at 4 terms), Deref to a slice so the emit helpers and tests are unchanged. Constructing an operand is now pure stack writes of constants, which LLVM folds. Also drop the per-row Vec of bit columns in EC_SCALAR's eval (iterator chain instead). No arithmetic change anywhere -- allocation behavior only. --- prover/src/constraints/templates.rs | 95 ++++++++++++++++++----- prover/src/tables/ec_scalar.rs | 11 +-- prover/src/tests/constraint_emit_tests.rs | 4 +- prover/src/tests/constraints_tests.rs | 8 +- 4 files changed, 86 insertions(+), 32 deletions(-) diff --git a/prover/src/constraints/templates.rs b/prover/src/constraints/templates.rs index 5c66044f6..bcfe2e7d7 100644 --- a/prover/src/constraints/templates.rs +++ b/prover/src/constraints/templates.rs @@ -37,7 +37,7 @@ pub const INV_SHIFT_32: u64 = 18446744065119617026; /// /// Uses i64 for coefficients to support negative values (e.g., `4 - 2*c`). /// Converted to FieldElement in eval(). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum AddLinearTerm { /// coefficient * column_value Column { @@ -50,6 +50,55 @@ pub enum AddLinearTerm { Constant(i64), } +/// Inline term storage for one limb of an [`AddOperand::Linear`]: at most +/// 4 terms (the byte-packed [`AddOperand::from_dword_bl`] limb is the widest). +/// +/// Operands are constructed INSIDE the per-row constraint bodies (the CPU +/// table builds two per row; KECCAK builds three per lane × 25 lanes), so +/// this must not heap-allocate — a `Vec` here costs allocations per operand +/// per LDE row. +#[derive(Debug, Clone, Copy)] +pub struct AddTerms { + terms: [AddLinearTerm; Self::CAP], + len: u8, +} + +impl AddTerms { + const CAP: usize = 4; + const FILL: AddLinearTerm = AddLinearTerm::Constant(0); + + /// The empty term list (a zero limb). + pub const fn empty() -> Self { + Self { + terms: [Self::FILL; Self::CAP], + len: 0, + } + } + + /// Term list from a slice. Panics if given more than 4 terms. + pub fn of(source: &[AddLinearTerm]) -> Self { + assert!( + source.len() <= Self::CAP, + "AddTerms holds at most {} terms, got {}", + Self::CAP, + source.len() + ); + let mut terms = [Self::FILL; Self::CAP]; + terms[..source.len()].copy_from_slice(source); + Self { + terms, + len: source.len() as u8, + } + } +} + +impl core::ops::Deref for AddTerms { + type Target = [AddLinearTerm]; + fn deref(&self) -> &[AddLinearTerm] { + &self.terms[..self.len as usize] + } +} + /// An ADD operand representing a 64-bit value as [lo, hi] words. /// /// Supports various representations: @@ -62,7 +111,7 @@ pub enum AddLinearTerm { /// - DWordHL → DWordWL: `AddOperand::from_dword_hl(col)` → repack 4 halves /// - DWordBL → DWordWL: `AddOperand::from_dword_bl(col)` → repack 8 bytes /// - Expressions: `AddOperand::linear(...)` → arbitrary linear combinations -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum AddOperand { /// Two consecutive columns (DWordWL): evaluates to [col, col+1] DWordWL { start_column: usize }, @@ -71,9 +120,9 @@ pub enum AddOperand { /// Handles: constants, single columns, expressions, and virtual columns. Linear { /// Terms for the low 32-bit word - lo: Vec, + lo: AddTerms, /// Terms for the high 32-bit word (empty = zero) - hi: Vec, + hi: AddTerms, }, } @@ -91,8 +140,8 @@ impl AddOperand { /// hi = 0 (since constants fit in 32 bits for VM use cases). pub fn constant(value: i64) -> Self { AddOperand::Linear { - lo: vec![AddLinearTerm::Constant(value)], - hi: vec![], + lo: AddTerms::of(&[AddLinearTerm::Constant(value)]), + hi: AddTerms::empty(), } } @@ -100,11 +149,11 @@ impl AddOperand { /// hi = 0. pub fn from_word(col: usize) -> Self { AddOperand::Linear { - lo: vec![AddLinearTerm::Column { + lo: AddTerms::of(&[AddLinearTerm::Column { coefficient: 1, column: col, - }], - hi: vec![], + }]), + hi: AddTerms::empty(), } } @@ -113,7 +162,7 @@ impl AddOperand { /// hi = h[2] + 2^16 * h[3] pub fn from_dword_hl(start_column: usize) -> Self { AddOperand::Linear { - lo: vec![ + lo: AddTerms::of(&[ AddLinearTerm::Column { coefficient: 1, column: start_column, @@ -122,8 +171,8 @@ impl AddOperand { coefficient: 1 << 16, column: start_column + 1, }, - ], - hi: vec![ + ]), + hi: AddTerms::of(&[ AddLinearTerm::Column { coefficient: 1, column: start_column + 2, @@ -132,7 +181,7 @@ impl AddOperand { coefficient: 1 << 16, column: start_column + 3, }, - ], + ]), } } @@ -141,7 +190,7 @@ impl AddOperand { /// hi = b[4] + 2^8*b[5] + 2^16*b[6] + 2^24*b[7] pub fn from_dword_bl(start_column: usize) -> Self { AddOperand::Linear { - lo: vec![ + lo: AddTerms::of(&[ AddLinearTerm::Column { coefficient: 1, column: start_column, @@ -158,8 +207,8 @@ impl AddOperand { coefficient: 1 << 24, column: start_column + 3, }, - ], - hi: vec![ + ]), + hi: AddTerms::of(&[ AddLinearTerm::Column { coefficient: 1, column: start_column + 4, @@ -176,14 +225,18 @@ impl AddOperand { coefficient: 1 << 24, column: start_column + 7, }, - ], + ]), } } - /// Creates a Linear operand from explicit lo/hi term lists. - /// Use this for complex expressions like `4 - 2*c` or virtual columns. - pub fn linear(lo: Vec, hi: Vec) -> Self { - AddOperand::Linear { lo, hi } + /// Creates a Linear operand from explicit lo/hi term lists (at most 4 + /// terms per limb). Use this for complex expressions like `4 - 2*c` or + /// virtual columns. + pub fn linear(lo: &[AddLinearTerm], hi: &[AddLinearTerm]) -> Self { + AddOperand::Linear { + lo: AddTerms::of(lo), + hi: AddTerms::of(hi), + } } } diff --git a/prover/src/tables/ec_scalar.rs b/prover/src/tables/ec_scalar.rs index 87040f2bb..7f8614cfe 100644 --- a/prover/src/tables/ec_scalar.rs +++ b/prover/src/tables/ec_scalar.rs @@ -292,11 +292,12 @@ impl ConstraintSet for EcScalarConstraints fn eval>(&self, b: &mut B) { // idx 0..10: unconditional IS_BIT `x·(1−x)` for - // [mu, limb_bit(0..8), last_limb], in that column order. - let mut bit_cols = vec![cols::MU]; - bit_cols.extend((0..8).map(cols::limb_bit)); - bit_cols.push(cols::LAST_LIMB); - for (i, &col) in bit_cols.iter().enumerate() { + // [mu, limb_bit(0..8), last_limb], in that column order. Iterator + // chain, not a Vec: eval runs once per LDE row. + let bit_cols = core::iter::once(cols::MU) + .chain((0..8).map(cols::limb_bit)) + .chain(core::iter::once(cols::LAST_LIMB)); + for (i, col) in bit_cols.enumerate() { let x = b.main(0, col); let one = b.one(); b.emit_base(i, x.clone() * (one - x)); diff --git a/prover/src/tests/constraint_emit_tests.rs b/prover/src/tests/constraint_emit_tests.rs index 0cfa8ac84..5439d4635 100644 --- a/prover/src/tests/constraint_emit_tests.rs +++ b/prover/src/tests/constraint_emit_tests.rs @@ -193,14 +193,14 @@ fn emit_add_pair_linear_unconditional() { // exercises const_signed on both signs. fn rhs() -> AddOperand { AddOperand::linear( - vec![ + &[ AddLinearTerm::Column { coefficient: -2, column: 2, }, AddLinearTerm::Constant(4), ], - vec![], + &[], ) } emit_body!(Body, 2, |b| { diff --git a/prover/src/tests/constraints_tests.rs b/prover/src/tests/constraints_tests.rs index 51f41c314..0caf71264 100644 --- a/prover/src/tests/constraints_tests.rs +++ b/prover/src/tests/constraints_tests.rs @@ -307,14 +307,14 @@ fn test_add_operand_linear_with_negative_coefficient() { // Test linear operand with negative coefficient: 4 - 2*c // This represents expressions like `4 - 2 * c_type_instruction` let op = AddOperand::linear( - vec![ + &[ AddLinearTerm::Constant(4), AddLinearTerm::Column { coefficient: -2, column: 0, }, ], - vec![], // hi = 0 + &[], // hi = 0 ); match op { AddOperand::Linear { lo, hi } => { @@ -344,7 +344,7 @@ fn test_add_operand_linear_with_negative_coefficient() { fn test_add_operand_linear_with_nonzero_hi() { // Test linear operand with non-trivial hi terms (virtual column case) let op = AddOperand::linear( - vec![ + &[ AddLinearTerm::Column { coefficient: 1 << 16, column: 0, @@ -358,7 +358,7 @@ fn test_add_operand_linear_with_nonzero_hi() { column: 2, }, ], - vec![ + &[ AddLinearTerm::Column { coefficient: 1 << 16, column: 3, From af8a23a537f1cdbb3ede405d0a74d365052298bd Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 15:14:56 -0300 Subject: [PATCH 40/52] math: #[inline(always)] the base-x-ext IsSubFieldOf ops The concrete IsSubFieldOf mul/add/sub/embed impls are non-generic, so under the default no-LTO release profile downstream crates call them instead of inlining -- and they sit in the constraint evaluation hot loop (the evaluator's eval*beta fold and every LogUp fingerprint term). The IsField ops in this file already carry the attribute; these were the gap. --- crypto/math/src/field/extensions_goldilocks.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crypto/math/src/field/extensions_goldilocks.rs b/crypto/math/src/field/extensions_goldilocks.rs index 45fd7274b..d6bac98df 100644 --- a/crypto/math/src/field/extensions_goldilocks.rs +++ b/crypto/math/src/field/extensions_goldilocks.rs @@ -199,6 +199,11 @@ impl IsField for Degree2GoldilocksExtensionField { } impl IsSubFieldOf for GoldilocksField { + // The base×ext ops run in the constraint-eval hot loop from downstream + // crates; these impls are concrete (non-generic), so without the + // attribute they compile as cross-crate calls under the default + // no-LTO release profile — unlike the #[inline(always)] IsField ops. + #[inline(always)] fn mul( a: &Self::BaseType, b: &::BaseType, @@ -208,6 +213,7 @@ impl IsSubFieldOf for GoldilocksField { [c0, c1] } + #[inline(always)] fn add( a: &Self::BaseType, b: &::BaseType, @@ -224,6 +230,7 @@ impl IsSubFieldOf for GoldilocksField { Ok(>::mul(a, &b_inv)) } + #[inline(always)] fn sub( a: &Self::BaseType, b: &::BaseType, @@ -233,6 +240,7 @@ impl IsSubFieldOf for GoldilocksField { [c0, c1] } + #[inline(always)] fn embed(a: Self::BaseType) -> ::BaseType { [FpE::from_raw(a), FpE::zero()] } @@ -410,6 +418,12 @@ impl IsField for Degree3GoldilocksExtensionField { } impl IsSubFieldOf for GoldilocksField { + // The base×ext ops run in the constraint-eval hot loop from downstream + // crates (the evaluator's eval·β fold and every LogUp fingerprint term); + // these impls are concrete (non-generic), so without the attribute they + // compile as cross-crate calls under the default no-LTO release profile — + // unlike the #[inline(always)] IsField ops. + #[inline(always)] fn mul( a: &Self::BaseType, b: &::BaseType, @@ -420,6 +434,7 @@ impl IsSubFieldOf for GoldilocksField { [c0, c1, c2] } + #[inline(always)] fn add( a: &Self::BaseType, b: &::BaseType, @@ -436,6 +451,7 @@ impl IsSubFieldOf for GoldilocksField { Ok(>::mul(a, &b_inv)) } + #[inline(always)] fn sub( a: &Self::BaseType, b: &::BaseType, @@ -446,6 +462,7 @@ impl IsSubFieldOf for GoldilocksField { [c0, c1, c2] } + #[inline(always)] fn embed(a: Self::BaseType) -> ::BaseType { [FpE::from_raw(a), FpE::zero(), FpE::zero()] } From c8f91eb998d0e67391122f1a3a97e6982750ad49 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 15:18:49 -0300 Subject: [PATCH 41/52] =?UTF-8?q?scripts:=20perf=5Fdiff.sh=20=E2=80=94=20s?= =?UTF-8?q?ymbol-level=20profile=20diff=20for=20ABBA-confirmed=20regressio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to bench_abba.sh: builds both refs release+debug-symbols (identical flags to the bench, debug=1 does not change optimization), records B A B A interleaved with perf, and prints two perf-diff tables (a symbol's delta is real only if it repeats in both) plus per-side self-time reports. --- scripts/perf_diff.sh | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100755 scripts/perf_diff.sh diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh new file mode 100755 index 000000000..ddadf53fd --- /dev/null +++ b/scripts/perf_diff.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# +# perf_diff.sh — symbol-level profile diff of two prover builds on the ethrex +# fixture. Companion to bench_abba.sh: once ABBA says a regression is REAL, +# this localizes it — `perf diff` reports per-symbol self-time deltas between +# the two binaries, which is the ground truth the source-level audits can't +# see (inlining, register pressure, allocator time). +# +# Builds mirror bench_abba.sh exactly (release, jemalloc-stats) plus debug +# symbols (CARGO_PROFILE_RELEASE_DEBUG=1 — see the note in the workspace +# Cargo.toml); debug=1 does not change optimization, so the profiled binary +# is the benched binary. +# +# USAGE (on the bench server): +# scripts/perf_diff.sh REF_A [REF_B=origin/main] +# Produces: +# - two perf-diff tables (recorded twice per side, interleaved B A B A — +# symbols whose delta repeats across both tables are real, one-off +# deltas are sampling noise) +# - top self-time report per side +# Requires: perf. If kernel.perf_event_paranoid > 2, run: +# sudo sysctl kernel.perf_event_paranoid=1 + +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "usage: perf_diff.sh REF_A [REF_B=origin/main]" >&2 + exit 2 +fi +REF_A="$1" +REF_B="${2:-origin/main}" + +ELF_REL="executor/program_artifacts/rust/ethrex.elf" +INPUT_REL="executor/tests/ethrex_bench_20.bin" +WORK="/tmp/perf_diff" +WT="/tmp/perf_diff_wt" +PROOF="/tmp/perf_diff_proof.bin" + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +command -v perf >/dev/null 2>&1 || { echo "ERROR: perf not installed (linux-tools)." >&2; exit 1; } +[ -f "$ELF_REL" ] || { echo "ERROR: missing $ELF_REL — run bench_abba.sh once (it builds the guest)." >&2; exit 1; } +[ -f "$INPUT_REL" ] || { echo "ERROR: missing $INPUT_REL — run bench_abba.sh once (it builds the fixture)." >&2; exit 1; } + +echo "==> Refs" +git fetch origin --quiet || echo "WARNING: 'git fetch origin' failed -- resolving against possibly-stale local refs." >&2 +SHA_A="$(git rev-parse "$REF_A")" +SHA_B="$(git rev-parse "$REF_B")" +echo " A (PR) $REF_A -> ${SHA_A:0:10}" +echo " B (baseline) $REF_B -> ${SHA_B:0:10}" + +mkdir -p "$WORK" + +# --- Build both binaries with debug symbols (cached per SHA) --- +need_build=0 +if [ ! -x "$WORK/cli_A" ] || [ ! -x "$WORK/cli_B" ]; then + need_build=1 +elif [ "$(cat "$WORK/cli_A.sha" 2>/dev/null)" != "$SHA_A" ] || [ "$(cat "$WORK/cli_B.sha" 2>/dev/null)" != "$SHA_B" ]; then + need_build=1 +fi +if [ "$need_build" = "1" ]; then + cleanup() { git worktree remove --force "$WT" 2>/dev/null || true; } + trap cleanup EXIT + git worktree remove --force "$WT" 2>/dev/null || true + echo "==> Building both binaries (release + debug symbols) in $WT" + git worktree add --detach "$WT" "$SHA_B" >/dev/null + build_cli() { # $1=sha $2=out + echo "==> Building cli @ ${1:0:10} -> $2" + git -C "$WT" checkout --quiet "$1" + if ! ( cd "$WT" && CARGO_PROFILE_RELEASE_DEBUG=1 cargo build --release -p cli --features jemalloc-stats >"$WORK/build_$2.log" 2>&1 ); then + echo "ERROR: build failed for $2. Tail of $WORK/build_$2.log:" >&2 + tail -40 "$WORK/build_$2.log" >&2 + exit 1 + fi + cp "$WT/target/release/cli" "$WORK/$2" + echo "$1" > "$WORK/$2.sha" + } + build_cli "$SHA_B" cli_B + build_cli "$SHA_A" cli_A + cleanup + trap - EXIT +else + echo "==> Reusing cached binaries (cli_A=${SHA_A:0:10} cli_B=${SHA_B:0:10})" +fi + +# --- Record: warmup, then B A B A (interleaved so drift hits both sides) --- +record() { # $1=binary $2=out.data + perf record -F 599 -o "$WORK/$2" -- \ + "$WORK/$1" prove "$ELF_REL" --private-input "$INPUT_REL" -o "$PROOF" --time \ + >"$WORK/$2.log" 2>&1 + rm -f "$PROOF" + grep -o 'Proving time: [0-9.]*' "$WORK/$2.log" || true +} +echo "==> Warmup (B, not recorded)" +"$WORK/cli_B" prove "$ELF_REL" --private-input "$INPUT_REL" -o "$PROOF" --time >/dev/null 2>&1 +rm -f "$PROOF" +echo "==> Recording B (main), run 1"; record cli_B B1.data +echo "==> Recording A (PR), run 1"; record cli_A A1.data +echo "==> Recording B (main), run 2"; record cli_B B2.data +echo "==> Recording A (PR), run 2"; record cli_A A2.data + +# --- Reports --- +echo +echo "=== perf diff, run 1 (Delta column: + = PR spends MORE self-time there) ===" +perf diff "$WORK/B1.data" "$WORK/A1.data" 2>/dev/null | head -60 +echo +echo "=== perf diff, run 2 (a symbol is REAL only if it repeats here) ===" +perf diff "$WORK/B2.data" "$WORK/A2.data" 2>/dev/null | head -60 +echo +echo "=== top self-time, B (main) run 1 ===" +perf report -i "$WORK/B1.data" --stdio --no-children --percent-limit 0.5 2>/dev/null | head -45 +echo +echo "=== top self-time, A (PR) run 1 ===" +perf report -i "$WORK/A1.data" --stdio --no-children --percent-limit 0.5 2>/dev/null | head -45 +echo +echo "Raw data in $WORK (perf report -i $WORK/A1.data for interactive drill-down)." From 73d33d4ea5d77209d10d902c33847cf0aa1ca457 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 16:19:55 -0300 Subject: [PATCH 42/52] stark+prover: micro-op bundle for the constraint hot path All value-identical; each item is independently revertable if the bench says it isn't worth its cleverness: - IsSubFieldOf gains sub_from (ext - base) with a neg(sub) default; Goldilocks ext2/ext3 specialize it to touch only component 0. sub_subfield routes through it (was 1 sub + 5 negs, now 1 sub). - ConstraintBuilder gains ext_sub_base with the -(v - e) default (capture IR unchanged); the eval folders override via sub_subfield. Used for the fingerprint seed z - bus_id and the 1-absorbed accumulated root. - LogUp sender/receiver signs resolve as sub-vs-add instead of negating the receiver term (x - (-t) = x + t): no ext negation per term. - Evaluator uniform fold seeds with constraint 0's promoted evaluation (transition_coefficients[0] is beta^0 = 1 by construction) - skips a multiply-by-one per row without branching on the value. - Shared-subexpression hoists: LOAD sign-extension product (was built 7x per row), MUL sign-fills (4x), LT carry_0/carry_1 (3x/2x), BRANCH next-pc repack + per-path carry_0 (4x/2x). --- crypto/math/src/field/element.rs | 4 +- .../math/src/field/extensions_goldilocks.rs | 22 +++++++ crypto/math/src/field/traits.rs | 7 +++ crypto/stark/src/constraints/builder.rs | 18 ++++++ crypto/stark/src/constraints/evaluator.rs | 25 +++++--- crypto/stark/src/lookup.rs | 61 +++++++++++-------- prover/src/tables/branch.rs | 34 ++++++----- prover/src/tables/load.rs | 16 +++-- prover/src/tables/lt.rs | 17 ++++-- prover/src/tables/mul.rs | 25 +++++--- 10 files changed, 160 insertions(+), 69 deletions(-) diff --git a/crypto/math/src/field/element.rs b/crypto/math/src/field/element.rs index 0eb0aef96..38225acd0 100644 --- a/crypto/math/src/field/element.rs +++ b/crypto/math/src/field/element.rs @@ -578,10 +578,8 @@ where /// explicitly converting rhs to the extension field. #[inline(always)] pub fn sub_subfield>(&self, rhs: &FieldElement) -> Self { - // embed(rhs) - self gives the negation of what we want, in F. - // Then negate to get self - embed(rhs). Self { - value: F::neg(&>::sub(&rhs.value, &self.value)), + value: >::sub_from(&rhs.value, &self.value), } } diff --git a/crypto/math/src/field/extensions_goldilocks.rs b/crypto/math/src/field/extensions_goldilocks.rs index d6bac98df..05e7c2f68 100644 --- a/crypto/math/src/field/extensions_goldilocks.rs +++ b/crypto/math/src/field/extensions_goldilocks.rs @@ -240,6 +240,17 @@ impl IsSubFieldOf for GoldilocksField { [c0, c1] } + // b − a touches only component 0 (the default neg(sub) shape negates + // every component twice). + #[inline(always)] + fn sub_from( + a: &Self::BaseType, + b: &::BaseType, + ) -> ::BaseType { + let c0 = FpE::from_raw(::sub(b[0].value(), a)); + [c0, b[1]] + } + #[inline(always)] fn embed(a: Self::BaseType) -> ::BaseType { [FpE::from_raw(a), FpE::zero()] @@ -462,6 +473,17 @@ impl IsSubFieldOf for GoldilocksField { [c0, c1, c2] } + // b − a touches only component 0 (the default neg(sub) shape negates + // every component twice). + #[inline(always)] + fn sub_from( + a: &Self::BaseType, + b: &::BaseType, + ) -> ::BaseType { + let c0 = FpE::from_raw(::sub(b[0].value(), a)); + [c0, b[1], b[2]] + } + #[inline(always)] fn embed(a: Self::BaseType) -> ::BaseType { [FpE::from_raw(a), FpE::zero(), FpE::zero()] diff --git a/crypto/math/src/field/traits.rs b/crypto/math/src/field/traits.rs index 04dcc410d..bd0f2829f 100644 --- a/crypto/math/src/field/traits.rs +++ b/crypto/math/src/field/traits.rs @@ -19,6 +19,13 @@ pub trait IsSubFieldOf: IsField { fn add(a: &Self::BaseType, b: &F::BaseType) -> F::BaseType; fn div(a: &Self::BaseType, b: &F::BaseType) -> Result; fn sub(a: &Self::BaseType, b: &F::BaseType) -> F::BaseType; + /// `b - a` (extension minus subfield). The default routes through + /// `neg(sub(a, b))`; extension impls with a subfield-aligned basis + /// override it to subtract on component 0 only. + #[inline(always)] + fn sub_from(a: &Self::BaseType, b: &F::BaseType) -> F::BaseType { + F::neg(&>::sub(a, b)) + } fn embed(a: Self::BaseType) -> F::BaseType; #[cfg(feature = "alloc")] fn to_subfield_vec(b: F::BaseType) -> alloc::vec::Vec; diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index b772353ad..8437ef907 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -144,6 +144,15 @@ pub trait ConstraintBuilder { ) -> Self::ExprE { fp - v * self.alpha_pow(alpha_idx) } + + /// `e − v` (extension minus base). The expression ops only implement + /// base ∘ ext (base operand LEFT), so the generic route — and the shape + /// this default keeps in the captured IR — is `−(v − e)`: one mixed sub + /// plus a full extension negation. The eval folders override it with + /// `FieldElement::sub_subfield`, which touches only component 0. + fn ext_sub_base(&self, e: Self::ExprE, v: Self::Expr) -> Self::ExprE { + -(v - e) + } } // ============================================================================= @@ -487,6 +496,10 @@ where fp - v * &self.alphas[alpha_idx] } } + + fn ext_sub_base(&self, e: FieldElement, v: FieldElement) -> FieldElement { + e.sub_subfield(&v) + } } // ============================================================================= @@ -602,6 +615,11 @@ where self.tracker.mark(constraint_idx); self.ext_out[constraint_idx] = e; } + + fn ext_sub_base(&self, e: FieldElement, v: FieldElement) -> FieldElement { + // Expr = ExprE at the OOD point: a plain extension subtraction. + e - v + } } // ============================================================================= diff --git a/crypto/stark/src/constraints/evaluator.rs b/crypto/stark/src/constraints/evaluator.rs index 17b42b058..9f77abcb1 100644 --- a/crypto/stark/src/constraints/evaluator.rs +++ b/crypto/stark/src/constraints/evaluator.rs @@ -94,16 +94,25 @@ where let acc_transition = if is_uniform { // All constraints share one zerofier: factor it out of the sum. let z = zerofier_data.get_uniform(i); - // F×E inner product for base constraints (3 muls per term) - let mut sum = base_buf + // transition_coefficients are the powers β⁰, β¹, … (round 2), + // so constraint 0's coefficient is one: seed the sum with its + // promoted evaluation and fold the rest — skips the + // multiply-by-one without branching on the value. + let mut base_iter = base_buf.iter().zip(&transition_coefficients[..num_base]); + let mut ext_iter = transition_buf[num_base..] .iter() - .zip(&transition_coefficients[..num_base]) - .fold(FieldElement::zero(), |acc, (eval, beta)| acc + eval * beta); + .zip(&transition_coefficients[num_base..]); + let seed = match base_iter.next() { + Some((eval, _beta0)) => eval.clone().to_extension(), + None => match ext_iter.next() { + Some((eval, _beta0)) => eval.clone(), + None => FieldElement::zero(), + }, + }; + // F×E inner product for base constraints (3 muls per term) + let sum = base_iter.fold(seed, |acc, (eval, beta)| acc + eval * beta); // E×E for extension constraints (9 muls per term) - sum = transition_buf[num_base..] - .iter() - .zip(&transition_coefficients[num_base..]) - .fold(sum, |acc, (eval, beta)| acc + eval * beta); + let sum = ext_iter.fold(sum, |acc, (eval, beta)| acc + eval * beta); z * &sum } else { let mut sum = base_buf diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 6499ad1e2..0ed4b56ad 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -1991,9 +1991,7 @@ where { let z = b.challenge(0); let bus = b.const_base(interaction.bus_id); - // `bus` is base and `z` ext; the tower only implements base − ext (base - // operand LEFT), so z − bus is written −(bus − z). - let mut fp = -(bus - z); + let mut fp = b.ext_sub_base(z, bus); let mut alpha_idx = 1; for bv in &interaction.values { let (next, consumed) = emit_busvalue_fingerprint::(b, bv, offset, alpha_idx, fp); @@ -2021,25 +2019,25 @@ where let fp_a = emit_fingerprint::(b, interaction_a, 0); let fp_b = emit_fingerprint::(b, interaction_b, 0); - // is_sender is a compile-time bool, resolved as add vs neg instead of an - // ext×ext sign multiply (same optimization as the runtime body). m·fp is - // base×ext = ext (base operand LEFT). + // is_sender is a compile-time bool, resolved as sub vs add — subtracting + // a receiver's negated term is adding it (x − (−t) = x + t), which skips + // the ext negation entirely. m·fp is base×ext = ext (base operand LEFT). let term_a = m_a * fp_b.clone(); - let term_a = if interaction_a.is_sender { - term_a - } else { - -term_a - }; let term_b = m_b * fp_a.clone(); - let term_b = if interaction_b.is_sender { - term_b - } else { - -term_b - }; // c · fp_a · fp_b: c is aux (ext), so this is ext throughout. let main = c * fp_a * fp_b; - b.emit_ext(idx, main - term_a - term_b); + let acc = if interaction_a.is_sender { + main - term_a + } else { + main + term_a + }; + let acc = if interaction_b.is_sender { + acc - term_b + } else { + acc + term_b + }; + b.emit_ext(idx, acc); } /// Emit the accumulated constraint (with 1–2 absorbed interactions). @@ -2071,23 +2069,36 @@ where // delta · f − sign · m let m = emit_multiplicity::(b, &absorbed[0].multiplicity, 1); let f = emit_fingerprint::(b, &absorbed[0], 1); - let mt = if absorbed[0].is_sender { m } else { -m }; - // delta · f is ext; `mt` is base. The tower only implements base − - // ext (base operand LEFT), so write `delta·f − mt` as `−(mt − delta·f)`. - -(mt - delta * f) + let df = delta * f; + // sign resolved as ext−base vs base+ext: for a sender the root is + // df − m; for a receiver, df − (−m) = m + df (base operand LEFT). + if absorbed[0].is_sender { + b.ext_sub_base(df, m) + } else { + m + df + } } 2 => { - // delta · f1 · f2 − sign1·m1·f2 − sign2·m2·f1 + // delta · f1 · f2 − sign1·m1·f2 − sign2·m2·f1, with the signs + // resolved as sub vs add (x − (−t) = x + t) — no ext negation. let m1 = emit_multiplicity::(b, &absorbed[0].multiplicity, 1); let m2 = emit_multiplicity::(b, &absorbed[1].multiplicity, 1); let f1 = emit_fingerprint::(b, &absorbed[0], 1); let f2 = emit_fingerprint::(b, &absorbed[1], 1); let term1 = m1 * f2.clone(); - let term1 = if absorbed[0].is_sender { term1 } else { -term1 }; let term2 = m2 * f1.clone(); - let term2 = if absorbed[1].is_sender { term2 } else { -term2 }; - delta * f1 * f2 - term1 - term2 + let acc = delta * f1 * f2; + let acc = if absorbed[0].is_sender { + acc - term1 + } else { + acc + term1 + }; + if absorbed[1].is_sender { + acc - term2 + } else { + acc + term2 + } } _ => unreachable!("absorbed must contain 1 or 2 interactions"), }; diff --git a/prover/src/tables/branch.rs b/prover/src/tables/branch.rs index b4a4fcb8d..7398c78e3 100644 --- a/prover/src/tables/branch.rs +++ b/prover/src/tables/branch.rs @@ -377,26 +377,28 @@ fn next_pc_unmasked_expr>( b: &B, base_col_0: usize, + unmasked_0: B::Expr, ) -> B::Expr { let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); - let (unmasked_0, _) = next_pc_unmasked_expr(b); (b.main(0, base_col_0) + b.main(0, cols::OFFSET_0) - unmasked_0) * inv_2_32 } /// `carry_1 = (base_1 + offset_1 + carry_0 − unmasked_1)·2⁻³²` (twin of -/// [`BranchConstraint::compute_carry_1_for`]). +/// [`BranchConstraint::compute_carry_1_for`]). Takes the path's `carry_0` +/// and the shared `unmasked_1` so neither is recomputed. fn carry_1_expr>( b: &B, - base_col_0: usize, base_col_1: usize, + carry_0: B::Expr, + unmasked_1: B::Expr, ) -> B::Expr { let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); - let carry_0 = carry_0_expr(b, base_col_0); - let (_, unmasked_1) = next_pc_unmasked_expr(b); (b.main(0, base_col_1) + b.main(0, cols::OFFSET_1) + carry_0 - unmasked_1) * inv_2_32 } @@ -421,31 +423,35 @@ impl ConstraintSet for BranchConstraints { } fn eval>(&self, b: &mut B) { + // The unmasked next-pc repack and each path's carry_0 are shared + // across the four carry constraints — computed once per row. + let (unmasked_0, unmasked_1) = next_pc_unmasked_expr(b); + let pc_c0 = carry_0_expr(b, cols::PC_0, unmasked_0.clone()); + let pc_c1 = carry_1_expr(b, cols::PC_1, pc_c0.clone(), unmasked_1.clone()); + let reg_c0 = carry_0_expr(b, cols::REGISTER_0, unmasked_0); + let reg_c1 = carry_1_expr(b, cols::REGISTER_1, reg_c0.clone(), unmasked_1); + // idx 0: (1 - JALR) * carry_0(pc) * (1 - carry_0) let one = b.one(); let cond = one - b.main(0, cols::JALR); - let c = carry_0_expr(b, cols::PC_0); let one = b.one(); - b.emit_base(0, cond * c.clone() * (one - c)); + b.emit_base(0, cond * pc_c0.clone() * (one - pc_c0)); // idx 1: (1 - JALR) * carry_1(pc) * (1 - carry_1) let one = b.one(); let cond = one - b.main(0, cols::JALR); - let c = carry_1_expr(b, cols::PC_0, cols::PC_1); let one = b.one(); - b.emit_base(1, cond * c.clone() * (one - c)); + b.emit_base(1, cond * pc_c1.clone() * (one - pc_c1)); // idx 2: JALR * carry_0(register) * (1 - carry_0) let cond = b.main(0, cols::JALR); - let c = carry_0_expr(b, cols::REGISTER_0); let one = b.one(); - b.emit_base(2, cond * c.clone() * (one - c)); + b.emit_base(2, cond * reg_c0.clone() * (one - reg_c0)); // idx 3: JALR * carry_1(register) * (1 - carry_1) let cond = b.main(0, cols::JALR); - let c = carry_1_expr(b, cols::REGISTER_0, cols::REGISTER_1); let one = b.one(); - b.emit_base(3, cond * c.clone() * (one - c)); + b.emit_base(3, cond * reg_c1.clone() * (one - reg_c1)); // idx 4: JALR * (1 - JALR) let one = b.one(); diff --git a/prover/src/tables/load.rs b/prover/src/tables/load.rs index b72b8bfdf..7c8a9db89 100644 --- a/prover/src/tables/load.rs +++ b/prover/src/tables/load.rs @@ -556,14 +556,17 @@ impl ConstraintSet for LoadConstraints { let one = b.one(); b.emit_base(5, read_sum * (one - mu)); + // The sign-extension value is shared by all seven extension + // constraints below — compute its two multiplies once per row. + let extended = Self::extended(b); + // idx 6..10: ExtensionHigh(i) for i in 4..8: // (1 - read8) * (res[i] - signed*sign_bit*255) for (offset, i) in (4..8).enumerate() { let read8 = b.main(0, cols::READ8); let res_i = b.main(0, cols::RES[i]); - let expected = Self::extended(b); let one = b.one(); - b.emit_base(6 + offset, (one - read8) * (res_i - expected)); + b.emit_base(6 + offset, (one - read8) * (res_i - extended.clone())); } // idx 10,11: ExtensionMid(i) for i in 2..4: @@ -572,9 +575,11 @@ impl ConstraintSet for LoadConstraints { let read4 = b.main(0, cols::READ4); let read8 = b.main(0, cols::READ8); let res_i = b.main(0, cols::RES[i]); - let expected = Self::extended(b); let one = b.one(); - b.emit_base(10 + offset, (one - read4 - read8) * (res_i - expected)); + b.emit_base( + 10 + offset, + (one - read4 - read8) * (res_i - extended.clone()), + ); } // idx 12: ExtensionLow: @@ -583,8 +588,7 @@ impl ConstraintSet for LoadConstraints { let read4 = b.main(0, cols::READ4); let read8 = b.main(0, cols::READ8); let res_1 = b.main(0, cols::RES[1]); - let expected = Self::extended(b); let one = b.one(); - b.emit_base(12, (one - read2 - read4 - read8) * (res_1 - expected)); + b.emit_base(12, (one - read2 - read4 - read8) * (res_1 - extended)); } } diff --git a/prover/src/tables/lt.rs b/prover/src/tables/lt.rs index 9302e2224..c752e6a35 100644 --- a/prover/src/tables/lt.rs +++ b/prover/src/tables/lt.rs @@ -372,8 +372,13 @@ impl LtConstraints { (rhs_0 + sub_lo - lhs_0) * inv_2_32 } - /// carry[1] = (rhs_hi + sub_hi + carry_0 - lhs_hi) / 2^32. - fn carry_1>(b: &B) -> B::Expr { + /// carry[1] = (rhs_hi + sub_hi + carry_0 - lhs_hi) / 2^32. Takes the + /// [`Self::carry_0`] value so the body computes it once per row (it is + /// also needed by the carry-0 IS_BIT constraint). + fn carry_1>( + b: &B, + carry_0: B::Expr, + ) -> B::Expr { let lhs_1 = b.main(0, cols::LHS_1); let lhs_2 = b.main(0, cols::LHS_2); let rhs_1 = b.main(0, cols::RHS_1); @@ -387,7 +392,6 @@ impl LtConstraints { let rhs_hi = rhs_1 + rhs_2 * shift_16.clone(); // cast(lhs_sub_rhs, DWordWL)[1] = sub_2 + 2^16 * sub_3 let sub_hi = sub_2 + sub_3 * shift_16; - let carry_0 = Self::carry_0(b); let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); (rhs_hi + sub_hi + carry_0 - lhs_hi) * inv_2_32 } @@ -407,14 +411,15 @@ impl ConstraintSet for LtConstraints { fn eval>(&self, b: &mut B) { // idx 0: IS_BIT: carry[0] * (1 - carry[0]) + // carry_0 and carry_1 are shared by idx 0/1/2 — one computation each. let c0 = Self::carry_0(b); + let c1 = Self::carry_1(b, c0.clone()); let one = b.one(); b.emit_base(0, c0.clone() * (one - c0)); // idx 1: IS_BIT: carry[1] * (1 - carry[1]) - let c1 = Self::carry_1(b); let one = b.one(); - b.emit_base(1, c1.clone() * (one - c1)); + b.emit_base(1, c1.clone() * (one - c1.clone())); // idx 2: LT formula: lt - (signed*signed_lt + (1-signed)*unsigned_lt) // signed_lt = A*(1-B) + A*C + (1-B)*C; unsigned_lt = C = carry[1] @@ -422,7 +427,7 @@ impl ConstraintSet for LtConstraints { let signed = b.main(0, cols::SIGNED); let a = b.main(0, cols::LHS_MSB); let bb = b.main(0, cols::RHS_MSB); - let c = Self::carry_1(b); + let c = c1; let unsigned_lt = c.clone(); let one = b.one(); let one_minus_b = one - bb; diff --git a/prover/src/tables/mul.rs b/prover/src/tables/mul.rs index 70adbedd6..9fcfd7efe 100644 --- a/prover/src/tables/mul.rs +++ b/prover/src/tables/mul.rs @@ -709,11 +709,25 @@ impl MulConstraints { x.clone() * (one - x) } + /// `sign_fill · is_neg` for both operands — shared by all four + /// [`Self::raw_product`] constraints, so computed once per row. + fn sign_fills>( + b: &B, + ) -> (B::Expr, B::Expr) { + let lhs_is_neg = b.main(0, cols::LHS_IS_NEGATIVE); + let rhs_is_neg = b.main(0, cols::RHS_IS_NEGATIVE); + let sign_fill = b.const_base(SIGN_FILL); + (sign_fill.clone() * lhs_is_neg, sign_fill * rhs_is_neg) + } + /// `raw_product[i] − Σ_k 2^(16k)·Σ_j lhs_ext[j]·rhs_ext[idx−j]` (idx = 2i+k). /// Mirrors [`MulConstraint::compute_raw_product_constraint`] exactly. + /// `lhs_hi`/`rhs_hi` are the [`Self::sign_fills`] products. fn raw_product>( b: &B, i: usize, + lhs_hi: B::Expr, + rhs_hi: B::Expr, ) -> B::Expr { let lhs = [ b.main(0, cols::LHS_0), @@ -727,13 +741,8 @@ impl MulConstraints { b.main(0, cols::RHS_2), b.main(0, cols::RHS_3), ]; - let lhs_is_neg = b.main(0, cols::LHS_IS_NEGATIVE); - let rhs_is_neg = b.main(0, cols::RHS_IS_NEGATIVE); // Sign-extended values: [0..4] = halfwords, [4..8] = sign_fill * is_neg. - let sign_fill = b.const_base(SIGN_FILL); - let lhs_hi = sign_fill.clone() * lhs_is_neg; - let rhs_hi = sign_fill * rhs_is_neg; let lhs_ext: [B::Expr; 8] = [ lhs[0].clone(), lhs[1].clone(), @@ -820,9 +829,11 @@ impl ConstraintSet for MulConstraints { let one = b.one(); b.emit_base(3, (one - rhs_signed) * rhs_is_neg); - // idx 4..8: raw_product convolution for i = 0..4. + // idx 4..8: raw_product convolution for i = 0..4. The sign-fill + // products are shared across all four constraints. + let (lhs_hi, rhs_hi) = Self::sign_fills(b); for i in 0..4 { - let root = Self::raw_product(b, i); + let root = Self::raw_product(b, i, lhs_hi.clone(), rhs_hi.clone()); b.emit_base(4 + i, root); } } From 41e2d35ea5c08711c8ad1fea21bfd3f916562b45 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 16:54:32 -0300 Subject: [PATCH 43/52] prover: revert the table-body subexpression hoists; keep the note The LOAD/MUL/LT/BRANCH hoists from the micro-op bundle measured flat on ABBA (-1.33% vs the pre-bundle -1.5%, within noise), so the bodies go back to their declarative per-emit form -- the constraint bodies double as the spec, and cleverness there has to earn its keep. Each site keeps a comment recording that the redundancy is known and was measured to not matter, so it doesn't get re-optimized. The engine-side pieces of the bundle (ext_sub_base, sender-sign sub-vs-add, beta^0 seed) are body-invisible and stay. --- prover/src/tables/branch.rs | 39 ++++++++++++++++++------------------- prover/src/tables/load.rs | 21 ++++++++++---------- prover/src/tables/lt.rs | 21 ++++++++++---------- prover/src/tables/mul.rs | 28 ++++++++++---------------- 4 files changed, 50 insertions(+), 59 deletions(-) diff --git a/prover/src/tables/branch.rs b/prover/src/tables/branch.rs index 7398c78e3..fc55149ef 100644 --- a/prover/src/tables/branch.rs +++ b/prover/src/tables/branch.rs @@ -377,28 +377,31 @@ fn next_pc_unmasked_expr>( b: &B, base_col_0: usize, - unmasked_0: B::Expr, ) -> B::Expr { let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); + let (unmasked_0, _) = next_pc_unmasked_expr(b); (b.main(0, base_col_0) + b.main(0, cols::OFFSET_0) - unmasked_0) * inv_2_32 } /// `carry_1 = (base_1 + offset_1 + carry_0 − unmasked_1)·2⁻³²` (twin of -/// [`BranchConstraint::compute_carry_1_for`]). Takes the path's `carry_0` -/// and the shared `unmasked_1` so neither is recomputed. +/// [`BranchConstraint::compute_carry_1_for`]). +/// +/// Known redundancy: this rebuilds the carry_0 expression (and the unmasked +/// next-pc repack) that the sibling constraints also compute. Sharing them +/// across the four carry constraints was tried and showed no measurable +/// speedup (ABBA), so the helpers stay self-contained. fn carry_1_expr>( b: &B, + base_col_0: usize, base_col_1: usize, - carry_0: B::Expr, - unmasked_1: B::Expr, ) -> B::Expr { let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); + let carry_0 = carry_0_expr(b, base_col_0); + let (_, unmasked_1) = next_pc_unmasked_expr(b); (b.main(0, base_col_1) + b.main(0, cols::OFFSET_1) + carry_0 - unmasked_1) * inv_2_32 } @@ -423,35 +426,31 @@ impl ConstraintSet for BranchConstraints { } fn eval>(&self, b: &mut B) { - // The unmasked next-pc repack and each path's carry_0 are shared - // across the four carry constraints — computed once per row. - let (unmasked_0, unmasked_1) = next_pc_unmasked_expr(b); - let pc_c0 = carry_0_expr(b, cols::PC_0, unmasked_0.clone()); - let pc_c1 = carry_1_expr(b, cols::PC_1, pc_c0.clone(), unmasked_1.clone()); - let reg_c0 = carry_0_expr(b, cols::REGISTER_0, unmasked_0); - let reg_c1 = carry_1_expr(b, cols::REGISTER_1, reg_c0.clone(), unmasked_1); - // idx 0: (1 - JALR) * carry_0(pc) * (1 - carry_0) let one = b.one(); let cond = one - b.main(0, cols::JALR); + let c = carry_0_expr(b, cols::PC_0); let one = b.one(); - b.emit_base(0, cond * pc_c0.clone() * (one - pc_c0)); + b.emit_base(0, cond * c.clone() * (one - c)); // idx 1: (1 - JALR) * carry_1(pc) * (1 - carry_1) let one = b.one(); let cond = one - b.main(0, cols::JALR); + let c = carry_1_expr(b, cols::PC_0, cols::PC_1); let one = b.one(); - b.emit_base(1, cond * pc_c1.clone() * (one - pc_c1)); + b.emit_base(1, cond * c.clone() * (one - c)); // idx 2: JALR * carry_0(register) * (1 - carry_0) let cond = b.main(0, cols::JALR); + let c = carry_0_expr(b, cols::REGISTER_0); let one = b.one(); - b.emit_base(2, cond * reg_c0.clone() * (one - reg_c0)); + b.emit_base(2, cond * c.clone() * (one - c)); // idx 3: JALR * carry_1(register) * (1 - carry_1) let cond = b.main(0, cols::JALR); + let c = carry_1_expr(b, cols::REGISTER_0, cols::REGISTER_1); let one = b.one(); - b.emit_base(3, cond * reg_c1.clone() * (one - reg_c1)); + b.emit_base(3, cond * c.clone() * (one - c)); // idx 4: JALR * (1 - JALR) let one = b.one(); diff --git a/prover/src/tables/load.rs b/prover/src/tables/load.rs index 7c8a9db89..c992b2050 100644 --- a/prover/src/tables/load.rs +++ b/prover/src/tables/load.rs @@ -502,6 +502,11 @@ impl LoadConstraints { } /// `signed · sign_bit · 255` — the sign-extended byte value. + /// + /// Known redundancy: each extension constraint below rebuilds this + /// product. Hoisting it to one per-row local was tried and showed no + /// measurable speedup (ABBA), so the constraints keep the declarative + /// per-emit form. fn extended>(b: &B) -> B::Expr { let signed = b.main(0, cols::SIGNED); let sign_bit = b.main(0, cols::SIGN_BIT); @@ -556,17 +561,14 @@ impl ConstraintSet for LoadConstraints { let one = b.one(); b.emit_base(5, read_sum * (one - mu)); - // The sign-extension value is shared by all seven extension - // constraints below — compute its two multiplies once per row. - let extended = Self::extended(b); - // idx 6..10: ExtensionHigh(i) for i in 4..8: // (1 - read8) * (res[i] - signed*sign_bit*255) for (offset, i) in (4..8).enumerate() { let read8 = b.main(0, cols::READ8); let res_i = b.main(0, cols::RES[i]); + let expected = Self::extended(b); let one = b.one(); - b.emit_base(6 + offset, (one - read8) * (res_i - extended.clone())); + b.emit_base(6 + offset, (one - read8) * (res_i - expected)); } // idx 10,11: ExtensionMid(i) for i in 2..4: @@ -575,11 +577,9 @@ impl ConstraintSet for LoadConstraints { let read4 = b.main(0, cols::READ4); let read8 = b.main(0, cols::READ8); let res_i = b.main(0, cols::RES[i]); + let expected = Self::extended(b); let one = b.one(); - b.emit_base( - 10 + offset, - (one - read4 - read8) * (res_i - extended.clone()), - ); + b.emit_base(10 + offset, (one - read4 - read8) * (res_i - expected)); } // idx 12: ExtensionLow: @@ -588,7 +588,8 @@ impl ConstraintSet for LoadConstraints { let read4 = b.main(0, cols::READ4); let read8 = b.main(0, cols::READ8); let res_1 = b.main(0, cols::RES[1]); + let expected = Self::extended(b); let one = b.one(); - b.emit_base(12, (one - read2 - read4 - read8) * (res_1 - extended)); + b.emit_base(12, (one - read2 - read4 - read8) * (res_1 - expected)); } } diff --git a/prover/src/tables/lt.rs b/prover/src/tables/lt.rs index c752e6a35..b4032b8e2 100644 --- a/prover/src/tables/lt.rs +++ b/prover/src/tables/lt.rs @@ -372,13 +372,12 @@ impl LtConstraints { (rhs_0 + sub_lo - lhs_0) * inv_2_32 } - /// carry[1] = (rhs_hi + sub_hi + carry_0 - lhs_hi) / 2^32. Takes the - /// [`Self::carry_0`] value so the body computes it once per row (it is - /// also needed by the carry-0 IS_BIT constraint). - fn carry_1>( - b: &B, - carry_0: B::Expr, - ) -> B::Expr { + /// carry[1] = (rhs_hi + sub_hi + carry_0 - lhs_hi) / 2^32. + /// + /// Known redundancy: this rebuilds [`Self::carry_0`], which idx 0 also + /// computes. Threading the value through was tried and showed no + /// measurable speedup (ABBA), so the helpers stay self-contained. + fn carry_1>(b: &B) -> B::Expr { let lhs_1 = b.main(0, cols::LHS_1); let lhs_2 = b.main(0, cols::LHS_2); let rhs_1 = b.main(0, cols::RHS_1); @@ -392,6 +391,7 @@ impl LtConstraints { let rhs_hi = rhs_1 + rhs_2 * shift_16.clone(); // cast(lhs_sub_rhs, DWordWL)[1] = sub_2 + 2^16 * sub_3 let sub_hi = sub_2 + sub_3 * shift_16; + let carry_0 = Self::carry_0(b); let inv_2_32 = b.const_base(crate::constraints::templates::INV_SHIFT_32); (rhs_hi + sub_hi + carry_0 - lhs_hi) * inv_2_32 } @@ -411,15 +411,14 @@ impl ConstraintSet for LtConstraints { fn eval>(&self, b: &mut B) { // idx 0: IS_BIT: carry[0] * (1 - carry[0]) - // carry_0 and carry_1 are shared by idx 0/1/2 — one computation each. let c0 = Self::carry_0(b); - let c1 = Self::carry_1(b, c0.clone()); let one = b.one(); b.emit_base(0, c0.clone() * (one - c0)); // idx 1: IS_BIT: carry[1] * (1 - carry[1]) + let c1 = Self::carry_1(b); let one = b.one(); - b.emit_base(1, c1.clone() * (one - c1.clone())); + b.emit_base(1, c1.clone() * (one - c1)); // idx 2: LT formula: lt - (signed*signed_lt + (1-signed)*unsigned_lt) // signed_lt = A*(1-B) + A*C + (1-B)*C; unsigned_lt = C = carry[1] @@ -427,7 +426,7 @@ impl ConstraintSet for LtConstraints { let signed = b.main(0, cols::SIGNED); let a = b.main(0, cols::LHS_MSB); let bb = b.main(0, cols::RHS_MSB); - let c = c1; + let c = Self::carry_1(b); let unsigned_lt = c.clone(); let one = b.one(); let one_minus_b = one - bb; diff --git a/prover/src/tables/mul.rs b/prover/src/tables/mul.rs index 9fcfd7efe..9a0b0e26d 100644 --- a/prover/src/tables/mul.rs +++ b/prover/src/tables/mul.rs @@ -709,25 +709,11 @@ impl MulConstraints { x.clone() * (one - x) } - /// `sign_fill · is_neg` for both operands — shared by all four - /// [`Self::raw_product`] constraints, so computed once per row. - fn sign_fills>( - b: &B, - ) -> (B::Expr, B::Expr) { - let lhs_is_neg = b.main(0, cols::LHS_IS_NEGATIVE); - let rhs_is_neg = b.main(0, cols::RHS_IS_NEGATIVE); - let sign_fill = b.const_base(SIGN_FILL); - (sign_fill.clone() * lhs_is_neg, sign_fill * rhs_is_neg) - } - /// `raw_product[i] − Σ_k 2^(16k)·Σ_j lhs_ext[j]·rhs_ext[idx−j]` (idx = 2i+k). /// Mirrors [`MulConstraint::compute_raw_product_constraint`] exactly. - /// `lhs_hi`/`rhs_hi` are the [`Self::sign_fills`] products. fn raw_product>( b: &B, i: usize, - lhs_hi: B::Expr, - rhs_hi: B::Expr, ) -> B::Expr { let lhs = [ b.main(0, cols::LHS_0), @@ -741,8 +727,16 @@ impl MulConstraints { b.main(0, cols::RHS_2), b.main(0, cols::RHS_3), ]; + let lhs_is_neg = b.main(0, cols::LHS_IS_NEGATIVE); + let rhs_is_neg = b.main(0, cols::RHS_IS_NEGATIVE); // Sign-extended values: [0..4] = halfwords, [4..8] = sign_fill * is_neg. + // Known redundancy: the two sign-fill products are rebuilt in each of + // the four raw_product constraints. Hoisting them was tried and showed + // no measurable speedup (ABBA), so the body keeps the declarative form. + let sign_fill = b.const_base(SIGN_FILL); + let lhs_hi = sign_fill.clone() * lhs_is_neg; + let rhs_hi = sign_fill * rhs_is_neg; let lhs_ext: [B::Expr; 8] = [ lhs[0].clone(), lhs[1].clone(), @@ -829,11 +823,9 @@ impl ConstraintSet for MulConstraints { let one = b.one(); b.emit_base(3, (one - rhs_signed) * rhs_is_neg); - // idx 4..8: raw_product convolution for i = 0..4. The sign-fill - // products are shared across all four constraints. - let (lhs_hi, rhs_hi) = Self::sign_fills(b); + // idx 4..8: raw_product convolution for i = 0..4. for i in 0..4 { - let root = Self::raw_product(b, i, lhs_hi.clone(), rhs_hi.clone()); + let root = Self::raw_product(b, i); b.emit_base(4 + i, root); } } From d67a0f65f2e6b1ca6a3663b3fe53970de88fbcf0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 16:57:54 -0300 Subject: [PATCH 44/52] stark+math: revert the engine-side micro-ops too Drops ext_sub_base (builder primitive + IsSubFieldOf::sub_from + the Goldilocks specializations), the sender-sign sub-vs-add rewrite, and the beta^0-seeded evaluator fold. All measured flat on ABBA like the body hoists, and each adds trait surface or non-local invariants the straightforward form doesn't need. The branch's constraint plumbing is back to the af8a23a5 state; the only bundle survivors are the known-redundancy comments in the table bodies. --- crypto/math/src/field/element.rs | 4 +- .../math/src/field/extensions_goldilocks.rs | 22 ------- crypto/math/src/field/traits.rs | 7 --- crypto/stark/src/constraints/builder.rs | 18 ------ crypto/stark/src/constraints/evaluator.rs | 25 +++----- crypto/stark/src/lookup.rs | 61 ++++++++----------- 6 files changed, 36 insertions(+), 101 deletions(-) diff --git a/crypto/math/src/field/element.rs b/crypto/math/src/field/element.rs index 38225acd0..0eb0aef96 100644 --- a/crypto/math/src/field/element.rs +++ b/crypto/math/src/field/element.rs @@ -578,8 +578,10 @@ where /// explicitly converting rhs to the extension field. #[inline(always)] pub fn sub_subfield>(&self, rhs: &FieldElement) -> Self { + // embed(rhs) - self gives the negation of what we want, in F. + // Then negate to get self - embed(rhs). Self { - value: >::sub_from(&rhs.value, &self.value), + value: F::neg(&>::sub(&rhs.value, &self.value)), } } diff --git a/crypto/math/src/field/extensions_goldilocks.rs b/crypto/math/src/field/extensions_goldilocks.rs index 05e7c2f68..d6bac98df 100644 --- a/crypto/math/src/field/extensions_goldilocks.rs +++ b/crypto/math/src/field/extensions_goldilocks.rs @@ -240,17 +240,6 @@ impl IsSubFieldOf for GoldilocksField { [c0, c1] } - // b − a touches only component 0 (the default neg(sub) shape negates - // every component twice). - #[inline(always)] - fn sub_from( - a: &Self::BaseType, - b: &::BaseType, - ) -> ::BaseType { - let c0 = FpE::from_raw(::sub(b[0].value(), a)); - [c0, b[1]] - } - #[inline(always)] fn embed(a: Self::BaseType) -> ::BaseType { [FpE::from_raw(a), FpE::zero()] @@ -473,17 +462,6 @@ impl IsSubFieldOf for GoldilocksField { [c0, c1, c2] } - // b − a touches only component 0 (the default neg(sub) shape negates - // every component twice). - #[inline(always)] - fn sub_from( - a: &Self::BaseType, - b: &::BaseType, - ) -> ::BaseType { - let c0 = FpE::from_raw(::sub(b[0].value(), a)); - [c0, b[1], b[2]] - } - #[inline(always)] fn embed(a: Self::BaseType) -> ::BaseType { [FpE::from_raw(a), FpE::zero(), FpE::zero()] diff --git a/crypto/math/src/field/traits.rs b/crypto/math/src/field/traits.rs index bd0f2829f..04dcc410d 100644 --- a/crypto/math/src/field/traits.rs +++ b/crypto/math/src/field/traits.rs @@ -19,13 +19,6 @@ pub trait IsSubFieldOf: IsField { fn add(a: &Self::BaseType, b: &F::BaseType) -> F::BaseType; fn div(a: &Self::BaseType, b: &F::BaseType) -> Result; fn sub(a: &Self::BaseType, b: &F::BaseType) -> F::BaseType; - /// `b - a` (extension minus subfield). The default routes through - /// `neg(sub(a, b))`; extension impls with a subfield-aligned basis - /// override it to subtract on component 0 only. - #[inline(always)] - fn sub_from(a: &Self::BaseType, b: &F::BaseType) -> F::BaseType { - F::neg(&>::sub(a, b)) - } fn embed(a: Self::BaseType) -> F::BaseType; #[cfg(feature = "alloc")] fn to_subfield_vec(b: F::BaseType) -> alloc::vec::Vec; diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index 8437ef907..b772353ad 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -144,15 +144,6 @@ pub trait ConstraintBuilder { ) -> Self::ExprE { fp - v * self.alpha_pow(alpha_idx) } - - /// `e − v` (extension minus base). The expression ops only implement - /// base ∘ ext (base operand LEFT), so the generic route — and the shape - /// this default keeps in the captured IR — is `−(v − e)`: one mixed sub - /// plus a full extension negation. The eval folders override it with - /// `FieldElement::sub_subfield`, which touches only component 0. - fn ext_sub_base(&self, e: Self::ExprE, v: Self::Expr) -> Self::ExprE { - -(v - e) - } } // ============================================================================= @@ -496,10 +487,6 @@ where fp - v * &self.alphas[alpha_idx] } } - - fn ext_sub_base(&self, e: FieldElement, v: FieldElement) -> FieldElement { - e.sub_subfield(&v) - } } // ============================================================================= @@ -615,11 +602,6 @@ where self.tracker.mark(constraint_idx); self.ext_out[constraint_idx] = e; } - - fn ext_sub_base(&self, e: FieldElement, v: FieldElement) -> FieldElement { - // Expr = ExprE at the OOD point: a plain extension subtraction. - e - v - } } // ============================================================================= diff --git a/crypto/stark/src/constraints/evaluator.rs b/crypto/stark/src/constraints/evaluator.rs index 9f77abcb1..17b42b058 100644 --- a/crypto/stark/src/constraints/evaluator.rs +++ b/crypto/stark/src/constraints/evaluator.rs @@ -94,25 +94,16 @@ where let acc_transition = if is_uniform { // All constraints share one zerofier: factor it out of the sum. let z = zerofier_data.get_uniform(i); - // transition_coefficients are the powers β⁰, β¹, … (round 2), - // so constraint 0's coefficient is one: seed the sum with its - // promoted evaluation and fold the rest — skips the - // multiply-by-one without branching on the value. - let mut base_iter = base_buf.iter().zip(&transition_coefficients[..num_base]); - let mut ext_iter = transition_buf[num_base..] - .iter() - .zip(&transition_coefficients[num_base..]); - let seed = match base_iter.next() { - Some((eval, _beta0)) => eval.clone().to_extension(), - None => match ext_iter.next() { - Some((eval, _beta0)) => eval.clone(), - None => FieldElement::zero(), - }, - }; // F×E inner product for base constraints (3 muls per term) - let sum = base_iter.fold(seed, |acc, (eval, beta)| acc + eval * beta); + let mut sum = base_buf + .iter() + .zip(&transition_coefficients[..num_base]) + .fold(FieldElement::zero(), |acc, (eval, beta)| acc + eval * beta); // E×E for extension constraints (9 muls per term) - let sum = ext_iter.fold(sum, |acc, (eval, beta)| acc + eval * beta); + sum = transition_buf[num_base..] + .iter() + .zip(&transition_coefficients[num_base..]) + .fold(sum, |acc, (eval, beta)| acc + eval * beta); z * &sum } else { let mut sum = base_buf diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 0ed4b56ad..6499ad1e2 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -1991,7 +1991,9 @@ where { let z = b.challenge(0); let bus = b.const_base(interaction.bus_id); - let mut fp = b.ext_sub_base(z, bus); + // `bus` is base and `z` ext; the tower only implements base − ext (base + // operand LEFT), so z − bus is written −(bus − z). + let mut fp = -(bus - z); let mut alpha_idx = 1; for bv in &interaction.values { let (next, consumed) = emit_busvalue_fingerprint::(b, bv, offset, alpha_idx, fp); @@ -2019,25 +2021,25 @@ where let fp_a = emit_fingerprint::(b, interaction_a, 0); let fp_b = emit_fingerprint::(b, interaction_b, 0); - // is_sender is a compile-time bool, resolved as sub vs add — subtracting - // a receiver's negated term is adding it (x − (−t) = x + t), which skips - // the ext negation entirely. m·fp is base×ext = ext (base operand LEFT). + // is_sender is a compile-time bool, resolved as add vs neg instead of an + // ext×ext sign multiply (same optimization as the runtime body). m·fp is + // base×ext = ext (base operand LEFT). let term_a = m_a * fp_b.clone(); - let term_b = m_b * fp_a.clone(); - - // c · fp_a · fp_b: c is aux (ext), so this is ext throughout. - let main = c * fp_a * fp_b; - let acc = if interaction_a.is_sender { - main - term_a + let term_a = if interaction_a.is_sender { + term_a } else { - main + term_a + -term_a }; - let acc = if interaction_b.is_sender { - acc - term_b + let term_b = m_b * fp_a.clone(); + let term_b = if interaction_b.is_sender { + term_b } else { - acc + term_b + -term_b }; - b.emit_ext(idx, acc); + + // c · fp_a · fp_b: c is aux (ext), so this is ext throughout. + let main = c * fp_a * fp_b; + b.emit_ext(idx, main - term_a - term_b); } /// Emit the accumulated constraint (with 1–2 absorbed interactions). @@ -2069,36 +2071,23 @@ where // delta · f − sign · m let m = emit_multiplicity::(b, &absorbed[0].multiplicity, 1); let f = emit_fingerprint::(b, &absorbed[0], 1); - let df = delta * f; - // sign resolved as ext−base vs base+ext: for a sender the root is - // df − m; for a receiver, df − (−m) = m + df (base operand LEFT). - if absorbed[0].is_sender { - b.ext_sub_base(df, m) - } else { - m + df - } + let mt = if absorbed[0].is_sender { m } else { -m }; + // delta · f is ext; `mt` is base. The tower only implements base − + // ext (base operand LEFT), so write `delta·f − mt` as `−(mt − delta·f)`. + -(mt - delta * f) } 2 => { - // delta · f1 · f2 − sign1·m1·f2 − sign2·m2·f1, with the signs - // resolved as sub vs add (x − (−t) = x + t) — no ext negation. + // delta · f1 · f2 − sign1·m1·f2 − sign2·m2·f1 let m1 = emit_multiplicity::(b, &absorbed[0].multiplicity, 1); let m2 = emit_multiplicity::(b, &absorbed[1].multiplicity, 1); let f1 = emit_fingerprint::(b, &absorbed[0], 1); let f2 = emit_fingerprint::(b, &absorbed[1], 1); let term1 = m1 * f2.clone(); + let term1 = if absorbed[0].is_sender { term1 } else { -term1 }; let term2 = m2 * f1.clone(); - let acc = delta * f1 * f2; - let acc = if absorbed[0].is_sender { - acc - term1 - } else { - acc + term1 - }; - if absorbed[1].is_sender { - acc - term2 - } else { - acc + term2 - } + let term2 = if absorbed[1].is_sender { term2 } else { -term2 }; + delta * f1 * f2 - term1 - term2 } _ => unreachable!("absorbed must contain 1 or 2 interactions"), }; From 3638d7a1c7f41de9f9e18f57231d2098108b85b5 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 16:38:39 -0300 Subject: [PATCH 45/52] stark: borrow trace rows in place for prover transition eval The LDE buffers have been row-major since the row-major LDE rework, but the evaluator still gather-copied every main and aux column of every transition offset into an owned Frame on each LDE point (~150-200 element clones per row per table) before the constraint body ran. Replace the Prover context's frame with RowFrame: one borrowed (main, aux) row-slice pair per transition offset, taken straight from the row-major storage with the same cyclic row arithmetic. The folder and the IR interpreter read rows[offset][col] directly; the per-thread preallocated Frame and fill_from_lde are deleted (single-row steps only - the sole shape since virtual columns were removed; asserted). Frame stays for the verifier/OOD path and debug validation, which bridges via Frame::as_row_frame. Reads the same values from the same memory - proofs are unchanged. --- crypto/stark/src/constraint_ir/interp.rs | 7 +- crypto/stark/src/constraint_ir/tests.rs | 49 +++++- crypto/stark/src/constraints/builder.rs | 18 +- crypto/stark/src/constraints/builder_tests.rs | 31 +++- crypto/stark/src/constraints/evaluator.rs | 42 ++--- crypto/stark/src/debug.rs | 2 +- crypto/stark/src/frame.rs | 164 +++++++++++------- crypto/stark/src/lookup.rs | 2 +- crypto/stark/src/trace.rs | 12 ++ crypto/stark/src/traits.rs | 10 +- prover/src/tests/constraint_emit_tests.rs | 9 +- prover/src/tests/constraint_set_tests_a.rs | 9 +- prover/src/tests/constraint_set_tests_b.rs | 9 +- prover/src/tests/cpu32_tests.rs | 8 +- prover/src/tests/ec_scalar_tests.rs | 8 +- prover/src/tests/ecdas_tests.rs | 8 +- prover/src/tests/ecsm_tests.rs | 8 +- 17 files changed, 254 insertions(+), 142 deletions(-) diff --git a/crypto/stark/src/constraint_ir/interp.rs b/crypto/stark/src/constraint_ir/interp.rs index 9bb049c27..a03044066 100644 --- a/crypto/stark/src/constraint_ir/interp.rs +++ b/crypto/stark/src/constraint_ir/interp.rs @@ -175,7 +175,7 @@ pub fn eval_program( E: IsField, { let TransitionEvaluationContext::Prover { - frame, + rows, rap_challenges, logup_alpha_powers, logup_table_offset, @@ -188,12 +188,11 @@ pub fn eval_program( let values = run( prog, |main, offset, row, col| { - let step: &TableView = frame.get_evaluation_step(offset as usize); debug_assert_eq!(row, 0, "tables read row 0 of each frame step"); if main { - Value::Base(step.get_main_evaluation_element(0, col as usize).clone()) + Value::Base(rows.main(offset as usize, col as usize).clone()) } else { - Value::Ext(step.get_aux_evaluation_element(0, col as usize).clone()) + Value::Ext(rows.aux(offset as usize, col as usize).clone()) } }, |idx| rap_challenges[idx as usize].clone(), diff --git a/crypto/stark/src/constraint_ir/tests.rs b/crypto/stark/src/constraint_ir/tests.rs index 57a2bef39..bbb52fa6b 100644 --- a/crypto/stark/src/constraint_ir/tests.rs +++ b/crypto/stark/src/constraint_ir/tests.rs @@ -200,7 +200,13 @@ fn frame_offset_reads_next_step() { let alpha: Vec = vec![]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &rap, + &alpha, + &offset, + &shifts, + ); let mut base_evals = vec![FpE::zero()]; let mut ext_evals: Vec = vec![]; @@ -234,7 +240,13 @@ fn mixed_base_ext_auto_embeds() { let alpha: Vec = vec![]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &rap, + &alpha, + &offset, + &shifts, + ); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; @@ -267,7 +279,13 @@ fn explicit_embed_and_ext_neg() { let alpha: Vec = vec![]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &rap, + &alpha, + &offset, + &shifts, + ); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero()]; @@ -317,7 +335,13 @@ fn all_leaf_kinds_logup_shaped() { let step = TableView::::new(vec![main_row], vec![aux_row]); let frame = Frame::::new(vec![step]); let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &rap, + &alpha, + &offset, + &shifts, + ); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero()]; @@ -354,7 +378,13 @@ fn prover_entry_point_splits_base_and_ext() { let alpha = vec![ext3(3, 3, 3)]; let offset = ExtE::zero(); let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &rap, + &alpha, + &offset, + &shifts, + ); let mut base_evals = vec![FpE::zero()]; let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; @@ -485,8 +515,13 @@ fn non_goldilocks_reflexive_tower_builds_and_interprets() { let alpha: Vec = vec![]; let offset = g(0); let shifts = PackingShifts::::new(); - let ctx = - TransitionEvaluationContext::::new_prover(&frame, &rap, &alpha, &offset, &shifts); + let ctx = TransitionEvaluationContext::::new_prover( + frame.as_row_frame(), + &rap, + &alpha, + &offset, + &shifts, + ); let mut base_evals = vec![GE::zero()]; let mut ext_evals = vec![GE::zero(), GE::zero()]; eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index b772353ad..9b02b40da 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -26,7 +26,7 @@ use math::field::element::FieldElement; use math::field::traits::{IsField, IsSubFieldOf}; use crate::constraint_ir::{ConstraintProgram, Dim, IrBuilder}; -use crate::frame::Frame; +use crate::frame::{Frame, RowFrame}; use crate::traits::TransitionEvaluationContext; // ============================================================================= @@ -372,7 +372,7 @@ where F: IsSubFieldOf, E: IsField, { - frame: &'a Frame, + rows: RowFrame<'a, F, E>, challenges: &'a [FieldElement], alphas: &'a [FieldElement], logup_table_offset: &'a FieldElement, @@ -397,7 +397,7 @@ where ext_out: &'a mut [FieldElement], ) -> Self { let TransitionEvaluationContext::Prover { - frame, + rows, rap_challenges, logup_alpha_powers, logup_table_offset, @@ -408,7 +408,7 @@ where }; let num_constraints = base_out.len().max(ext_out.len()); Self { - frame, + rows: *rows, challenges: rap_challenges, alphas: logup_alpha_powers, logup_table_offset, @@ -434,16 +434,10 @@ where type ExprE = FieldElement; fn main(&self, offset: usize, col: usize) -> FieldElement { - self.frame - .get_evaluation_step(offset) - .get_main_evaluation_element(0, col) - .clone() + self.rows.main(offset, col).clone() } fn aux(&self, offset: usize, col: usize) -> FieldElement { - self.frame - .get_evaluation_step(offset) - .get_aux_evaluation_element(0, col) - .clone() + self.rows.aux(offset, col).clone() } fn challenge(&self, idx: usize) -> FieldElement { self.challenges[idx].clone() diff --git a/crypto/stark/src/constraints/builder_tests.rs b/crypto/stark/src/constraints/builder_tests.rs index e48776422..7e2b30970 100644 --- a/crypto/stark/src/constraints/builder_tests.rs +++ b/crypto/stark/src/constraints/builder_tests.rs @@ -188,7 +188,7 @@ fn prover_folder_matches_direct_arithmetic() { let challenges = vec![t.challenge0]; let alphas = vec![t.alpha0]; let ctx = TransitionEvaluationContext::new_prover( - &frame, + frame.as_row_frame(), &challenges, &alphas, &t.offset, @@ -226,7 +226,7 @@ fn prover_folder_matches_interpreted_capture() { let challenges = vec![t.challenge0]; let alphas = vec![t.alpha0]; let ctx = TransitionEvaluationContext::new_prover( - &frame, + frame.as_row_frame(), &challenges, &alphas, &t.offset, @@ -360,8 +360,13 @@ fn prover_folder_missing_emit_asserts() { let challenges: Vec = vec![]; let alphas: Vec = vec![]; let offset = ExtE::zero(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &challenges, &alphas, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &offset, + &shifts, + ); let mut base_out = vec![FpE::zero(); 2]; let mut ext_out = vec![ExtE::zero(); 2]; @@ -381,8 +386,13 @@ fn prover_folder_double_emit_asserts() { let challenges: Vec = vec![]; let alphas: Vec = vec![]; let offset = ExtE::zero(); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &challenges, &alphas, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &offset, + &shifts, + ); let mut base_out = vec![FpE::zero(); 2]; let mut ext_out = vec![ExtE::zero(); 2]; @@ -594,8 +604,13 @@ fn next_row_aux_and_multi_alpha_folder_matches_capture() { .map(|s| TableView::new(vec![rows[s].clone()], vec![auxs[s].clone()])) .collect(); let frame = Frame::::new(steps); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &challenges, &alphas, &offset, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &offset, + &shifts, + ); let mut folder_base = vec![FpE::zero(); num_base]; let mut folder_ext = vec![ExtE::zero(); meta.len()]; diff --git a/crypto/stark/src/constraints/evaluator.rs b/crypto/stark/src/constraints/evaluator.rs index 17b42b058..1c84f22ea 100644 --- a/crypto/stark/src/constraints/evaluator.rs +++ b/crypto/stark/src/constraints/evaluator.rs @@ -1,6 +1,6 @@ use super::boundary::BoundaryConstraints; use crate::domain::Domain; -use crate::frame::Frame; +use crate::frame::RowFrame; use crate::lookup::{BusPublicInputs, LOGUP_CHALLENGE_ALPHA, PackingShifts, compute_alpha_powers}; use crate::trace::LDETraceTable; use crate::traits::{AIR, TransitionEvaluationContext, ZerofierEvaluations}; @@ -61,29 +61,21 @@ where // Precompute packing shift constants once for all LDE domain points. let packing_shifts = PackingShifts::::new(); - // Per-thread buffers via map_init: each Rayon worker allocates once, - // then reuses for all iterations assigned to that thread. - // The Frame is pre-allocated and filled in-place to avoid Vec allocations - // on every LDE point (a significant fraction of total CPU time). - let blowup_factor = lde_trace.blowup_factor; - let lde_step_size = lde_trace.lde_step_size; - let rows_per_step = lde_step_size / blowup_factor; - let num_main_cols = lde_trace.num_main_cols(); - let num_aux_cols = lde_trace.num_aux_cols(); - let num_offsets = offsets.len(); - + // Per-thread output buffers via map_init: each Rayon worker allocates + // once, then reuses for all iterations assigned to that thread. The + // trace rows themselves are BORROWED in place per LDE point (the LDE + // buffers are row-major) — no per-row gather copy. // Per-row evaluation, shared by the parallel and sequential paths below: - // fill the frame, evaluate transition constraints, accumulate with zerofiers. + // borrow the rows, evaluate transition constraints, accumulate with zerofiers. let eval_row = |i: usize, boundary: FieldElement, transition_buf: &mut [FieldElement], - base_buf: &mut [FieldElement], - frame: &mut Frame| + base_buf: &mut [FieldElement]| -> FieldElement { - frame.fill_from_lde(lde_trace, i, offsets); + let rows = RowFrame::from_lde(lde_trace, i, offsets); let ctx = TransitionEvaluationContext::new_prover( - frame, + rows, rap_challenges, &logup_alpha_powers, logup_table_offset, @@ -136,16 +128,10 @@ where ( vec![FieldElement::::zero(); num_transition], vec![FieldElement::::zero(); num_base], - Frame::preallocate( - num_offsets, - rows_per_step, - num_main_cols, - num_aux_cols, - ), ) }, - |(transition_buf, base_buf, frame), (i, boundary)| { - eval_row(i, boundary, transition_buf, base_buf, frame) + |(transition_buf, base_buf), (i, boundary)| { + eval_row(i, boundary, transition_buf, base_buf) }, ) .collect() @@ -155,15 +141,11 @@ where { let mut transition_buf = vec![FieldElement::::zero(); num_transition]; let mut base_buf = vec![FieldElement::::zero(); num_base]; - let mut frame = - Frame::preallocate(num_offsets, rows_per_step, num_main_cols, num_aux_cols); boundary_evaluation .into_iter() .enumerate() - .map(|(i, boundary)| { - eval_row(i, boundary, &mut transition_buf, &mut base_buf, &mut frame) - }) + .map(|(i, boundary)| eval_row(i, boundary, &mut transition_buf, &mut base_buf)) .collect() } } diff --git a/crypto/stark/src/debug.rs b/crypto/stark/src/debug.rs index 04ce1cf62..d2f049190 100644 --- a/crypto/stark/src/debug.rs +++ b/crypto/stark/src/debug.rs @@ -104,7 +104,7 @@ pub fn validate_trace< for step in 0..lde_trace.num_steps() { let frame = Frame::read_step_from_lde(&lde_trace, step, &air.context().transition_offsets); let transition_evaluation_context = TransitionEvaluationContext::new_prover( - &frame, + frame.as_row_frame(), rap_challenges, &logup_alpha_powers, &logup_table_offset, diff --git a/crypto/stark/src/frame.rs b/crypto/stark/src/frame.rs index 952a3a110..deaf4e5ed 100644 --- a/crypto/stark/src/frame.rs +++ b/crypto/stark/src/frame.rs @@ -3,6 +3,80 @@ use itertools::Itertools; use math::field::element::FieldElement; use math::field::traits::{IsField, IsSubFieldOf}; +/// Maximum number of transition offsets a [`RowFrame`] can hold. Every +/// production table uses two (`[0, 1]`); the widest example AIR uses three. +pub const MAX_TRANSITION_OFFSETS: usize = 4; + +/// Borrowed per-row view of the trace for prover-side transition +/// evaluation: one contiguous `(main, aux)` row-slice pair per transition +/// offset, taken IN PLACE from the row-major storage. Replaces the per-row +/// gather-copy into an owned [`Frame`] on the evaluator hot path — the LDE +/// buffers are row-major, so a step is just two borrowed slices. +/// +/// Requires single-row steps (step_size 1) — the only shape since +/// virtual columns were removed. +pub struct RowFrame<'a, F: IsSubFieldOf, E: IsField> { + mains: [&'a [FieldElement]; MAX_TRANSITION_OFFSETS], + auxs: [&'a [FieldElement]; MAX_TRANSITION_OFFSETS], + num_offsets: usize, +} + +// Manual impls: the derives would demand `F: Copy`/`E: Copy`, but every field +// is a shared reference (or usize), which is Copy for any field type. +impl, E: IsField> Clone for RowFrame<'_, F, E> { + fn clone(&self) -> Self { + *self + } +} +impl, E: IsField> Copy for RowFrame<'_, F, E> {} + +impl<'a, F: IsSubFieldOf, E: IsField> RowFrame<'a, F, E> { + /// Borrow the rows for LDE point `row` at each transition offset, + /// wrapping cyclically at the domain end (same row arithmetic as + /// [`Frame::fill_from_lde`] with single-row steps). + pub fn from_lde(lde_trace: &'a LDETraceTable, row: usize, offsets: &[usize]) -> Self { + debug_assert_eq!( + lde_trace.lde_step_size, lde_trace.blowup_factor, + "RowFrame requires single-row steps (step_size 1)" + ); + assert!( + offsets.len() <= MAX_TRANSITION_OFFSETS, + "RowFrame supports at most {MAX_TRANSITION_OFFSETS} transition offsets" + ); + let num_rows = lde_trace.num_rows(); + let mut mains: [&'a [FieldElement]; MAX_TRANSITION_OFFSETS] = + [&[]; MAX_TRANSITION_OFFSETS]; + let mut auxs: [&'a [FieldElement]; MAX_TRANSITION_OFFSETS] = + [&[]; MAX_TRANSITION_OFFSETS]; + for (k, &offset) in offsets.iter().enumerate() { + let idx = (row + offset * lde_trace.lde_step_size) % num_rows; + mains[k] = lde_trace.main_row(idx); + auxs[k] = lde_trace.aux_row(idx); + } + Self { + mains, + auxs, + num_offsets: offsets.len(), + } + } + + /// The main-trace element at (offset position, column). + #[inline(always)] + pub fn main(&self, offset: usize, col: usize) -> &FieldElement { + &self.mains[offset][col] + } + + /// The aux-trace element at (offset position, column). + #[inline(always)] + pub fn aux(&self, offset: usize, col: usize) -> &FieldElement { + &self.auxs[offset][col] + } + + pub fn num_offsets(&self) -> usize { + self.num_offsets + } +} + /// A frame represents a collection of trace steps. /// The collected steps are all the necessary steps for /// all transition constraints over a trace to be evaluated. @@ -23,6 +97,31 @@ impl, E: IsField> Frame { &self.steps[step] } + /// Borrow this frame's single-row steps as a [`RowFrame`] — the bridge + /// for callers that own a `Frame` (debug validation, tests); the + /// evaluator hot loop uses [`RowFrame::from_lde`] directly. + pub fn as_row_frame(&self) -> RowFrame<'_, F, E> { + assert!( + self.steps.len() <= MAX_TRANSITION_OFFSETS, + "RowFrame supports at most {MAX_TRANSITION_OFFSETS} transition offsets" + ); + let mut mains: [&[FieldElement]; MAX_TRANSITION_OFFSETS] = [&[]; MAX_TRANSITION_OFFSETS]; + let mut auxs: [&[FieldElement]; MAX_TRANSITION_OFFSETS] = [&[]; MAX_TRANSITION_OFFSETS]; + for (k, step) in self.steps.iter().enumerate() { + debug_assert!( + step.data.len() <= 1 && step.aux_data.len() <= 1, + "RowFrame requires single-row steps (step_size 1)" + ); + mains[k] = step.data.first().map(|r| r.as_slice()).unwrap_or(&[]); + auxs[k] = step.aux_data.first().map(|r| r.as_slice()).unwrap_or(&[]); + } + RowFrame { + mains, + auxs, + num_offsets: self.steps.len(), + } + } + /// Build a Frame by gathering row data from a column-major LDETraceTable. /// /// Each step gathers elements from columns into owned Vecs. For the typical @@ -74,69 +173,4 @@ impl, E: IsField> Frame { let row = lde_trace.step_to_row(step); Self::read_from_lde(lde_trace, row, offsets) } - - /// Pre-allocate a Frame with the right dimensions for reuse in hot loops. - /// - /// The frame will have `offsets.len()` steps, each containing - /// `step_size / blowup_factor` rows (typically 1) of main and aux columns. - pub fn preallocate( - num_offsets: usize, - rows_per_step: usize, - num_main_cols: usize, - num_aux_cols: usize, - ) -> Self { - let steps = (0..num_offsets) - .map(|_| { - let main_data: Vec>> = (0..rows_per_step) - .map(|_| vec![FieldElement::zero(); num_main_cols]) - .collect(); - let aux_data: Vec>> = (0..rows_per_step) - .map(|_| vec![FieldElement::zero(); num_aux_cols]) - .collect(); - TableView::new(main_data, aux_data) - }) - .collect(); - Frame { steps } - } - - /// Fill a pre-allocated frame from LDE data, without allocating. - /// - /// The frame must have been created with `preallocate` with matching dimensions. - pub fn fill_from_lde( - &mut self, - lde_trace: &LDETraceTable, - row: usize, - offsets: &[usize], - ) { - let blowup_factor = lde_trace.blowup_factor; - let num_rows = lde_trace.num_rows(); - let step_size = lde_trace.lde_step_size; - let num_main_cols = lde_trace.num_main_cols(); - let num_aux_cols = lde_trace.num_aux_cols(); - - for (step_idx, &offset) in offsets.iter().enumerate() { - let initial_step_row = row + offset * step_size; - let end_step_row = initial_step_row + step_size; - let step = &mut self.steps[step_idx]; - - let mut sub_row_idx = 0; - let mut step_row = initial_step_row; - while step_row < end_step_row { - let step_row_idx = step_row % num_rows; - - // Overwrite main row elements - for col in 0..num_main_cols { - step.data[sub_row_idx][col] = lde_trace.get_main(step_row_idx, col).clone(); - } - - // Overwrite aux row elements - for col in 0..num_aux_cols { - step.aux_data[sub_row_idx][col] = lde_trace.get_aux(step_row_idx, col).clone(); - } - - sub_row_idx += 1; - step_row += blowup_factor; - } - } - } } diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 6499ad1e2..b31e20927 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -2327,7 +2327,7 @@ mod logup_single_source_tests { let table_offset = rand_fp3(&mut rng); let prover_ctx = TransitionEvaluationContext::new_prover( - &frame, + frame.as_row_frame(), &rap_challenges, &alpha_powers, &table_offset, diff --git a/crypto/stark/src/trace.rs b/crypto/stark/src/trace.rs index 0782ea245..831b95284 100644 --- a/crypto/stark/src/trace.rs +++ b/crypto/stark/src/trace.rs @@ -453,6 +453,18 @@ where &self.aux_data[row * self.num_aux_cols + col] } + /// Borrow a full main-trace row as a contiguous slice (row-major buffer). + #[inline] + pub fn main_row(&self, row: usize) -> &[FieldElement] { + &self.main_data[row * self.num_main_cols..(row + 1) * self.num_main_cols] + } + + /// Borrow a full aux-trace row as a contiguous slice (row-major buffer). + #[inline] + pub fn aux_row(&self, row: usize) -> &[FieldElement] { + &self.aux_data[row * self.num_aux_cols..(row + 1) * self.num_aux_cols] + } + /// Gather a full main-trace row into an owned Vec. /// Used by `open_trace_polys` (called ~30 times per table, allocation is negligible). pub fn gather_main_row(&self, row_idx: usize) -> Vec> { diff --git a/crypto/stark/src/traits.rs b/crypto/stark/src/traits.rs index 4818d8be4..747290cdb 100644 --- a/crypto/stark/src/traits.rs +++ b/crypto/stark/src/traits.rs @@ -15,7 +15,7 @@ use crate::{ use super::{ config::Commitment, constraints::boundary::BoundaryConstraints, context::AirContext, - frame::Frame, proof::options::ProofOptions, trace::TraceTable, + frame::Frame, frame::RowFrame, proof::options::ProofOptions, trace::TraceTable, }; /// Deduplicated zerofier evaluations: unique zerofier vectors indexed by constraint. @@ -71,7 +71,9 @@ where E: IsField, { Prover { - frame: &'a Frame, + /// Borrowed row view straight into the row-major trace storage — + /// the prover hot path never copies rows into an owned frame. + rows: RowFrame<'a, F, E>, rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, @@ -92,14 +94,14 @@ where E: IsField, { pub fn new_prover( - frame: &'a Frame, + rows: RowFrame<'a, F, E>, rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, packing_shifts: &'a PackingShifts, ) -> Self { Self::Prover { - frame, + rows, rap_challenges, logup_alpha_powers, logup_table_offset, diff --git a/prover/src/tests/constraint_emit_tests.rs b/prover/src/tests/constraint_emit_tests.rs index 5439d4635..c9a6410ca 100644 --- a/prover/src/tests/constraint_emit_tests.rs +++ b/prover/src/tests/constraint_emit_tests.rs @@ -115,8 +115,13 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &no_ch, &no_ch, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_ch, + &no_ch, + &offset_e, + &shifts, + ); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); diff --git a/prover/src/tests/constraint_set_tests_a.rs b/prover/src/tests/constraint_set_tests_a.rs index 2387f0cad..7be6d4359 100644 --- a/prover/src/tests/constraint_set_tests_a.rs +++ b/prover/src/tests/constraint_set_tests_a.rs @@ -100,8 +100,13 @@ where // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &no_ch, &no_ch, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_ch, + &no_ch, + &offset_e, + &shifts, + ); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index c81a03008..492d6dad8 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -93,8 +93,13 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz // --- ProverEvalFolder (base) --- let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); - let ctx = - TransitionEvaluationContext::new_prover(&frame, &no_ch, &no_ch, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_ch, + &no_ch, + &offset_e, + &shifts, + ); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base_out, &mut ext_out); diff --git a/prover/src/tests/cpu32_tests.rs b/prover/src/tests/cpu32_tests.rs index 8627e107a..37f2bfec2 100644 --- a/prover/src/tests/cpu32_tests.rs +++ b/prover/src/tests/cpu32_tests.rs @@ -25,7 +25,13 @@ fn eval_cpu32(row: &[FE]) -> Vec { let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &no_e, &no_e, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_e, + &no_e, + &offset_e, + &shifts, + ); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ec_scalar_tests.rs b/prover/src/tests/ec_scalar_tests.rs index 01afec94e..b8a60edc5 100644 --- a/prover/src/tests/ec_scalar_tests.rs +++ b/prover/src/tests/ec_scalar_tests.rs @@ -27,7 +27,13 @@ fn eval_row(trace: &TraceTable, row: usize let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &no_e, &no_e, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_e, + &no_e, + &offset_e, + &shifts, + ); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ecdas_tests.rs b/prover/src/tests/ecdas_tests.rs index bb5083323..b692d5c4a 100644 --- a/prover/src/tests/ecdas_tests.rs +++ b/prover/src/tests/ecdas_tests.rs @@ -58,7 +58,13 @@ fn eval_row(trace: &TraceTable, row: usize let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &no_e, &no_e, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_e, + &no_e, + &offset_e, + &shifts, + ); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ecsm_tests.rs b/prover/src/tests/ecsm_tests.rs index 7ba04dc7d..3b671a8fd 100644 --- a/prover/src/tests/ecsm_tests.rs +++ b/prover/src/tests/ecsm_tests.rs @@ -54,7 +54,13 @@ fn eval_row(trace: &TraceTable, row: usize let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = TransitionEvaluationContext::new_prover(&frame, &no_e, &no_e, &offset_e, &shifts); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_e, + &no_e, + &offset_e, + &shifts, + ); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); From 30b614864994f265be38a34bd427d36f1752e4f3 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 17:13:57 -0300 Subject: [PATCH 46/52] prover: drop a redundant Multiplicity clone in shift bus setup Found by clippy::redundant_clone sweeping for pointless clones; the only hit in production code (setup-time, cosmetic). The remaining test/example hits are left as-is. --- prover/src/tables/shift.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index dc8165555..d66e48ab5 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -537,7 +537,7 @@ pub fn bus_interactions() -> Vec { // second output = extension - X[4] (the carry, expressed as a linear combination) interactions.push(BusInteraction::sender( BusId::Hwsl, - one_minus_zbs.clone(), + one_minus_zbs, vec![ BusValue::linear(vec![LinearTerm::Column { coefficient: 65535, From 26782e26d1e4048ff5a71938bdf4fca494accac3 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 18:31:21 -0300 Subject: [PATCH 47/52] docs: describe the constraint code as it is, not as it was Four-agent review sweep found ~60 stale comments left behind by the multi-phase migration: doc blocks claiming the deleted per-constraint structs still exist ('the old structs stay for now'), ~31 dangling rustdoc links to deleted symbols (Twin of X, matches X::eval), count headers that disagreed with the code (MEMW says 11 constraints, has 15; SHIFT lists 26 columns, has 29), and references to removed concepts (virtual columns, periodic tests, empty_constraints()). All comments now describe the current code in plain present tense -- constraint-index maps verified against meta()/eval(), bus names and counts recounted from bus_interactions(). No historical framing: migration provenance lives in git, not doc comments. --- crypto/stark/examples/examples_cli.rs | 4 +-- crypto/stark/src/constraints/zerofier.rs | 3 -- .../stark/src/examples/multi_table_lookup.rs | 4 +-- crypto/stark/src/tests/air_tests.rs | 2 +- prover/src/constraints/cpu.rs | 28 +++++++----------- prover/src/constraints/templates.rs | 29 ++++++++----------- prover/src/continuation.rs | 8 ++--- prover/src/tables/branch.rs | 12 ++++---- prover/src/tables/commit.rs | 3 +- prover/src/tables/cpu.rs | 2 +- prover/src/tables/cpu32.rs | 3 +- prover/src/tables/dvrm.rs | 21 +++++--------- prover/src/tables/ec_scalar.rs | 6 ++-- prover/src/tables/ecdas.rs | 15 ++++------ prover/src/tables/ecsm.rs | 15 ++++------ prover/src/tables/keccak.rs | 3 +- prover/src/tables/keccak_rnd.rs | 7 ++--- prover/src/tables/load.rs | 7 ++--- prover/src/tables/memw.rs | 6 ++-- prover/src/tables/memw_aligned.rs | 10 +++---- prover/src/tables/memw_register.rs | 3 +- prover/src/tables/mul.rs | 12 +++----- prover/src/tables/shift.rs | 23 +++++++-------- prover/src/tables/store.rs | 3 +- 24 files changed, 91 insertions(+), 138 deletions(-) diff --git a/crypto/stark/examples/examples_cli.rs b/crypto/stark/examples/examples_cli.rs index ad455b1ba..58afa0d5f 100644 --- a/crypto/stark/examples/examples_cli.rs +++ b/crypto/stark/examples/examples_cli.rs @@ -8,8 +8,8 @@ //! //! Proofs are bincode-serialized, mirroring `bin/cli`'s VM-proof format. //! Trace sizes and public inputs mirror the existing stark tests -//! (`tests/air_tests.rs`, `tests/small_trace_tests.rs`, -//! `tests/bus_tests/completeness_tests.rs`) so a proof produced by one +//! (`src/tests/air_tests.rs`, `src/tests/small_trace_tests.rs`, +//! `src/tests/bus_tests/completeness_tests.rs`) so a proof produced by one //! version of the constraint system can be checked by another. //! //! Exit code 0 = success (prove written / verify accepted); nonzero = failure. diff --git a/crypto/stark/src/constraints/zerofier.rs b/crypto/stark/src/constraints/zerofier.rs index 9308c9674..95da082da 100644 --- a/crypto/stark/src/constraints/zerofier.rs +++ b/crypto/stark/src/constraints/zerofier.rs @@ -5,9 +5,6 @@ //! over each constraint's plain metadata. Every constraint applies to every //! row of the trace, so the zerofier is `x^N − 1` corrected by the constraint's //! `end_exemptions` (the last rows it must skip). -//! The bodies were relocated from the deleted boxed-constraint trait's default -//! methods, specialized to the every-row shape (equivalence was asserted by -//! migration-time tests). use math::field::element::FieldElement; use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; diff --git a/crypto/stark/src/examples/multi_table_lookup.rs b/crypto/stark/src/examples/multi_table_lookup.rs index 91b0b9999..5f14530c0 100644 --- a/crypto/stark/src/examples/multi_table_lookup.rs +++ b/crypto/stark/src/examples/multi_table_lookup.rs @@ -1,8 +1,8 @@ //! NOTE(single-source constraints): this example defines NO example-level //! transition constraints — every constraint is LogUp, generated by the //! `AirWithBuses` framework from the bus interactions below. It therefore -//! has no `ConstraintSet` to migrate; it moves to the single-body path -//! together with `AirWithBuses` in the engine-switch phase. +//! has no example-level `ConstraintSet`; it passes `EmptyConstraints` and +//! runs the single-body path together with `AirWithBuses`. use crate::{ constraints::builder::EmptyConstraints, diff --git a/crypto/stark/src/tests/air_tests.rs b/crypto/stark/src/tests/air_tests.rs index d18e18c7d..b6a4108f9 100644 --- a/crypto/stark/src/tests/air_tests.rs +++ b/crypto/stark/src/tests/air_tests.rs @@ -1,4 +1,4 @@ -//! Tests for various AIR implementations (Fibonacci, periodic, RAP, memory, etc.). +//! Tests for various AIR implementations (Fibonacci, RAP, memory, etc.). use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::{ diff --git a/prover/src/constraints/cpu.rs b/prover/src/constraints/cpu.rs index 3340c28ef..f27070193 100644 --- a/prover/src/constraints/cpu.rs +++ b/prover/src/constraints/cpu.rs @@ -70,7 +70,7 @@ use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintS use super::templates::{INV_SHIFT_32, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta}; -/// `col_a · col_b = 0`. Twin of `ProductZeroConstraint`. +/// `col_a · col_b = 0`. pub fn emit_product_zero>( b: &mut B, idx: usize, @@ -86,8 +86,7 @@ pub fn product_zero_meta(idx: usize) -> ConstraintMeta { ConstraintMeta::base(idx, 2) } -/// `(1 − MEMORY − BRANCH) · read_register2 · imm[i] = 0`. Twin of -/// `Arg2ExclusiveConstraint`. +/// `(1 − MEMORY − BRANCH) · read_register2 · imm[i] = 0`. pub fn emit_arg2_exclusive>( b: &mut B, idx: usize, @@ -106,8 +105,7 @@ pub fn arg2_exclusive_meta(idx: usize) -> ConstraintMeta { ConstraintMeta::base(idx, 3) } -/// `(1 − MEMORY) · mem_flags · (1 − mem_flags) = 0`. Twin of -/// `MemFlagsBitConstraint`. +/// `(1 − MEMORY) · mem_flags · (1 − mem_flags) = 0`. pub fn emit_mem_flags_bit>( b: &mut B, idx: usize, @@ -126,7 +124,7 @@ pub fn mem_flags_bit_meta(idx: usize) -> ConstraintMeta { ConstraintMeta::base(idx, 3) } -/// `(1 − flag) · value = 0`. Twin of `RegNotReadIsZeroConstraint`. +/// `(1 − flag) · value = 0`. pub fn emit_reg_not_read_is_zero>( b: &mut B, idx: usize, @@ -144,8 +142,7 @@ pub fn reg_not_read_is_zero_meta(idx: usize) -> ConstraintMeta { ConstraintMeta::base(idx, 2) } -/// `arg2` multiplex for word index `word_idx ∈ {0, 1}`. Twin of -/// `Arg2Constraint`: +/// `arg2` multiplex for word index `word_idx ∈ {0, 1}`: /// /// ```text /// arg2[i] − (MEMORY·imm[i] + BRANCH·rv2[i] + (1−MEMORY−BRANCH)·(rv2[i] + imm[i])) @@ -174,13 +171,12 @@ pub fn emit_arg2>( } /// Metadata for [`emit_arg2`] (degree 2 relies on the live `MEMORY·BRANCH = 0` -/// mutex, as with the struct). +/// mutex). pub fn arg2_meta(idx: usize) -> ConstraintMeta { ConstraintMeta::base(idx, 2) } -/// `cast(res, DWordWL)` word from the four `res` halves (DWordHL); twin of -/// [`res_word`]. +/// `cast(res, DWordWL)` word from the four `res` halves (DWordHL). fn res_word_expr>( b: &B, high: bool, @@ -193,8 +189,7 @@ fn res_word_expr>( b.main(0, lo_col) + b.main(0, hi_col) * b.const_base(SHIFT_16) } -/// `(1 − MEMORY − BRANCH) · (rvd[i] − cast(res, WL)[i]) = 0`. Twin of -/// `RvdEqResConstraint`. +/// `(1 − MEMORY − BRANCH) · (rvd[i] − cast(res, WL)[i]) = 0`. pub fn emit_rvd_eq_res>( b: &mut B, idx: usize, @@ -250,8 +245,7 @@ fn emit_pc_len_add_pair>( b: &mut B, idx: usize, @@ -269,8 +263,7 @@ pub fn branch_rvd_meta(idx: usize) -> [ConstraintMeta; 2] { ] } -/// `branch_cond − (BRANCH·JALR + BRANCH·(1−JALR)·res[0])`. Twin of -/// `BranchCondConstraint`. +/// `branch_cond − (BRANCH·JALR + BRANCH·(1−JALR)·res[0])`. pub fn emit_branch_cond>( b: &mut B, idx: usize, @@ -292,7 +285,6 @@ pub fn branch_cond_meta(idx: usize) -> ConstraintMeta { /// `(1 − branch_cond) · carry · (1 − carry) = 0` for /// `next_pc = pc + instruction_length` (two instances at `idx`, `idx + 1`). -/// Twin of `NextPcAddConstraint::new_pair`. pub fn emit_next_pc_add_pair>( b: &mut B, idx: usize, diff --git a/prover/src/constraints/templates.rs b/prover/src/constraints/templates.rs index bcfe2e7d7..f8f0ec0ea 100644 --- a/prover/src/constraints/templates.rs +++ b/prover/src/constraints/templates.rs @@ -7,7 +7,7 @@ //! - **IS_BIT**: Enforces that a value is binary (0 or 1) //! - Constraint: `cond * X * (1-X) = 0` //! -//! - **ADD**: 64-bit addition with embedded virtual carry columns +//! - **ADD**: 64-bit addition with carries as inline expressions //! - lhs, rhs, sum: DWordWL (2 × 32-bit words) //! - Embeds carry constraints inline @@ -117,7 +117,7 @@ pub enum AddOperand { DWordWL { start_column: usize }, /// Linear combination for lo and hi limbs. - /// Handles: constants, single columns, expressions, and virtual columns. + /// Handles: constants, single columns, and expressions. Linear { /// Terms for the low 32-bit word lo: AddTerms, @@ -230,8 +230,7 @@ impl AddOperand { } /// Creates a Linear operand from explicit lo/hi term lists (at most 4 - /// terms per limb). Use this for complex expressions like `4 - 2*c` or - /// virtual columns. + /// terms per limb). Use this for complex expressions like `4 - 2*c`. pub fn linear(lo: &[AddLinearTerm], hi: &[AddLinearTerm]) -> Self { AddOperand::Linear { lo: AddTerms::of(lo), @@ -244,11 +243,9 @@ impl AddOperand { // Single-body emit functions (ConstraintBuilder front-end) // ========================================================================= // -// Non-destructive twins of the boxed constraint structs above, written once -// against the generic `ConstraintBuilder` so one body serves the compiled -// prover folder, the verifier folder and IR capture. The structs stay for -// now (they are the differential oracle for the emit functions); the table -// conversion deletes them. +// The single-body emit functions: one body written against the generic +// `ConstraintBuilder` serves the compiled prover folder, the verifier folder +// and IR capture. // // Each `emit_*` takes the constraint index it emits at; the matching // `*_meta` returns the idx-ordered metadata (declared degree; default @@ -258,7 +255,7 @@ impl AddOperand { use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta}; /// IS_BIT: `x·(1−x)`, optionally gated by a condition column: -/// `cond·x·(1−x)`. Twin of `IsBitConstraint`. +/// `cond·x·(1−x)`. pub fn emit_is_bit>( b: &mut B, idx: usize, @@ -282,8 +279,7 @@ pub fn is_bit_meta(idx: usize, conditional: bool) -> ConstraintMeta { ConstraintMeta::base(idx, if conditional { 3 } else { 2 }) } -/// One [`AddLinearTerm`]: `column · coefficient` or a constant -/// (matches `AddLinearTerm::eval`'s operand order). +/// One [`AddLinearTerm`]: `column · coefficient` or a constant. fn add_term_expr>( b: &B, t: &AddLinearTerm, @@ -297,7 +293,7 @@ fn add_term_expr>( } } -/// Sum of terms, from zero (matches `eval_terms`). +/// Sum of terms, from zero. fn add_terms_expr>( b: &B, terms: &[AddLinearTerm], @@ -309,7 +305,7 @@ fn add_terms_expr>( acc } -/// An operand's low word (matches `AddOperand::eval_lo`). +/// An operand's low word. fn add_operand_lo>( b: &B, op: &AddOperand, @@ -320,7 +316,7 @@ fn add_operand_lo>( } } -/// An operand's high word (matches `AddOperand::eval_hi`). +/// An operand's high word. fn add_operand_hi>( b: &B, op: &AddOperand, @@ -331,8 +327,7 @@ fn add_operand_hi>( } } -/// The ADD carry pair, emitted from ONE body at `idx` and `idx + 1` -/// (twin of `AddConstraint::new_pair`): +/// The ADD carry pair, emitted from ONE body at `idx` and `idx + 1`: /// /// ```text /// carry_0 = (lhs.lo + rhs.lo − sum.lo)·2⁻³² diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index c601a2611..48f963fac 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -129,7 +129,7 @@ impl ConstraintSet for L2gMemoryConstraints { /// `epoch_label` is this epoch's 1-based label; it is the `fini_epoch` constant /// the fini token carries (not a trace column, since it's the same for every row). /// -/// Uses `empty_constraints()` deliberately: the MU boolean (`MU·(1-MU)=0`), the +/// Uses the `EmptyConstraints` set deliberately: the MU boolean (`MU·(1-MU)=0`), the /// column range checks, and the `init_epoch < fini_epoch` ordering are NOT /// re-asserted here. They are enforced once in the epoch proof's `l2g_memory_air`, /// and `verify_l2g_commitment_binding` ties this global L2G sub-table to the *same* @@ -299,9 +299,9 @@ impl ContinuationProof { } /// Build an epoch's AIRs identically on the prove and verify sides — the single -/// source of truth for the AIR set, so the two halves can never diverge. Mirrors -/// the old integrated path: `VmAirs` (HALT included iff `is_final`), with REGISTER -/// preprocessed to INIT = `register_init` and FINI = `reg_fini`. Continuation epochs +/// source of truth for the AIR set, so the two halves can never diverge. The set +/// is `VmAirs` (HALT included iff `is_final`), with REGISTER preprocessed to +/// INIT = `register_init` and FINI = `reg_fini`. Continuation epochs /// use the L2G bookend, so PAGE is skipped and `page_configs` is empty. The /// epoch-local L2G air is built separately by the caller (it needs the `label`). fn build_epoch_airs( diff --git a/prover/src/tables/branch.rs b/prover/src/tables/branch.rs index fc55149ef..d45618329 100644 --- a/prover/src/tables/branch.rs +++ b/prover/src/tables/branch.rs @@ -358,7 +358,8 @@ pub fn bus_interactions() -> Vec { // ========================================================================= /// `(unmasked_0, unmasked_1)` — the next-pc value repacked into two words, -/// as builder expressions (twin of [`BranchConstraint::compute_next_pc_unmasked`]): +/// as builder expressions (the constraint-expression form of the value +/// [`BranchOperation::compute_next_pc_unmasked`] computes for trace generation): /// /// ```text /// unmasked_0 = unmasked_low_byte + next_pc_low_1·2⁸ + next_pc_high_0·2¹⁶ @@ -376,8 +377,7 @@ fn next_pc_unmasked_expr>( b: &B, base_col_0: usize, @@ -387,8 +387,7 @@ fn carry_0_expr>( (b.main(0, base_col_0) + b.main(0, cols::OFFSET_0) - unmasked_0) * inv_2_32 } -/// `carry_1 = (base_1 + offset_1 + carry_0 − unmasked_1)·2⁻³²` (twin of -/// [`BranchConstraint::compute_carry_1_for`]). +/// `carry_1 = (base_1 + offset_1 + carry_0 − unmasked_1)·2⁻³²`. /// /// Known redundancy: this rebuilds the carry_0 expression (and the unmasked /// next-pc repack) that the sibling constraints also compute. Sharing them @@ -405,8 +404,7 @@ fn carry_1_expr>( (b.main(0, base_col_1) + b.main(0, cols::OFFSET_1) + carry_0 - unmasked_1) * inv_2_32 } -/// The BRANCH table's transition constraints as a single [`ConstraintSet`], -/// mirroring `branch_constraints` index-for-index (5 constraints): +/// The BRANCH table's 5 transition constraints as a single [`ConstraintSet`]: /// - idx 0: `(1 − JALR)·carry_0·(1 − carry_0)` on the pc path (degree 3); /// - idx 1: `(1 − JALR)·carry_1·(1 − carry_1)` on the pc path (degree 3); /// - idx 2: `JALR·carry_0·(1 − carry_0)` on the register path (degree 3); diff --git a/prover/src/tables/commit.rs b/prover/src/tables/commit.rs index cf781b186..ce27d5d80 100644 --- a/prover/src/tables/commit.rs +++ b/prover/src/tables/commit.rs @@ -729,8 +729,7 @@ pub fn bus_interactions() -> Vec { // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// The COMMIT table's transition constraints as a single [`ConstraintSet`], -/// mirroring `create_constraints` index-for-index (8 constraints): +/// The COMMIT table's 8 transition constraints as a single [`ConstraintSet`]: /// - idx 0-2: `IS_BIT` on `first`, `end`, `μ`; /// - idx 3: `(first + end)·(1 − μ) = 0` (first/end ⇒ μ); /// - idx 4,5: `ADD` pair `address + 1 = address_incr` (unconditional); diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 7c7dc3313..42d197942 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -314,7 +314,7 @@ impl CpuOperation { // address `pc + instruction_length` on every BRANCH row (written to `rd` // only by JAL/JALR — `cpu.toml` branch group); `res` // otherwise. The spec computes this `pc + len` via the ADD chip gated on - // `BRANCH`; we pin it with `BranchRvdConstraint` (carry-omitting, like + // `BRANCH`; we pin it with `emit_branch_rvd_pair` (carry-omitting, like // `next_pc`). For conditional branches `rvd` is computed but never // written (`write_register = 0`). let store = f.memory && jalr; // under MEMORY, mem_flags bit 0 = memory_op (1 = store) diff --git a/prover/src/tables/cpu32.rs b/prover/src/tables/cpu32.rs index 7b37bf16a..3786228ae 100644 --- a/prover/src/tables/cpu32.rs +++ b/prover/src/tables/cpu32.rs @@ -588,8 +588,7 @@ pub fn bus_interactions() -> Vec { // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// The CPU32 table's transition constraints as a single [`ConstraintSet`], -/// mirroring `cpu32_constraints` index-for-index (32 constraints): +/// The CPU32 table's 32 transition constraints as a single [`ConstraintSet`]: /// - idx 0-6: `IS_BIT` on `read_register1/2`, `write_register`, `alu`, `add`, /// `sub`, `μ`; /// - idx 7,8: `ADD` pair `arg1 + arg2 = res` (gated on `add`); diff --git a/prover/src/tables/dvrm.rs b/prover/src/tables/dvrm.rs index 148b27400..4a6f3d75b 100644 --- a/prover/src/tables/dvrm.rs +++ b/prover/src/tables/dvrm.rs @@ -27,7 +27,7 @@ //! - Sender: ALU (×3, on the unified bus: ×1 LT-flavored for `|r| < |d|`, //! ×2 MUL-flavored for `n - r = d * q` lo/hi) //! - Sender: ZERO (×5 for div_by_zero, overflow, NEG template) -//! - Receiver: DVRM (×2 for quotient and remainder results) +//! - Receiver: ALU (×2, on the unified bus, for quotient and remainder results) use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; @@ -973,11 +973,8 @@ pub fn bus_interactions() -> Vec { // Single-body constraint set (ConstraintSet front-end) // ========================================================================= // -// Non-destructive twin of `DvrmConstraint` / `dvrm_constraints` above, written -// once against the generic `ConstraintBuilder` so one body serves the compiled -// prover folder, the verifier folder and IR capture. The old structs stay for -// now (they are the differential oracle); the final deletion phase removes -// them. Constraint indices 0..19 match `dvrm_constraints(0)` exactly. +// One body against the generic `ConstraintBuilder` serves the compiled prover +// folder, the verifier folder and IR capture. Constraint indices 0..19. use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; @@ -986,8 +983,7 @@ use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintS pub struct DvrmConstraints; impl DvrmConstraints { - /// Sign-extended QuadWL word `k` (0..4) of a halfword group, matching - /// [`DvrmConstraint::build_extended_quad`]: + /// Sign-extended QuadWL word `k` (0..4) of a halfword group: /// `[hw0 + hw1·2^16, hw2 + hw3·2^16, ext, ext]`, where /// `ext = sign·SIGN_FILL + sign·SIGN_FILL·2^16`. fn ext_quad>( @@ -1019,8 +1015,7 @@ impl DvrmConstraints { } } - /// Virtual carry[i] for `n = n_sub_r + r`, matching - /// [`DvrmConstraint::compute_carry`] (extended QuadWL, recursive chain). + /// Virtual carry[i] for `n = n_sub_r + r` (extended QuadWL, recursive chain). fn carry>( b: &B, i: usize, @@ -1049,8 +1044,8 @@ impl DvrmConstraints { } } - /// `r::DWordWL[i]` (i = 0 → lo32, else hi32), matching the `AbsRFormula` - /// arm; used generically for r or d halfword groups. + /// `r::DWordWL[i]` (i = 0 → lo32, else hi32); used generically for r or d + /// halfword groups. fn dword_wl>( b: &B, lo: usize, @@ -1065,7 +1060,7 @@ impl DvrmConstraints { impl ConstraintSet for DvrmConstraints { fn meta(&self) -> Vec { - // All DVRM constraints are declared degree 2 (see DvrmConstraint::degree). + // All DVRM constraints are declared degree 2. (0..19).map(|i| ConstraintMeta::base(i, 2)).collect() } diff --git a/prover/src/tables/ec_scalar.rs b/prover/src/tables/ec_scalar.rs index 7f8614cfe..da3eecf04 100644 --- a/prover/src/tables/ec_scalar.rs +++ b/prover/src/tables/ec_scalar.rs @@ -273,10 +273,8 @@ pub fn bus_interactions() -> Vec { // Single-body constraint set (ConstraintSet front-end) // ========================================================================= // -// Non-destructive twin of `create_constraints` above, written once against the -// generic `ConstraintBuilder`. The old structs/builder stay as the differential -// oracle; the final deletion phase removes them. Constraint indices 0..20 -// match `create_constraints(0)` exactly. +// One body against the generic `ConstraintBuilder` serves the compiled prover +// folder, the verifier folder and IR capture. Constraint indices 0..20. use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; diff --git a/prover/src/tables/ecdas.rs b/prover/src/tables/ecdas.rs index b8d6ee31d..8b30fbe08 100644 --- a/prover/src/tables/ecdas.rs +++ b/prover/src/tables/ecdas.rs @@ -262,10 +262,8 @@ pub enum Relation { // Single-body constraint set (ConstraintSet front-end) // ========================================================================= // -// Non-destructive twin of `create_constraints` above, written once against the -// generic `ConstraintBuilder`. The old structs/builder stay as the differential -// oracle; the final deletion phase removes them. Constraint indices 0..200 -// match `create_constraints(0)` exactly: +// One body against the generic `ConstraintBuilder` serves the compiled prover +// folder, the verifier folder and IR capture. Constraint indices 0..200: // 0,1,2 : IS_BIT(MU), IS_BIT(OP), IS_BIT(NEXT_OP) // 3 : OP · NEXT_OP // 4 : NEXT_OP · (1 − MU) @@ -278,8 +276,7 @@ use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintS pub struct EcdasConstraints; impl EcdasConstraints { - /// Byte `m` of the base-point order `P` (zero beyond 32 bytes). Twin of - /// [`p_byte`]. + /// Byte `m` of the base-point order `P` (zero beyond 32 bytes). fn p_byte_expr>( b: &B, m: usize, @@ -291,7 +288,7 @@ impl EcdasConstraints { } } - /// Byte `m` of `R` (zero beyond 33 bytes). Twin of `r_byte`. + /// Byte `m` of `R` (zero beyond 33 bytes). fn r_byte_expr>( b: &B, m: usize, @@ -333,7 +330,7 @@ impl EcdasConstraints { s } - /// `S_i` for `relation` at limb `i` (twin of `ConvCarry::s_i`). + /// `S_i` for `relation` at limb `i`. fn s_i>( b: &B, relation: Relation, @@ -385,7 +382,7 @@ impl EcdasConstraints { } } - /// `256·c_i − c_{i-1} − S_i` (twin of `ConvCarry::evaluate`). + /// `256·c_i − c_{i-1} − S_i`. fn conv_carry>( b: &B, relation: Relation, diff --git a/prover/src/tables/ecsm.rs b/prover/src/tables/ecsm.rs index f23e2c7d7..cc87224e4 100644 --- a/prover/src/tables/ecsm.rs +++ b/prover/src/tables/ecsm.rs @@ -628,10 +628,8 @@ impl OverflowKind { // Single-body constraint set (ConstraintSet front-end) // ========================================================================= // -// Non-destructive twin of `create_constraints` above, written once against the -// generic `ConstraintBuilder`. The old structs/builder stay as the differential -// oracle; the final deletion phase removes them. Constraint indices 0..148 -// match `create_constraints(0)` exactly: +// One body against the generic `ConstraintBuilder` serves the compiled prover +// folder, the verifier folder and IR capture. Constraint indices 0..148: // 0 : IS_BIT(MU) // 1..65 : ConvCarry(X2, 0..64) // 65 : ColIsZero(c0(63)) @@ -650,8 +648,7 @@ use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintS pub struct EcsmConstraints; impl EcsmConstraints { - /// Byte `m` of the base-point order `P` (zero beyond 32 bytes). Twin of - /// [`p_byte`]. + /// Byte `m` of the base-point order `P` (zero beyond 32 bytes). fn p_byte_expr>( b: &B, m: usize, @@ -677,7 +674,7 @@ impl EcsmConstraints { } } - /// `S_i` for `relation` at limb `i` (twin of `ConvCarry::s_i`). + /// `S_i` for `relation` at limb `i`. fn s_i>( b: &B, relation: Relation, @@ -713,7 +710,7 @@ impl EcsmConstraints { s } - /// `256·c_i − c_{i-1} − S_i` (twin of `ConvCarry::evaluate`). + /// `256·c_i − c_{i-1} − S_i`. fn conv_carry>( b: &B, relation: Relation, @@ -733,7 +730,7 @@ impl EcsmConstraints { two_pow_8 * c_i - c_prev - Self::s_i(b, relation, i) } - /// The 8 word-carries of the `kind` addition (twin of [`carry_chain`]). + /// The 8 word-carries of the `kind` addition. fn carry_chain>( b: &B, kind: OverflowKind, diff --git a/prover/src/tables/keccak.rs b/prover/src/tables/keccak.rs index 77e71ed5e..31cc4b824 100644 --- a/prover/src/tables/keccak.rs +++ b/prover/src/tables/keccak.rs @@ -455,8 +455,7 @@ pub fn bus_interactions() -> Vec { // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// The KECCAK core table's transition constraints as a single [`ConstraintSet`], -/// mirroring `create_constraints` index-for-index (51 constraints): +/// The KECCAK core table's 51 transition constraints as a single [`ConstraintSet`]: /// - idx 0-49: for `lane_idx ∈ 0..25`, the `ADD` carry pair (gated on `μ`) /// enforcing `state_ptr[lane] = addr + 8·lane_idx` (`addr` DWordBL, /// `state_ptr` DWordHL); diff --git a/prover/src/tables/keccak_rnd.rs b/prover/src/tables/keccak_rnd.rs index f77cb373d..a69acb763 100644 --- a/prover/src/tables/keccak_rnd.rs +++ b/prover/src/tables/keccak_rnd.rs @@ -634,7 +634,7 @@ pub fn bus_interactions() -> Vec { // Spec emits 40 `IS_BYTE` templates; we merge adjacent // byte pairs (z=2i, z=2i+1) into ARE_BYTES interactions per the // implementation guidance in spec/is_byte.typ. - // Cxz_right uses IS_BIT polynomial constraints (see create_constraints). + // Cxz_right uses IS_BIT polynomial constraints (see `KeccakRndConstraints`). for x in 0..5 { for i in 0..4 { interactions.push(BusInteraction::sender( @@ -900,9 +900,8 @@ pub fn bus_interactions() -> Vec { // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// The KECCAK round table's transition constraints as a single -/// [`ConstraintSet`], mirroring `create_constraints` index-for-index (20 -/// constraints): for `x ∈ 0..5`, `hw ∈ 0..4` (idx `x·4 + hw`), the μ-gated +/// The KECCAK round table's 20 transition constraints as a single +/// [`ConstraintSet`]: for `x ∈ 0..5`, `hw ∈ 0..4` (idx `x·4 + hw`), the μ-gated /// `IS_BIT` on `Cxz_right[x][hw]` — `μ · Cxz_right·(1 − Cxz_right)`. pub struct KeccakRndConstraints; diff --git a/prover/src/tables/load.rs b/prover/src/tables/load.rs index c992b2050..7b2f8e833 100644 --- a/prover/src/tables/load.rs +++ b/prover/src/tables/load.rs @@ -475,11 +475,8 @@ pub fn bus_interactions() -> Vec { // Single-body constraint set (ConstraintSet front-end) // ========================================================================= // -// Non-destructive twin of `LoadConstraint` / `constraints()` above, written -// once against the generic `ConstraintBuilder` so one body serves the compiled -// prover folder, the verifier folder and IR capture. The old structs stay for -// now (they are the differential oracle); the final deletion phase removes -// them. Constraint indices 0..13 match `constraints()` (idx_start = 0) exactly: +// One body against the generic `ConstraintBuilder` serves the compiled prover +// folder, the verifier folder and IR capture. Constraint indices 0..13: // 0..4: FlagIsBit(SIGNED, READ2, READ4, READ8) 4: WidthSumIsBit // 5: ReadImpliesMu 6..10: ExtensionHigh(4..8) // 10..12: ExtensionMid(2..4) 12: ExtensionLow diff --git a/prover/src/tables/memw.rs b/prover/src/tables/memw.rs index 722d5ef85..30e27fb9a 100644 --- a/prover/src/tables/memw.rs +++ b/prover/src/tables/memw.rs @@ -27,7 +27,8 @@ //! - 16 Memory bus tokens (read old + write new, per byte) //! - 2 MEMW output interactions (read + write, from CPU) //! -//! ## Constraints (11 total: 2 custom + 2 IS_BIT for multiplicities + 7 IS_BIT for carry) +//! ## Constraints (15 total: 2 custom + 2 IS_BIT for multiplicities + 7 IS_BIT +//! for carry + 3 IS_BIT for width flags (write2/4/8) + 1 IS_BIT for the width sum) use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; @@ -849,8 +850,7 @@ fn w2_expr>(b: &B) -> b.main(0, cols::WRITE2) + b.main(0, cols::WRITE4) + b.main(0, cols::WRITE8) } -/// The MEMW table's transition constraints as a single [`ConstraintSet`], -/// mirroring `constraints` index-for-index (15 constraints): +/// The MEMW table's 15 transition constraints as a single [`ConstraintSet`]: /// - idx 0: `IS_BIT<μ_sum>`; /// - idx 1: `w2 ⇒ μ_sum` (`w2·(1 − μ_sum)`); /// - idx 2,3: `IS_BIT` on `μ_read`, `μ_write`; diff --git a/prover/src/tables/memw_aligned.rs b/prover/src/tables/memw_aligned.rs index acc3651ae..ec8b8832c 100644 --- a/prover/src/tables/memw_aligned.rs +++ b/prover/src/tables/memw_aligned.rs @@ -24,11 +24,13 @@ //! - 16 Memory bus tokens //! - 2 MEMW output interactions (read + write) //! -//! ## Constraints (4 total) +//! ## Constraints (8 total) //! - IS_BIT<μ_sum> (1) //! - w2 => μ_sum (1) //! - IS_BIT<μ_read> (1) //! - IS_BIT<μ_write> (1) +//! - IS_BIT, IS_BIT, IS_BIT (3) +//! - IS_BIT (width sum is a bit) (1) //! //! ## Assumptions (caller's responsibility, not enforced here) //! - IS_HALF[base_address[i]] for i ∈ [0, 1] @@ -649,8 +651,7 @@ pub fn bus_interactions() -> Vec { // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// `μ_sum = μ_read + μ_write` as a builder expression (twin of the inlined -/// `compute`). +/// `μ_sum = μ_read + μ_write` as a builder expression. fn mu_sum_expr>(b: &B) -> B::Expr { b.main(0, cols::MU_READ) + b.main(0, cols::MU_WRITE) } @@ -660,8 +661,7 @@ fn w2_expr>(b: &B) -> b.main(0, cols::WRITE2) + b.main(0, cols::WRITE4) + b.main(0, cols::WRITE8) } -/// The MEMW_A table's transition constraints as a single [`ConstraintSet`], -/// mirroring `constraints` index-for-index (8 constraints): +/// The MEMW_A table's 8 transition constraints as a single [`ConstraintSet`]: /// - idx 0: `IS_BIT<μ_sum>`; /// - idx 1: `w2 ⇒ μ_sum` (`w2·(1 − μ_sum)`); /// - idx 2,3: `IS_BIT` on `μ_read`, `μ_write`; diff --git a/prover/src/tables/memw_register.rs b/prover/src/tables/memw_register.rs index fd6ad0ca3..5e2e8ba76 100644 --- a/prover/src/tables/memw_register.rs +++ b/prover/src/tables/memw_register.rs @@ -361,8 +361,7 @@ pub fn bus_interactions() -> Vec { // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// The MEMW_R table's transition constraints as a single [`ConstraintSet`], -/// mirroring `constraints` index-for-index (3 constraints): +/// The MEMW_R table's 3 transition constraints as a single [`ConstraintSet`]: /// - idx 0,1: `IS_BIT` on `μ_read`, `μ_write`; /// - idx 2: `IS_BIT<μ_sum>` with `μ_sum = μ_read + μ_write`. pub struct MemwRegisterConstraints; diff --git a/prover/src/tables/mul.rs b/prover/src/tables/mul.rs index 9a0b0e26d..08f1c749c 100644 --- a/prover/src/tables/mul.rs +++ b/prover/src/tables/mul.rs @@ -453,7 +453,7 @@ pub fn bus_interactions() -> Vec { // ------------------------------------------------------------------------- // IS_B20 lookups for carry range checks (multiplicity: mu_lo + mu_hi) - // Carries are virtual columns computed as linear combinations: + // Carries are virtual (computed inline) as linear combinations: // carry[0] = 2^-32 * (raw_product[0] - res[0]) // carry[i] = 2^-32 * (raw_product[i] + carry[i-1] - res[i]) // where res = [lo_word0, lo_word1, hi_word0, hi_word1] @@ -683,11 +683,8 @@ pub fn bus_interactions() -> Vec { // Single-body constraint set (ConstraintSet front-end) // ========================================================================= // -// Non-destructive twin of `MulConstraint` / `mul_constraints` above, written -// once against the generic `ConstraintBuilder` so one body serves the compiled -// prover folder, the verifier folder and IR capture. The old structs stay for -// now (they are the differential oracle); the final deletion phase removes -// them. Constraint indices 0..8 match `mul_constraints(0)` exactly: +// One body against the generic `ConstraintBuilder` serves the compiled prover +// folder, the verifier folder and IR capture. Constraint indices 0..8: // 0: SignedIsBit(LHS_SIGNED) 1: SignedIsBit(RHS_SIGNED) // 2: LhsSign 3: RhsSign // 4..8: RawProduct(0..4) @@ -710,7 +707,6 @@ impl MulConstraints { } /// `raw_product[i] − Σ_k 2^(16k)·Σ_j lhs_ext[j]·rhs_ext[idx−j]` (idx = 2i+k). - /// Mirrors [`MulConstraint::compute_raw_product_constraint`] exactly. fn raw_product>( b: &B, i: usize, @@ -758,7 +754,7 @@ impl MulConstraints { rhs_hi, ]; - // Convolution sum (same k/j bounds as the old code). + // Convolution sum. let shift_16 = b.const_base(SHIFT_16); let mut sum = b.zero(); for k in 0..=1usize { diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index d66e48ab5..202b67033 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -6,16 +6,17 @@ //! 1. Intra-limb shift by `bit_shift = shift mod 16` using paired HWSL lookups (returning [SLL, SLLC]). //! 2. Full-limb shift by `limb_shift` (unary encoding of `shift >> 4`). //! -//! ## Columns (26 total) +//! ## Columns (29 total) //! - Input: `in[0..3]` (DWordHL), `shift` (Byte), `direction` (Bit), `signed` (Bit), `word_instr` (Bit) //! - Output: `out[0..1]` (DWordWL) //! - Auxiliary: `is_negative`, `bit_shift`, `zbs`, `X[0..4]`, `Y[0..3]`, `limb_shift_raw[0..2]` //! - Virtual: `limb_shift[3] = 1 - limb_shift_raw[0] - limb_shift_raw[1] - limb_shift_raw[2]` +//! - Shift decomposition (ALU-bus shift amount): `shift_b1` (idx 26, Byte = shift[1]), `shift_h1` (idx 27, Half = shift[2]), `shift_high` (idx 28, Word = shift[3]) //! - Multiplicity: `μ` //! -//! ## Bus Interactions (15 total) -//! - Senders: MSB16, BYTE_ALU[AND] (×3), ZERO, HWSL (×5), IS_HALFWORD (×4) -//! - Receiver: SHIFT (from CPU) +//! ## Bus Interactions (18 total) +//! - Senders: MSB16, BYTE_ALU[AND] (×3), ZERO, HWSL (×5), ARE_BYTES (×2), IS_HALFWORD (×5) +//! - Receiver: ALU (from CPU) use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; @@ -218,7 +219,7 @@ impl ShiftOperation { // AIR constrains IS_NEGATIVE via the MSB16 bus (SHIFT-C14) only when // `signed = 1` — for `signed = 0` IS_NEGATIVE is free, so we set it // to zero. This makes `extension = 65535 * is_negative = 0` for SRL, - // so the extension contribution in `compute_shifted_half` naturally + // so the extension contribution in `shifted_half` naturally // vanishes (zero fill) — matching RISC-V SRL semantics regardless of // the top-bit value of the input. let is_negative = self.signed && (self.in_halves[3] >> 15) & 1 == 1; @@ -731,11 +732,8 @@ pub const NUM_SHIFT_CONSTRAINTS: usize = 19; // Single-body constraint set (ConstraintSet front-end) // ========================================================================= // -// Non-destructive twin of `ShiftConstraint` / `shift_constraints` above, -// written once against the generic `ConstraintBuilder` so one body serves the -// compiled prover folder, the verifier folder and IR capture. The old structs -// stay for now (they are the differential oracle); the final deletion phase -// removes them. Constraint indices 0..19 match `shift_constraints(0)` exactly. +// One body against the generic `ConstraintBuilder` serves the compiled prover +// folder, the verifier folder and IR capture. Constraint indices 0..19. use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; @@ -745,7 +743,7 @@ pub struct ShiftConstraints; impl ShiftConstraints { /// `limb_shift[i]` (i = 0..2 raw, i = 3 virtual - /// `1 - ls_raw[0] - ls_raw[1] - ls_raw[2]`), matching `get_ls`. + /// `1 - ls_raw[0] - ls_raw[1] - ls_raw[2]`). fn limb_shift>( b: &B, i: usize, @@ -785,8 +783,7 @@ impl ShiftConstraints { y + x } - /// The `shifted` virtual column at index `half_idx` (0..4), matching - /// [`ShiftConstraint::compute_shifted_half`]. + /// The `shifted` virtual column at index `half_idx` (0..4). fn shifted_half>( b: &B, i: usize, diff --git a/prover/src/tables/store.rs b/prover/src/tables/store.rs index 93ba98332..044f97662 100644 --- a/prover/src/tables/store.rs +++ b/prover/src/tables/store.rs @@ -254,8 +254,7 @@ pub fn bus_interactions() -> Vec { // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// The STORE table's transition constraints as a single [`ConstraintSet`], -/// mirroring `store_constraints` index-for-index: +/// The STORE table's transition constraints as a single [`ConstraintSet`]: /// - idx 0-3: `IS_BIT` on `write2`, `write4`, `write8`, `μ` (unconditional); /// - idx 4: `(Σ width)·(1 − Σ width) = 0` (width sum is a bit); /// - idx 5: `(Σ width)·(1 − μ) = 0` (width ⇒ μ). From b26050e25eb0ff0cc61e9dcd0a1718349a00acc0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 18:31:35 -0300 Subject: [PATCH 48/52] stark: drop the dead packing_shifts context field and the complete flag Both are write-only vestiges the design review flagged: - TransitionEvaluationContext::packing_shifts was constructed and passed at five sites and read at none -- the single-source bodies lower the packing shift constants through const_base, so the folders never touch it. The field, both constructor params, and three per-prove PackingShifts::new() constructions go away. The PackingShifts type stays: the aux-trace build path genuinely uses it. - ConstraintProgram::complete / IrBuilder::mark_unsupported encoded a fall-back-to-boxed-path protocol for partially captured AIRs; the boxed path no longer exists and every AIR captures fully, so the flag was always true and read only by tests asserting it's true. Also adds fail-loud asserts on the capture-time u8/u16 narrowing (offsets, columns, challenge/alpha indices) in IrBuilder and CaptureBuilder: capture runs once at setup, and a table wider than the IR encoding must panic rather than silently truncate into the GPU program. --- crypto/stark/src/constraint_ir/builder.rs | 30 ++++---- crypto/stark/src/constraint_ir/ir.rs | 5 -- crypto/stark/src/constraint_ir/tests.rs | 70 ++----------------- crypto/stark/src/constraints/builder.rs | 9 ++- crypto/stark/src/constraints/builder_tests.rs | 29 +------- crypto/stark/src/constraints/evaluator.rs | 6 +- crypto/stark/src/debug.rs | 4 +- crypto/stark/src/traits.rs | 26 +++---- crypto/stark/src/verifier.rs | 4 +- 9 files changed, 44 insertions(+), 139 deletions(-) diff --git a/crypto/stark/src/constraint_ir/builder.rs b/crypto/stark/src/constraint_ir/builder.rs index ff1365e96..82512e974 100644 --- a/crypto/stark/src/constraint_ir/builder.rs +++ b/crypto/stark/src/constraint_ir/builder.rs @@ -54,10 +54,6 @@ pub struct IrBuilder>, ext_consts: Vec>, roots: Vec, - /// Set by [`Self::mark_unsupported`] when a constraint couldn't be - /// captured. Propagated to [`ConstraintProgram::complete`] so callers know - /// not to interpret an incomplete program. - complete: bool, } impl Default for IrBuilder { @@ -76,7 +72,6 @@ impl IrBuilder { base_consts: Vec::new(), ext_consts: Vec::new(), roots: Vec::new(), - complete: true, }; // Reserve id 0 = ConstBase(0) = base-field zero. `const_base(0)` will // dedup to this. @@ -85,14 +80,6 @@ impl IrBuilder { b } - /// Record that the constraint currently being captured has no capture - /// implementation. Does not panic and does not emit a root for it — the - /// resulting program is marked incomplete (see - /// [`ConstraintProgram::complete`]) so callers know not to interpret it. - pub fn mark_unsupported(&mut self) { - self.complete = false; - } - /// Append (or reuse) a node with the given op and result dimension. fn push(&mut self, op: Op, dim: Dim) -> Expr { if let Some(&id) = self.cse.get(&(op, dim)) { @@ -111,6 +98,10 @@ impl IrBuilder { /// A main-trace column read at the given frame `offset`, row 0. pub fn main(&mut self, offset: u8, col: usize) -> Expr { + assert!( + u16::try_from(col).is_ok(), + "column {col} exceeds the IR's u16 index" + ); self.push( Op::Var { main: true, @@ -125,6 +116,10 @@ impl IrBuilder { /// An aux-trace column read at the given frame `offset`, row 0 /// ([`Dim::Ext`]). pub fn aux(&mut self, offset: u8, col: usize) -> Expr { + assert!( + u16::try_from(col).is_ok(), + "column {col} exceeds the IR's u16 index" + ); self.push( Op::Var { main: false, @@ -138,11 +133,19 @@ impl IrBuilder { /// A LogUp RAP challenge, uniform per proof ([`Dim::Ext`]). pub fn challenge(&mut self, idx: usize) -> Expr { + assert!( + u16::try_from(idx).is_ok(), + "challenge index {idx} exceeds the IR's u16 index" + ); self.push(Op::RapChallenge { idx: idx as u16 }, Dim::Ext) } /// A precomputed LogUp alpha power, uniform per proof ([`Dim::Ext`]). pub fn alpha_power(&mut self, idx: usize) -> Expr { + assert!( + u16::try_from(idx).is_ok(), + "alpha index {idx} exceeds the IR's u16 index" + ); self.push(Op::AlphaPow { idx: idx as u16 }, Dim::Ext) } @@ -271,7 +274,6 @@ impl IrBuilder { ext_consts: self.ext_consts, roots: self.roots, num_base, - complete: self.complete, } } } diff --git a/crypto/stark/src/constraint_ir/ir.rs b/crypto/stark/src/constraint_ir/ir.rs index ba4c2c935..cc770fd06 100644 --- a/crypto/stark/src/constraint_ir/ir.rs +++ b/crypto/stark/src/constraint_ir/ir.rs @@ -99,11 +99,6 @@ pub struct ConstraintProgram ConstraintProgram { diff --git a/crypto/stark/src/constraint_ir/tests.rs b/crypto/stark/src/constraint_ir/tests.rs index bbb52fa6b..4950bfc31 100644 --- a/crypto/stark/src/constraint_ir/tests.rs +++ b/crypto/stark/src/constraint_ir/tests.rs @@ -12,7 +12,6 @@ use super::builder::IrBuilder; use super::interp::{eval_program, eval_program_base, eval_program_verifier}; use super::ir::{ConstraintProgram, Dim, Op}; use crate::frame::Frame; -use crate::lookup::PackingShifts; use crate::table::TableView; use crate::traits::TransitionEvaluationContext; @@ -199,14 +198,7 @@ fn frame_offset_reads_next_step() { let rap: Vec = vec![]; let alpha: Vec = vec![]; let offset = ExtE::zero(); - let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &rap, - &alpha, - &offset, - &shifts, - ); + let ctx = TransitionEvaluationContext::new_prover(frame.as_row_frame(), &rap, &alpha, &offset); let mut base_evals = vec![FpE::zero()]; let mut ext_evals: Vec = vec![]; @@ -239,14 +231,7 @@ fn mixed_base_ext_auto_embeds() { let rap: Vec = vec![]; let alpha: Vec = vec![]; let offset = ExtE::zero(); - let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &rap, - &alpha, - &offset, - &shifts, - ); + let ctx = TransitionEvaluationContext::new_prover(frame.as_row_frame(), &rap, &alpha, &offset); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; @@ -278,14 +263,7 @@ fn explicit_embed_and_ext_neg() { let rap: Vec = vec![]; let alpha: Vec = vec![]; let offset = ExtE::zero(); - let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &rap, - &alpha, - &offset, - &shifts, - ); + let ctx = TransitionEvaluationContext::new_prover(frame.as_row_frame(), &rap, &alpha, &offset); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero()]; @@ -334,14 +312,7 @@ fn all_leaf_kinds_logup_shaped() { let step = TableView::::new(vec![main_row], vec![aux_row]); let frame = Frame::::new(vec![step]); - let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &rap, - &alpha, - &offset, - &shifts, - ); + let ctx = TransitionEvaluationContext::new_prover(frame.as_row_frame(), &rap, &alpha, &offset); let mut base_evals: Vec = vec![]; let mut ext_evals = vec![ExtE::zero()]; @@ -377,14 +348,7 @@ fn prover_entry_point_splits_base_and_ext() { let rap: Vec = vec![]; let alpha = vec![ext3(3, 3, 3)]; let offset = ExtE::zero(); - let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &rap, - &alpha, - &offset, - &shifts, - ); + let ctx = TransitionEvaluationContext::new_prover(frame.as_row_frame(), &rap, &alpha, &offset); let mut base_evals = vec![FpE::zero()]; let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; @@ -408,10 +372,7 @@ fn verifier_entry_point_promotes_base_roots() { let rap: Vec = vec![]; let alpha = vec![ext3(3, 3, 3)]; let offset = ExtE::zero(); - let shifts = PackingShifts::::new(); - let ctx = TransitionEvaluationContext::::new_verifier( - &frame, &rap, &alpha, &offset, &shifts, - ); + let ctx = TransitionEvaluationContext::::new_verifier(&frame, &rap, &alpha, &offset); let mut ext_evals = vec![ExtE::zero(), ExtE::zero()]; eval_program_verifier(&prog, &ctx, &mut ext_evals); @@ -445,20 +406,6 @@ fn roots_indexed_by_constraint_idx_any_order() { assert_eq!(eval_program_base(&prog, 2, &row), fp(4) * fp(4)); } -// ------------------------------------------------------------------------ -// complete flag plumbing. -// ------------------------------------------------------------------------ - -#[test] -fn complete_flag_defaults_true_and_mark_unsupported_clears_it() { - let b = IrBuilder::::new(); - assert!(b.finish(0).complete); - - let mut b = IrBuilder::::new(); - b.mark_unsupported(); - assert!(!b.finish(0).complete); -} - // ------------------------------------------------------------------------ // Non-Goldilocks tower: E = F over the Baby-Bear-prime U32 test field. // Exercises the reflexive IsSubFieldOf impl and proves the module is @@ -514,13 +461,11 @@ fn non_goldilocks_reflexive_tower_builds_and_interprets() { let rap: Vec = vec![]; let alpha: Vec = vec![]; let offset = g(0); - let shifts = PackingShifts::::new(); let ctx = TransitionEvaluationContext::::new_prover( frame.as_row_frame(), &rap, &alpha, &offset, - &shifts, ); let mut base_evals = vec![GE::zero()]; let mut ext_evals = vec![GE::zero(), GE::zero()]; @@ -529,8 +474,7 @@ fn non_goldilocks_reflexive_tower_builds_and_interprets() { assert_eq!(ext_evals[1], g(4) + g(10)); // Verifier entry point too (the frame is Frame either way here). - let vctx = - TransitionEvaluationContext::::new_verifier(&frame, &rap, &alpha, &offset, &shifts); + let vctx = TransitionEvaluationContext::::new_verifier(&frame, &rap, &alpha, &offset); let mut v_evals = vec![GE::zero(), GE::zero()]; eval_program_verifier(&prog, &vctx, &mut v_evals); assert_eq!(v_evals[0], g(6) * g(7) + g(3)); diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index 9b02b40da..8c0cecf36 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -282,8 +282,7 @@ pub fn run_transition_prover( /// A Verifier context runs the [`VerifierEvalFolder`] (the OOD/recursion /// path). A Prover context is also accepted — debug trace validation calls /// this method with a prover frame — by running the [`ProverEvalFolder`] -/// and promoting the Base-prefix results, mirroring the old boxed path's -/// `evaluate_verifier` promotion. +/// and promoting the Base-prefix results into the extension. pub fn run_transition_verifier( cs: &CS, ctx: &TransitionEvaluationContext<'_, F, E>, @@ -768,6 +767,9 @@ impl ConstraintBuilder for CaptureBuilder { type ExprE = IrExpr; fn main(&self, offset: usize, col: usize) -> IrExpr { + // Capture runs once at setup — assert the narrow IR encodings fit + // rather than silently truncating into the GPU program. + assert!(u8::try_from(offset).is_ok() && u16::try_from(col).is_ok()); IrExpr::leaf( TreeKind::Main { offset: offset as u8, @@ -778,6 +780,7 @@ impl ConstraintBuilder for CaptureBuilder { ) } fn aux(&self, offset: usize, col: usize) -> IrExpr { + assert!(u8::try_from(offset).is_ok() && u16::try_from(col).is_ok()); IrExpr::leaf( TreeKind::Aux { offset: offset as u8, @@ -788,9 +791,11 @@ impl ConstraintBuilder for CaptureBuilder { ) } fn challenge(&self, idx: usize) -> IrExpr { + assert!(u16::try_from(idx).is_ok()); IrExpr::leaf(TreeKind::Challenge(idx as u16), Dim::Ext, 0) } fn alpha_pow(&self, idx: usize) -> IrExpr { + assert!(u16::try_from(idx).is_ok()); IrExpr::leaf(TreeKind::AlphaPow(idx as u16), Dim::Ext, 0) } fn table_offset(&self) -> IrExpr { diff --git a/crypto/stark/src/constraints/builder_tests.rs b/crypto/stark/src/constraints/builder_tests.rs index 7e2b30970..61b494cf2 100644 --- a/crypto/stark/src/constraints/builder_tests.rs +++ b/crypto/stark/src/constraints/builder_tests.rs @@ -21,7 +21,6 @@ use crate::constraints::builder::{ VerifierEvalFolder, num_base_from_meta, }; use crate::frame::Frame; -use crate::lookup::PackingShifts; use crate::table::TableView; use crate::traits::TransitionEvaluationContext; @@ -179,7 +178,6 @@ fn random_trial(rng: &mut SplitMix64) -> TrialData { #[test] fn prover_folder_matches_direct_arithmetic() { - let shifts = PackingShifts::::new(); let mut rng = SplitMix64(0x0001_F01D_u64 ^ 0xABCD); for trial in 0..TRIALS { let t = random_trial(&mut rng); @@ -192,7 +190,6 @@ fn prover_folder_matches_direct_arithmetic() { &challenges, &alphas, &t.offset, - &shifts, ); let mut base_out = vec![FpE::zero(); NUM_BASE]; @@ -216,8 +213,6 @@ fn prover_folder_matches_interpreted_capture() { let mut cb = CaptureBuilder::::new(); SampleSet.eval(&mut cb); let (prog, _degrees) = cb.finish(NUM_BASE); - - let shifts = PackingShifts::::new(); let mut rng = SplitMix64(0x0002_F01D_u64 ^ 0xABCD); for trial in 0..TRIALS { let t = random_trial(&mut rng); @@ -230,7 +225,6 @@ fn prover_folder_matches_interpreted_capture() { &challenges, &alphas, &t.offset, - &shifts, ); let mut folder_base = vec![FpE::zero(); NUM_BASE]; @@ -253,8 +247,6 @@ fn verifier_folder_matches_interpreted_capture() { let mut cb = CaptureBuilder::::new(); SampleSet.eval(&mut cb); let (prog, _degrees) = cb.finish(NUM_BASE); - - let shifts = PackingShifts::::new(); let mut rng = SplitMix64(0x0003_F01D_u64 ^ 0xABCD); for trial in 0..TRIALS { let t = random_trial(&mut rng); @@ -269,7 +261,6 @@ fn verifier_folder_matches_interpreted_capture() { &challenges, &alphas, &t.offset, - &shifts, ); let mut folder_ext = vec![ExtE::zero(); NUM_CONSTRAINTS]; @@ -293,7 +284,6 @@ fn capture_measured_degrees_match_declared_meta() { let mut cb = CaptureBuilder::::new(); SampleSet.eval(&mut cb); let (prog, degrees) = cb.finish(NUM_BASE); - assert!(prog.complete); assert_eq!(prog.roots.len(), NUM_CONSTRAINTS); let meta = SampleSet.meta(); @@ -354,7 +344,6 @@ fn meta_non_dense_panics() { #[test] #[should_panic(expected = "never emitted")] fn prover_folder_missing_emit_asserts() { - let shifts = PackingShifts::::new(); let step = TableView::::new(vec![vec![FpE::zero(); cols::NUM_COLS]], vec![vec![]]); let frame = Frame::::new(vec![step]); let challenges: Vec = vec![]; @@ -365,7 +354,6 @@ fn prover_folder_missing_emit_asserts() { &challenges, &alphas, &offset, - &shifts, ); let mut base_out = vec![FpE::zero(); 2]; @@ -380,7 +368,6 @@ fn prover_folder_missing_emit_asserts() { #[test] #[should_panic(expected = "emitted twice")] fn prover_folder_double_emit_asserts() { - let shifts = PackingShifts::::new(); let step = TableView::::new(vec![vec![FpE::zero(); cols::NUM_COLS]], vec![vec![]]); let frame = Frame::::new(vec![step]); let challenges: Vec = vec![]; @@ -391,7 +378,6 @@ fn prover_folder_double_emit_asserts() { &challenges, &alphas, &offset, - &shifts, ); let mut base_out = vec![FpE::zero(); 2]; @@ -407,19 +393,13 @@ fn prover_folder_double_emit_asserts() { #[test] #[should_panic(expected = "never emitted")] fn verifier_folder_missing_emit_asserts() { - let shifts = PackingShifts::::new(); let step = TableView::::new(vec![vec![ExtE::zero(); cols::NUM_COLS]], vec![vec![]]); let frame = Frame::::new(vec![step]); let challenges: Vec = vec![]; let alphas: Vec = vec![]; let offset = ExtE::zero(); - let ctx = TransitionEvaluationContext::::new_verifier( - &frame, - &challenges, - &alphas, - &offset, - &shifts, - ); + let ctx = + TransitionEvaluationContext::::new_verifier(&frame, &challenges, &alphas, &offset); let mut ext_out = vec![ExtE::zero(); 2]; let mut folder = VerifierEvalFolder::new(&ctx, &mut ext_out); @@ -583,9 +563,6 @@ fn next_row_aux_and_multi_alpha_folder_matches_capture() { for &(idx, measured) in °rees { assert_eq!(measured, meta[idx].degree, "constraint {idx} degree"); } - - let shifts = PackingShifts::::new(); - let vshifts = PackingShifts::::new(); let mut rng = SplitMix64(0x0004_F01D_u64 ^ 0xABCD); for trial in 0..TRIALS { // Two frame steps with distinct main and aux rows. @@ -609,7 +586,6 @@ fn next_row_aux_and_multi_alpha_folder_matches_capture() { &challenges, &alphas, &offset, - &shifts, ); let mut folder_base = vec![FpE::zero(); num_base]; @@ -647,7 +623,6 @@ fn next_row_aux_and_multi_alpha_folder_matches_capture() { &challenges, &alphas, &offset, - &vshifts, ); let mut vfolder_ext = vec![ExtE::zero(); meta.len()]; diff --git a/crypto/stark/src/constraints/evaluator.rs b/crypto/stark/src/constraints/evaluator.rs index 1c84f22ea..48434b6e1 100644 --- a/crypto/stark/src/constraints/evaluator.rs +++ b/crypto/stark/src/constraints/evaluator.rs @@ -1,7 +1,7 @@ use super::boundary::BoundaryConstraints; use crate::domain::Domain; use crate::frame::RowFrame; -use crate::lookup::{BusPublicInputs, LOGUP_CHALLENGE_ALPHA, PackingShifts, compute_alpha_powers}; +use crate::lookup::{BusPublicInputs, LOGUP_CHALLENGE_ALPHA, compute_alpha_powers}; use crate::trace::LDETraceTable; use crate::traits::{AIR, TransitionEvaluationContext, ZerofierEvaluations}; use math::field::element::FieldElement; @@ -58,9 +58,6 @@ where Vec::new() }; - // Precompute packing shift constants once for all LDE domain points. - let packing_shifts = PackingShifts::::new(); - // Per-thread output buffers via map_init: each Rayon worker allocates // once, then reuses for all iterations assigned to that thread. The // trace rows themselves are BORROWED in place per LDE point (the LDE @@ -79,7 +76,6 @@ where rap_challenges, &logup_alpha_powers, logup_table_offset, - &packing_shifts, ); air.compute_transition_prover(&ctx, base_buf, transition_buf); diff --git a/crypto/stark/src/debug.rs b/crypto/stark/src/debug.rs index d2f049190..24a4fba23 100644 --- a/crypto/stark/src/debug.rs +++ b/crypto/stark/src/debug.rs @@ -2,7 +2,7 @@ use super::domain::Domain; use super::lookup::BusPublicInputs; use super::trace::TraceTable; use super::traits::{AIR, TransitionEvaluationContext}; -use crate::lookup::{LOGUP_CHALLENGE_ALPHA, PackingShifts, compute_alpha_powers}; +use crate::lookup::{LOGUP_CHALLENGE_ALPHA, compute_alpha_powers}; use crate::{frame::Frame, trace::LDETraceTable}; use log::{error, info}; use math::field::traits::IsSubFieldOf; @@ -100,7 +100,6 @@ pub fn validate_trace< }; // Iterate over trace and compute transitions - let packing_shifts = PackingShifts::::new(); for step in 0..lde_trace.num_steps() { let frame = Frame::read_step_from_lde(&lde_trace, step, &air.context().transition_offsets); let transition_evaluation_context = TransitionEvaluationContext::new_prover( @@ -108,7 +107,6 @@ pub fn validate_trace< rap_challenges, &logup_alpha_powers, &logup_table_offset, - &packing_shifts, ); let evaluations = air.compute_transition(&transition_evaluation_context); diff --git a/crypto/stark/src/traits.rs b/crypto/stark/src/traits.rs index 747290cdb..c28f831a2 100644 --- a/crypto/stark/src/traits.rs +++ b/crypto/stark/src/traits.rs @@ -7,10 +7,8 @@ use math::field::{ }; use crate::{ - constraint_ir::ConstraintProgram, - constraints::builder::ConstraintMeta, - domain::Domain, - lookup::{BusPublicInputs, PackingShifts}, + constraint_ir::ConstraintProgram, constraints::builder::ConstraintMeta, domain::Domain, + lookup::BusPublicInputs, }; use super::{ @@ -77,14 +75,12 @@ where rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, - packing_shifts: &'a PackingShifts, }, Verifier { frame: &'a Frame, rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, - packing_shifts: &'a PackingShifts, }, } @@ -98,14 +94,12 @@ where rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, - packing_shifts: &'a PackingShifts, ) -> Self { Self::Prover { rows, rap_challenges, logup_alpha_powers, logup_table_offset, - packing_shifts, } } @@ -114,14 +108,12 @@ where rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, - packing_shifts: &'a PackingShifts, ) -> Self { Self::Verifier { frame, rap_challenges, logup_alpha_powers, logup_table_offset, - packing_shifts, } } } @@ -208,10 +200,11 @@ pub trait AIR: Send + Sync { fn composition_poly_degree_bound(&self, trace_length: usize) -> usize; - /// The method called by the prover to evaluate the transitions corresponding to an evaluation frame. - /// In the case of the prover, the main evaluation table of the frame takes values in - /// `Self::Field`, since they are the evaluations of the main trace at the LDE domain. - /// In the case of the verifier, the frame take elements of Self::FieldExtension. + /// Evaluates the transitions corresponding to an evaluation frame at the + /// out-of-domain point. The verifier and the debug trace validation call + /// this; the prover instead uses `compute_transition_prover`. + /// In the verifier's case, the frame takes elements of `Self::FieldExtension`; + /// the debug validation path evaluates over the base `Self::Field` trace. /// /// Required: implemented via the single-source constraint body (the /// [`VerifierEvalFolder`](crate::constraints::builder::VerifierEvalFolder) @@ -252,9 +245,8 @@ pub trait AIR: Send + Sync { ); /// The idx-ordered metadata for every transition constraint (kind, declared - /// degree, zerofier shape) — plain data replacing the old per-constraint - /// trait objects. `RootKind::Base` entries form a prefix (its length is - /// `num_base_transition_constraints()`). + /// degree, zerofier shape), as plain data. `RootKind::Base` entries form a + /// prefix (its length is `num_base_transition_constraints()`). fn constraints_meta(&self) -> &[ConstraintMeta]; /// The lazily captured flat IR ([`ConstraintProgram`]) of every transition diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index ff0a7b024..3550aa6b0 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -9,7 +9,7 @@ use super::{ use crate::{ config::Commitment, domain::new_verifier_domain, - lookup::{LOGUP_CHALLENGE_ALPHA, LOGUP_NUM_CHALLENGES, PackingShifts, compute_alpha_powers}, + lookup::{LOGUP_CHALLENGE_ALPHA, LOGUP_NUM_CHALLENGES, compute_alpha_powers}, proof::stark::{DeepPolynomialOpening, MultiProof, PolynomialOpenings}, }; use crypto::{fiat_shamir::is_transcript::IsStarkTranscript, merkle_tree::proof::Proof}; @@ -190,13 +190,11 @@ pub trait IsStarkVerifier< let ood_frame = (proof.trace_ood_evaluations).into_frame(num_main_trace_columns, air.step_size()); - let packing_shifts = PackingShifts::::new(); let transition_evaluation_context = TransitionEvaluationContext::new_verifier( &ood_frame, &challenges.rap_challenges, &logup_alpha_powers, &logup_table_offset, - &packing_shifts, ); let transition_ood_frame_evaluations = air.compute_transition(&transition_evaluation_context); From daef533d1d3557852fdb1c093ae92098ff52f19e Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 18:31:47 -0300 Subject: [PATCH 49/52] tests: close the interpreter-path coverage gaps; release-safe exact-once checks The test-gap review found the capture->IR->interpret pipeline (the future GPU path; no production caller today) was verified only on shapes production never uses: no real table's combined base+LogUp program was ever interpreted, multi-committed-pair layouts and five of the seven Multiplicity variants never flowed through the interpreter. - constraint_program_tests: every production AIR's captured program (via the production constraint_program() entry point) interpreted and compared bit-for-bit against the compiled folders on random two-step frames, prover and verifier sides -- all 26 AIRs. - logup_two_committed_pairs: >= 2-pair layout fixture, exercising the batched-term loop and the accumulated term-column sum past their first iteration. - RowFrame::from_lde unit tests: per-offset row borrows, the cyclic wrap at the domain end, the offsets cap, and as_row_frame equivalence. The correctness review found the exact-once-emission invariant had no release-safe gate: EmitTracker is debug-only and CI runs tests --release, so a double-emit/skip-swap typo would ship a silently unenforced (always-zero) constraint. Every differential harness now asserts the emitted index set is exactly 0..n in any build profile, and the per-table test rejects roots left at the id-0 sentinel. --- crypto/stark/src/frame.rs | 72 +++++++- crypto/stark/src/lookup.rs | 75 +++++--- prover/src/tests/constraint_emit_tests.rs | 17 +- prover/src/tests/constraint_program_tests.rs | 183 +++++++++++++++++++ prover/src/tests/constraint_set_tests_a.rs | 17 +- prover/src/tests/constraint_set_tests_b.rs | 17 +- prover/src/tests/cpu32_tests.rs | 11 +- prover/src/tests/ec_scalar_tests.rs | 11 +- prover/src/tests/ecdas_tests.rs | 11 +- prover/src/tests/ecsm_tests.rs | 11 +- prover/src/tests/mod.rs | 2 + 11 files changed, 347 insertions(+), 80 deletions(-) create mode 100644 prover/src/tests/constraint_program_tests.rs diff --git a/crypto/stark/src/frame.rs b/crypto/stark/src/frame.rs index deaf4e5ed..5300be90d 100644 --- a/crypto/stark/src/frame.rs +++ b/crypto/stark/src/frame.rs @@ -32,8 +32,8 @@ impl, E: IsField> Copy for RowFrame<'_, F, E> {} impl<'a, F: IsSubFieldOf, E: IsField> RowFrame<'a, F, E> { /// Borrow the rows for LDE point `row` at each transition offset, - /// wrapping cyclically at the domain end (same row arithmetic as - /// [`Frame::fill_from_lde`] with single-row steps). + /// wrapping cyclically at the domain end (the same cyclic row arithmetic + /// the owned-Frame gather used, with single-row steps). pub fn from_lde(lde_trace: &'a LDETraceTable, row: usize, offsets: &[usize]) -> Self { debug_assert_eq!( lde_trace.lde_step_size, lde_trace.blowup_factor, @@ -174,3 +174,71 @@ impl, E: IsField> Frame { Self::read_from_lde(lde_trace, row, offsets) } } + +#[cfg(test)] +mod row_frame_tests { + use super::*; + use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext3; + use math::field::goldilocks::GoldilocksField as Gl; + + type Fp = FieldElement; + type Fp3 = FieldElement; + + /// An 8-row, 2-main/1-aux LDE table (blowup 2) with distinct per-cell + /// values, so any mis-indexed read is caught by value. + fn table() -> LDETraceTable { + let main: Vec> = (0..2) + .map(|c| (0..8).map(|r| Fp::from((100 * c + r) as u64)).collect()) + .collect(); + let aux: Vec> = vec![ + (0..8) + .map(|r| Fp3::new([Fp::from(1000 + r as u64), Fp::zero(), Fp::zero()])) + .collect(), + ]; + LDETraceTable::from_columns(main, aux, 1, 2) + } + + #[test] + fn borrows_rows_at_each_offset() { + let t = table(); + let rows = RowFrame::from_lde(&t, 3, &[0, 1]); + // offset 0 -> row 3; offset 1 -> row 3 + lde_step_size (= blowup 2) = 5. + assert_eq!(rows.main(0, 0), t.get_main(3, 0)); + assert_eq!(rows.main(0, 1), t.get_main(3, 1)); + assert_eq!(rows.main(1, 0), t.get_main(5, 0)); + assert_eq!(rows.aux(0, 0), t.get_aux(3, 0)); + assert_eq!(rows.aux(1, 0), t.get_aux(5, 0)); + assert_eq!(rows.num_offsets(), 2); + } + + #[test] + fn wraps_cyclically_at_the_domain_end() { + let t = table(); + // Last LDE row: offset 1 reads (7 + 2) % 8 = row 1. + let rows = RowFrame::from_lde(&t, 7, &[0, 1]); + assert_eq!(rows.main(0, 0), t.get_main(7, 0)); + assert_eq!(rows.main(1, 0), t.get_main(1, 0)); + assert_eq!(rows.aux(1, 0), t.get_aux(1, 0)); + } + + #[test] + #[should_panic(expected = "at most")] + fn rejects_too_many_offsets() { + let t = table(); + let _ = RowFrame::from_lde(&t, 0, &[0, 1, 2, 3, 4]); + } + + #[test] + fn as_row_frame_matches_owned_frame() { + let t = table(); + let frame = Frame::read_step_from_lde(&t, 2, &[0, 1]); + let rows = frame.as_row_frame(); + let direct = RowFrame::from_lde(&t, t.step_to_row(2), &[0, 1]); + for offset in 0..2 { + for col in 0..2 { + assert_eq!(rows.main(offset, col), direct.main(offset, col)); + } + assert_eq!(rows.aux(offset, 0), direct.aux(offset, 0)); + } + } +} diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index b31e20927..97ef28f6b 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -542,8 +542,9 @@ pub enum LinearTerm { /// A value that contributes to the bus fingerprint. /// -/// Each `BusValue` produces exactly **1 bus element** for the fingerprint. -/// The fingerprint is computed as: `z - (v₀ + α·v₁ + α²·v₂ + ...)` +/// A `BusValue` produces 1, 2, or 4 bus elements for the fingerprint depending +/// on its packing (see [`BusValue::num_bus_elements`]); `Linear` always +/// produces 1. The fingerprint is computed as: `z - (v₀ + α·v₁ + α²·v₂ + ...)` /// where each `vᵢ` is a bus element from a `BusValue`. #[derive(Debug, Clone)] pub enum BusValue { @@ -591,7 +592,8 @@ impl BusValue { BusValue::Linear(terms) } - /// Returns the number of bus elements this value produces (always 1). + /// Returns the number of bus elements this value produces: 1, 2, or 4 for + /// `Packed` depending on the packing, always 1 for `Linear`. pub fn num_bus_elements(&self) -> usize { match self { BusValue::Packed { packing, .. } => packing.num_bus_elements(), @@ -1729,11 +1731,12 @@ where // All LogUp constraints use the default zerofier shape (every row, no // exemptions), so [`logup_meta`] emits plain [`RootKind::Ext`] entries. // -// Honesty note (matches the runtime body): `BusValue::Linear`'s data-dependent -// "skip the multiply when the row value is zero" optimization is NOT reproduced -// here — the constraint body is row-agnostic and always emits the multiply. -// This is value-preserving (adding `0·α` is a no-op) and only costs a few extra -// base×ext muls per row. +// The data-dependent "skip the multiply when the row value is zero" +// optimization IS reproduced, through the [`ConstraintBuilder::fold_fingerprint_term`] +// hook rather than in this row-agnostic body: capture and the verifier fold the +// term unconditionally (value-identical, since `0·α = 0`), while +// `ProverEvalFolder` overrides the hook to skip the base×ext multiply for a +// zero bus element on the hot per-row path. use crate::constraints::builder::ConstraintBuilder; @@ -1963,8 +1966,8 @@ where ), BusValue::Linear(terms) => { // Routed through the builder so the prover folder can zero-skip - // the multiply, as the old runtime body did (Linear is where the - // constant-0 bus-width padding lives). Value-identical either way. + // the multiply (Linear is where the constant-0 bus-width padding + // lives). Value-identical either way. let result = emit_linear_terms(b, terms, offset); (b.fold_fingerprint_term(fp, result, alpha_offset), 1) } @@ -2117,11 +2120,10 @@ where } /// The idx-ordered [`ConstraintMeta`] for `layout`'s LogUp constraints, starting -/// at `idx_start`. Reproduces the boxed structs' answers exactly: batched terms -/// are degree 3; the accumulated constraint is degree `1 + absorbed.len()` -/// (2 for one absorbed, 3 for two). All are [`RootKind::Ext`] with the default -/// zerofier shape (period 1, offset 0, no exemptions) — the structs override -/// none of those. +/// at `idx_start`: batched terms are degree 3; the accumulated constraint is +/// degree `1 + absorbed.len()` (2 for one absorbed, 3 for two). All are +/// [`RootKind::Ext`] with the default zerofier shape (period 1, offset 0, no +/// exemptions). pub fn logup_meta(layout: &LogUpLayout, idx_start: usize) -> Vec { let mut meta = Vec::with_capacity(layout.num_constraints()); if layout.interactions.is_empty() { @@ -2167,7 +2169,7 @@ fn run_air_transition_prover( /// A Verifier context runs the [`VerifierEvalFolder`] (the OOD/recursion path). /// A Prover context is also accepted — debug trace validation calls this with a /// prover frame — by running the [`ProverEvalFolder`] and promoting the -/// base-prefix results, mirroring the old boxed path. +/// base-prefix results. fn run_air_transition_verifier( constraint_set: &CS, logup: &LogUpLayout, @@ -2268,9 +2270,7 @@ mod logup_single_source_tests { /// two-step frames: the LogUp body run three ways from ONE definition must /// agree bit-for-bit — [`ProverEvalFolder`] == capture→[`eval_program`] /// (prover) and [`VerifierEvalFolder`] == capture→[`eval_program_verifier`] - /// (verifier). (The old boxed-constraint oracle these were originally - /// differentiated against is gone; the folder-vs-interpreter equality it - /// established stays as the standing invariant.) + /// (verifier). fn check_layout(label: &str, layout: &LogUpLayout, num_main_cols: usize) { let n_base = 0usize; // LogUp constraints are all extension-rooted. let n = layout.num_constraints(); @@ -2296,8 +2296,16 @@ mod logup_single_source_tests { let mut cb = CaptureBuilder::::new(); emit_logup_constraints(&mut cb, layout, n_base); let (prog, degrees) = cb.finish(num_base); - assert!(prog.complete, "[{label}] capture must be complete"); assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); + // Release-safe exact-once check: the emitted indices must be exactly + // 0..n (the per-emit EmitTracker only exists under debug_assertions, + // which a --release test build compiles out). + let mut emitted: Vec = degrees.iter().map(|&(idx, _)| idx).collect(); + emitted.sort_unstable(); + assert!( + emitted.iter().enumerate().all(|(i, &idx)| i == idx), + "[{label}] emitted constraint indices are not exactly 0..{n}: {emitted:?}" + ); for &(idx, measured) in °rees { assert!( measured <= meta[idx].degree, @@ -2307,8 +2315,6 @@ mod logup_single_source_tests { } let n_aux = num_aux_cols(layout); - let shifts = PackingShifts::::new(); - let vshifts = PackingShifts::::new(); for trial in 0..TRIALS { let mut rng = SplitMix64::new(0xC0FF_EE00_u64 ^ (label.len() as u64) ^ trial as u64); @@ -2331,7 +2337,6 @@ mod logup_single_source_tests { &rap_challenges, &alpha_powers, &table_offset, - &shifts, ); // --- ProverEvalFolder == capture → interpret (prover) --- @@ -2370,7 +2375,6 @@ mod logup_single_source_tests { &rap_challenges, &alpha_powers, &table_offset, - &vshifts, ); // --- VerifierEvalFolder == capture → interpret (verifier) --- @@ -2497,6 +2501,29 @@ mod logup_single_source_tests { } } + #[test] + fn logup_two_committed_pairs() { + // >= 2 committed pairs: split(6) = (2 pairs, 2 absorbed). Exercises + // the batched-term loop past its first iteration (pair_idx*2 + // interaction indexing, per-pair term columns) and the accumulated + // constraint's committed-term sum over more than one aux column — + // the layout shape every production table has, which the fixtures + // above (<= 4 interactions, <= 1 pair) never reach. + let interactions = vec![ + direct_sender(3), + column_receiver(5), + direct_sender(7), + column_receiver(11), + direct_sender(13), + column_receiver(17), + ]; + let layout = LogUpLayout::from_interactions(interactions); + assert_eq!(layout.num_committed_pairs, 2, "must exercise >= 2 pairs"); + assert_eq!(layout.absorbed().len(), 2); + assert_eq!(layout.num_constraints(), 3); // 2 batched terms + accumulated + check_layout("two_committed_pairs", &layout, 8); + } + #[test] fn logup_linear_zero_skip() { // The prover folder zero-skips the F×E multiply for Linear bus diff --git a/prover/src/tests/constraint_emit_tests.rs b/prover/src/tests/constraint_emit_tests.rs index c9a6410ca..21e9e8fb4 100644 --- a/prover/src/tests/constraint_emit_tests.rs +++ b/prover/src/tests/constraint_emit_tests.rs @@ -16,7 +16,6 @@ use stark::constraints::builder::{ VerifierEvalFolder, num_base_from_meta, }; use stark::frame::Frame; -use stark::lookup::PackingShifts; use stark::table::TableView; use stark::traits::TransitionEvaluationContext; @@ -95,6 +94,16 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { body.eval(&mut cb); let (prog, degrees) = cb.finish(n); assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); + // Release-safe exact-once check: the emitted indices must be exactly + // 0..n. The per-emit EmitTracker only exists under debug_assertions, + // which CI's --release test build compiles out; this assert catches a + // double-emit/skip typo (count still == n) in any build profile. + let mut emitted: Vec = degrees.iter().map(|&(idx, _)| idx).collect(); + emitted.sort_unstable(); + assert!( + emitted.iter().enumerate().all(|(i, &idx)| i == idx), + "[{label}] emitted constraint indices are not exactly 0..{n}: {emitted:?}" + ); for &(idx, measured) in °rees { assert_eq!( measured, meta[idx].degree, @@ -102,9 +111,6 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { meta[idx].degree ); } - - let shifts = PackingShifts::::new(); - let vshifts = PackingShifts::::new(); let no_ch: Vec = vec![]; let offset_e = Fp3::zero(); @@ -120,7 +126,6 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { &no_ch, &no_ch, &offset_e, - &shifts, ); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; @@ -132,7 +137,7 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( - &frame_e, &no_ch, &no_ch, &offset_e, &vshifts, + &frame_e, &no_ch, &no_ch, &offset_e, ); let mut vext_out = vec![Fp3::zero(); n]; let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); diff --git a/prover/src/tests/constraint_program_tests.rs b/prover/src/tests/constraint_program_tests.rs new file mode 100644 index 000000000..5de95a446 --- /dev/null +++ b/prover/src/tests/constraint_program_tests.rs @@ -0,0 +1,183 @@ +//! Combined-program differential tests: every production table's CAPTURED +//! constraint program — the [`stark::traits::AIR::constraint_program`] the +//! GPU interpreter will consume — is interpreted and compared bit-for-bit +//! against the compiled folders on random off-trace frames. +//! +//! This is the interpreter-side counterpart of the folder coverage in +//! `constraint_set_tests_*` (base bodies only) and +//! `lookup::logup_single_source_tests` (synthetic LogUp layouts only): +//! here each table's REAL bus-interaction layout runs through capture with +//! its base constraints spliced ahead of the LogUp suffix, so multi-pair +//! layouts, every production `Multiplicity` variant, and `idx_base > 0` +//! LogUp emission are all exercised on the interpreter path — which has no +//! production caller until the GPU lands, and therefore no other safety net. +//! +//! The folders are the oracle: they are the production prove/verify path, +//! independently pinned by the prove→verify suites and cross-version +//! verification. + +use math::field::element::FieldElement; +use stark::constraint_ir::{eval_program, eval_program_verifier}; +use stark::frame::Frame; +use stark::proof::options::GoldilocksCubicProofOptions; +use stark::table::TableView; +use stark::traits::{AIR, TransitionEvaluationContext}; + +use crate::tables::types::{GoldilocksExtension, GoldilocksField}; +use crate::test_utils::*; + +type Gl = GoldilocksField; +type Ext3 = GoldilocksExtension; +type Fp = FieldElement; +type Fp3 = FieldElement; + +const TRIALS: usize = 200; + +/// Deterministic SplitMix64 (no `rand` dependency). +struct SplitMix64(u64); +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + fn fp3(&mut self) -> Fp3 { + Fp3::new([ + Fp::from(self.next_u64()), + Fp::from(self.next_u64()), + Fp::from(self.next_u64()), + ]) + } +} + +/// The differential for one production AIR: capture the combined program +/// once via the production entry point, then assert on random two-step +/// frames that interpreting it matches the compiled folders — prover side +/// (`eval_program` vs `compute_transition_prover`) and verifier side +/// (`eval_program_verifier` vs `compute_transition`). +fn check_air(air: &dyn AIR, label: &str) { + let n = air.context().num_transition_constraints; + let num_base = air.num_base_transition_constraints(); + let (n_main, n_aux) = air.trace_layout(); + + // The production capture (lazy OnceLock behind the AIR). + let prog = air.constraint_program(); + assert_eq!(prog.roots.len(), n, "[{label}] one root per constraint"); + // Release-safe exact-once backstop: root id 0 is the reserved base-zero + // sentinel, and no production constraint is identically zero — a root + // left at the sentinel means its constraint_idx was never emitted + // (e.g. a double-emit/skip typo), which the debug-only EmitTracker + // would miss in a release test build. + for (i, &root) in prog.roots.iter().enumerate() { + assert_ne!(root, 0, "[{label}] constraint {i} was never captured"); + } + + let mut rng = SplitMix64(0xBADC_0FFE ^ label.len() as u64); + for trial in 0..TRIALS { + // Random two-step prover frame shaped like this table. + let mk_step = |rng: &mut SplitMix64| { + let main: Vec = (0..n_main).map(|_| Fp::from(rng.next_u64())).collect(); + let aux: Vec = (0..n_aux).map(|_| rng.fp3()).collect(); + TableView::new(vec![main], vec![aux]) + }; + let frame = Frame::::new(vec![mk_step(&mut rng), mk_step(&mut rng)]); + let challenges = vec![rng.fp3(), rng.fp3()]; // [z, alpha] + let alphas: Vec = (0..air.max_bus_elements() + 2).map(|_| rng.fp3()).collect(); + let offset = rng.fp3(); + + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &offset, + ); + + // --- prover side: folder vs interpreter --- + let mut f_base = vec![Fp::zero(); num_base]; + let mut f_ext = vec![Fp3::zero(); n]; + air.compute_transition_prover(&ctx, &mut f_base, &mut f_ext); + + let mut i_base = vec![Fp::zero(); num_base]; + let mut i_ext = vec![Fp3::zero(); n]; + eval_program(prog, &ctx, &mut i_base, &mut i_ext); + + for c in 0..num_base { + assert_eq!( + f_base[c], i_base[c], + "[{label}] prover folder vs interpreter, base constraint {c}, trial {trial}" + ); + } + for c in num_base..n { + assert_eq!( + f_ext[c], i_ext[c], + "[{label}] prover folder vs interpreter, ext constraint {c}, trial {trial}" + ); + } + + // --- verifier side: embed the frame into the extension --- + let embed = |step: &TableView| -> TableView { + let main: Vec = (0..n_main) + .map(|c| step.get_main_evaluation_element(0, c).to_extension()) + .collect(); + let aux: Vec = (0..n_aux) + .map(|c| *step.get_aux_evaluation_element(0, c)) + .collect(); + TableView::new(vec![main], vec![aux]) + }; + let vframe: Frame = Frame::new(vec![ + embed(frame.get_evaluation_step(0)), + embed(frame.get_evaluation_step(1)), + ]); + let vctx = TransitionEvaluationContext::::new_verifier( + &vframe, + &challenges, + &alphas, + &offset, + ); + + let v_folder = air.compute_transition(&vctx); + let mut v_interp = vec![Fp3::zero(); n]; + eval_program_verifier(prog, &vctx, &mut v_interp); + + for c in 0..n { + assert_eq!( + v_folder[c], v_interp[c], + "[{label}] verifier folder vs interpreter, constraint {c}, trial {trial}" + ); + } + } +} + +#[test] +fn all_table_programs_match_folders() { + let opts = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 valid"); + + check_air(&create_cpu_air(&opts), "CPU"); + check_air(&create_bitwise_air(&opts), "BITWISE"); + check_air(&create_lt_air(&opts), "LT"); + check_air(&create_shift_air(&opts), "SHIFT"); + check_air(&create_eq_air(&opts), "EQ"); + check_air(&create_bytewise_air(&opts), "BYTEWISE"); + check_air(&create_store_air(&opts), "STORE"); + check_air(&create_cpu32_air(&opts), "CPU32"); + check_air(&create_memw_air(&opts), "MEMW"); + check_air(&create_memw_aligned_air(&opts), "MEMW_A"); + check_air(&create_memw_register_air(&opts), "MEMW_R"); + check_air(&create_load_air(&opts), "LOAD"); + check_air(&create_decode_air(&opts), "DECODE"); + check_air(&create_mul_air(&opts), "MUL"); + check_air(&create_dvrm_air(&opts), "DVRM"); + check_air(&create_branch_air(&opts), "BRANCH"); + check_air(&create_halt_air(&opts), "HALT"); + check_air(&create_commit_air(&opts), "COMMIT"); + check_air(&create_page_air(&opts, 0x1000), "PAGE"); + check_air(&create_register_air(&opts), "REGISTER"); + check_air(&create_keccak_air(&opts), "KECCAK"); + check_air(&create_keccak_rnd_air(&opts), "KECCAK_RND"); + check_air(&create_keccak_rc_air(&opts), "KECCAK_RC"); + check_air(&create_ecsm_air(&opts), "ECSM"); + check_air(&create_ec_scalar_air(&opts), "EC_SCALAR"); + check_air(&create_ecdas_air(&opts), "ECDAS"); +} diff --git a/prover/src/tests/constraint_set_tests_a.rs b/prover/src/tests/constraint_set_tests_a.rs index 7be6d4359..fafaedfe9 100644 --- a/prover/src/tests/constraint_set_tests_a.rs +++ b/prover/src/tests/constraint_set_tests_a.rs @@ -17,7 +17,6 @@ use stark::constraints::builder::{ num_base_from_meta, }; use stark::frame::Frame; -use stark::lookup::PackingShifts; use stark::table::TableView; use stark::traits::TransitionEvaluationContext; @@ -80,6 +79,16 @@ where set.eval(&mut cb); let (prog, degrees) = cb.finish(num_base); assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); + // Release-safe exact-once check: the emitted indices must be exactly + // 0..n. The per-emit EmitTracker only exists under debug_assertions, + // which CI's --release test build compiles out; this assert catches a + // double-emit/skip typo (count still == n) in any build profile. + let mut emitted: Vec = degrees.iter().map(|&(idx, _)| idx).collect(); + emitted.sort_unstable(); + assert!( + emitted.iter().enumerate().all(|(i, &idx)| i == idx), + "[{label}] emitted constraint indices are not exactly 0..{n}: {emitted:?}" + ); for &(idx, measured) in °rees { assert!( measured <= meta[idx].degree, @@ -87,9 +96,6 @@ where meta[idx].degree ); } - - let shifts = PackingShifts::::new(); - let vshifts = PackingShifts::::new(); let no_ch: Vec = vec![]; let offset_e = Fp3::zero(); @@ -105,7 +111,6 @@ where &no_ch, &no_ch, &offset_e, - &shifts, ); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; @@ -117,7 +122,7 @@ where let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( - &frame_e, &no_ch, &no_ch, &offset_e, &vshifts, + &frame_e, &no_ch, &no_ch, &offset_e, ); let mut vext_out = vec![Fp3::zero(); n]; let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index 492d6dad8..87f1d3101 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -21,7 +21,6 @@ use stark::constraints::builder::{ num_base_from_meta, }; use stark::frame::Frame; -use stark::lookup::PackingShifts; use stark::table::TableView; use stark::traits::TransitionEvaluationContext; @@ -73,6 +72,16 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz set.eval(&mut cb); let (prog, degrees) = cb.finish(n); assert_eq!(degrees.len(), n, "[{label}] one emit per constraint"); + // Release-safe exact-once check: the emitted indices must be exactly + // 0..n. The per-emit EmitTracker only exists under debug_assertions, + // which CI's --release test build compiles out; this assert catches a + // double-emit/skip typo (count still == n) in any build profile. + let mut emitted: Vec = degrees.iter().map(|&(idx, _)| idx).collect(); + emitted.sort_unstable(); + assert!( + emitted.iter().enumerate().all(|(i, &idx)| i == idx), + "[{label}] emitted constraint indices are not exactly 0..{n}: {emitted:?}" + ); for &(idx, measured) in °rees { assert_eq!( measured, meta[idx].degree, @@ -80,9 +89,6 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz meta[idx].degree ); } - - let shifts = PackingShifts::::new(); - let vshifts = PackingShifts::::new(); let no_ch: Vec = vec![]; let offset_e = Fp3::zero(); @@ -98,7 +104,6 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz &no_ch, &no_ch, &offset_e, - &shifts, ); let mut base_out = vec![FE::zero(); n]; let mut ext_out = vec![Fp3::zero(); n]; @@ -110,7 +115,7 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz let frame_e = Frame::::new(vec![TableView::new(vec![row_e.clone()], vec![vec![]])]); let vctx = TransitionEvaluationContext::::new_verifier( - &frame_e, &no_ch, &no_ch, &offset_e, &vshifts, + &frame_e, &no_ch, &no_ch, &offset_e, ); let mut vext_out = vec![Fp3::zero(); n]; let mut vfolder = VerifierEvalFolder::new(&vctx, &mut vext_out); diff --git a/prover/src/tests/cpu32_tests.rs b/prover/src/tests/cpu32_tests.rs index 37f2bfec2..2b683cdfd 100644 --- a/prover/src/tests/cpu32_tests.rs +++ b/prover/src/tests/cpu32_tests.rs @@ -10,7 +10,6 @@ use crate::tables::types::{ use math::field::element::FieldElement; use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; use stark::frame::Frame; -use stark::lookup::PackingShifts; use stark::table::TableView; use stark::traits::TransitionEvaluationContext; @@ -22,16 +21,10 @@ fn eval_cpu32(row: &[FE]) -> Vec { vec![row.to_vec()], vec![vec![]], )]); - let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &no_e, - &no_e, - &offset_e, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(frame.as_row_frame(), &no_e, &no_e, &offset_e); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ec_scalar_tests.rs b/prover/src/tests/ec_scalar_tests.rs index b8a60edc5..f8a19cf79 100644 --- a/prover/src/tests/ec_scalar_tests.rs +++ b/prover/src/tests/ec_scalar_tests.rs @@ -8,7 +8,6 @@ use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; use math::field::element::FieldElement; use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; use stark::frame::Frame; -use stark::lookup::PackingShifts; use stark::table::TableView; use stark::trace::TraceTable; use stark::traits::TransitionEvaluationContext; @@ -24,16 +23,10 @@ fn eval_row(trace: &TraceTable, row: usize vec![main], vec![vec![]], )]); - let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &no_e, - &no_e, - &offset_e, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(frame.as_row_frame(), &no_e, &no_e, &offset_e); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ecdas_tests.rs b/prover/src/tests/ecdas_tests.rs index b692d5c4a..d50cf9abd 100644 --- a/prover/src/tests/ecdas_tests.rs +++ b/prover/src/tests/ecdas_tests.rs @@ -8,7 +8,6 @@ use ecsm::compute_witness; use math::field::element::FieldElement; use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; use stark::frame::Frame; -use stark::lookup::PackingShifts; use stark::table::TableView; use stark::trace::TraceTable; use stark::traits::TransitionEvaluationContext; @@ -55,16 +54,10 @@ fn eval_row(trace: &TraceTable, row: usize vec![main], vec![vec![]], )]); - let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &no_e, - &no_e, - &offset_e, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(frame.as_row_frame(), &no_e, &no_e, &offset_e); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/ecsm_tests.rs b/prover/src/tests/ecsm_tests.rs index 3b671a8fd..9b98f8934 100644 --- a/prover/src/tests/ecsm_tests.rs +++ b/prover/src/tests/ecsm_tests.rs @@ -7,7 +7,6 @@ use ecsm::compute_witness; use math::field::element::FieldElement; use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; use stark::frame::Frame; -use stark::lookup::PackingShifts; use stark::table::TableView; use stark::trace::TraceTable; use stark::traits::TransitionEvaluationContext; @@ -51,16 +50,10 @@ fn eval_row(trace: &TraceTable, row: usize vec![main], vec![vec![]], )]); - let shifts = PackingShifts::::new(); let no_e: Vec> = vec![]; let offset_e = FieldElement::::zero(); - let ctx = TransitionEvaluationContext::new_prover( - frame.as_row_frame(), - &no_e, - &no_e, - &offset_e, - &shifts, - ); + let ctx = + TransitionEvaluationContext::new_prover(frame.as_row_frame(), &no_e, &no_e, &offset_e); let mut base = vec![FE::zero(); n]; let mut ext = vec![FieldElement::::zero(); n]; let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index e1322e341..2c06d7fcf 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -17,6 +17,8 @@ pub mod compute_commit_bus_offset_tests; #[cfg(test)] pub mod constraint_emit_tests; #[cfg(test)] +pub mod constraint_program_tests; +#[cfg(test)] pub mod constraint_set_tests_a; #[cfg(test)] pub mod constraint_set_tests_b; From 8cc1803c76618876cc70ce5351276dca5c40db69 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 18:42:32 -0300 Subject: [PATCH 50/52] stark: small review-feedback comments + drop a duplicate pow - ConstraintSet doc: meta() and eval are parallel index walks that must agree entry for entry; say so where implementers read it. - IrBuilder::const_ext/embed: note both are unreachable from the single-body capture path and kept for IR completeness / GPU lowering. - emit_busvalue_fingerprint: state why only the Linear arm routes through the zero-skip hook. - zerofier: the end-exemption walk computed the same pow twice. --- crypto/stark/src/constraint_ir/builder.rs | 7 +++++++ crypto/stark/src/constraints/builder.rs | 6 ++++++ crypto/stark/src/constraints/zerofier.rs | 2 +- crypto/stark/src/lookup.rs | 4 +++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crypto/stark/src/constraint_ir/builder.rs b/crypto/stark/src/constraint_ir/builder.rs index 82512e974..57d09e2bd 100644 --- a/crypto/stark/src/constraint_ir/builder.rs +++ b/crypto/stark/src/constraint_ir/builder.rs @@ -196,6 +196,10 @@ impl IrBuilder { } /// An extension-field constant, deduplicated by value. + /// + /// No production body produces one today (constraints reach the + /// extension only through trace/challenge leaves); kept for IR + /// completeness and GPU-side lowering. pub fn const_ext(&mut self, v: FieldElement) -> Expr { self.intern_ext(v) } @@ -233,6 +237,9 @@ impl IrBuilder { } /// Explicitly embed a base value into the extension ([`Dim::Ext`]). + /// + /// Unreachable from the single-body capture path (mixed base×ext ops + /// embed implicitly); kept for IR completeness and GPU-side lowering. pub fn embed(&mut self, a: Expr) -> Expr { self.push(Op::Embed(a.id), Dim::Ext) } diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index 8c0cecf36..56b10efd0 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -226,6 +226,12 @@ pub fn num_base_from_meta(meta: &[ConstraintMeta]) -> usize { } /// One table's constraints: metadata + THE single body. +/// +/// `meta()` and `eval` are parallel index walks that must agree entry for +/// entry: `meta()[i]` describes the constraint `eval` emits at index `i` +/// (kind, declared degree). The differential tests enforce the pairing +/// (exact-once emission, declared == measured degree); keep the two methods +/// side by side when editing a set. pub trait ConstraintSet: Send + Sync { /// Idx-ordered metadata (see [`num_base_from_meta`] for the invariants). fn meta(&self) -> Vec; diff --git a/crypto/stark/src/constraints/zerofier.rs b/crypto/stark/src/constraints/zerofier.rs index 95da082da..ba22098de 100644 --- a/crypto/stark/src/constraints/zerofier.rs +++ b/crypto/stark/src/constraints/zerofier.rs @@ -29,7 +29,7 @@ pub fn end_exemptions_roots( // The last row of the trace is g^(N-1); walking backward by g^-1 = g^(N-1) // gives the remaining end-exemption roots. let decrement = trace_primitive_root.pow(trace_length - 1); - let mut current = trace_primitive_root.pow(trace_length - 1); + let mut current = decrement.clone(); let mut roots = Vec::with_capacity(end_exemptions); for _ in 0..end_exemptions { roots.push(current.clone()); diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 97ef28f6b..f06d45e01 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -1967,7 +1967,9 @@ where BusValue::Linear(terms) => { // Routed through the builder so the prover folder can zero-skip // the multiply (Linear is where the constant-0 bus-width padding - // lives). Value-identical either way. + // lives; the packed contributions above fold unconditionally — + // their elements are real trace columns with no zero-heavy + // padding). Value-identical either way. let result = emit_linear_terms(b, terms, offset); (b.fold_fingerprint_term(fp, result, alpha_offset), 1) } From 6614332c54afad0acde3e8e4940a2a36294b2e39 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 2 Jul 2026 18:42:55 -0300 Subject: [PATCH 51/52] reports: move the cross-verification evidence logs off the PR branch The raw cross-version verification logs (examples 22/22, VM 6/6, at the pre-deletion / post-deletion / final-tip checkpoints) are runtime artifacts, not source. They stay available on the frozen branch reference/sscs-cross-verify-evidence; this branch keeps only the scripts that regenerate them (scripts/cross_verify_*.sh). --- .../sscs/cross_verify_examples_final-tip.log | 32 ------------------- .../cross_verify_examples_post-deletion.log | 32 ------------------- reports/sscs/cross_verify_vm_final-tip.log | 17 ---------- .../sscs/cross_verify_vm_post-deletion.log | 17 ---------- reports/sscs/cross_verify_vm_pre-deletion.log | 16 ---------- 5 files changed, 114 deletions(-) delete mode 100644 reports/sscs/cross_verify_examples_final-tip.log delete mode 100644 reports/sscs/cross_verify_examples_post-deletion.log delete mode 100644 reports/sscs/cross_verify_vm_final-tip.log delete mode 100644 reports/sscs/cross_verify_vm_post-deletion.log delete mode 100644 reports/sscs/cross_verify_vm_pre-deletion.log diff --git a/reports/sscs/cross_verify_examples_final-tip.log b/reports/sscs/cross_verify_examples_final-tip.log deleted file mode 100644 index 4b2c38dc5..000000000 --- a/reports/sscs/cross_verify_examples_final-tip.log +++ /dev/null @@ -1,32 +0,0 @@ -==> Refs - OLD 88adbfa6 -> 88adbfa64c - NEW 5d725904 -> 5d7259041a -Preparing worktree (detached HEAD 88adbfa6) -==> Building examples_cli @ 88adbfa64c -> cli_old -==> Building examples_cli @ 5d7259041a -> cli_new -==> Cross-verifying 11 examples, both directions -PASS prove-NEW-verify-OLD : simple_fibonacci -PASS prove-OLD-verify-NEW : simple_fibonacci -PASS prove-NEW-verify-OLD : fibonacci_2_columns -PASS prove-OLD-verify-NEW : fibonacci_2_columns -PASS prove-NEW-verify-OLD : fibonacci_2_cols_shifted -PASS prove-OLD-verify-NEW : fibonacci_2_cols_shifted -PASS prove-NEW-verify-OLD : fibonacci_multi_column -PASS prove-OLD-verify-NEW : fibonacci_multi_column -PASS prove-NEW-verify-OLD : quadratic_air -PASS prove-OLD-verify-NEW : quadratic_air -PASS prove-NEW-verify-OLD : fibonacci_rap -PASS prove-OLD-verify-NEW : fibonacci_rap -PASS prove-NEW-verify-OLD : dummy_air -PASS prove-OLD-verify-NEW : dummy_air -PASS prove-NEW-verify-OLD : simple_addition -PASS prove-OLD-verify-NEW : simple_addition -PASS prove-NEW-verify-OLD : read_only_memory -PASS prove-OLD-verify-NEW : read_only_memory -PASS prove-NEW-verify-OLD : read_only_memory_logup -PASS prove-OLD-verify-NEW : read_only_memory_logup -PASS prove-NEW-verify-OLD : multi_table_lookup -PASS prove-OLD-verify-NEW : multi_table_lookup - -==> RESULT: all 11 examples cross-verify in both directions. -EXIT=0 diff --git a/reports/sscs/cross_verify_examples_post-deletion.log b/reports/sscs/cross_verify_examples_post-deletion.log deleted file mode 100644 index 068964958..000000000 --- a/reports/sscs/cross_verify_examples_post-deletion.log +++ /dev/null @@ -1,32 +0,0 @@ -==> Refs - OLD 88adbfa6 -> 88adbfa64c - NEW 734faae0 -> 734faae04c -Preparing worktree (detached HEAD 88adbfa6) -==> Building examples_cli @ 88adbfa64c -> cli_old -==> Building examples_cli @ 734faae04c -> cli_new -==> Cross-verifying 11 examples, both directions -PASS prove-NEW-verify-OLD : simple_fibonacci -PASS prove-OLD-verify-NEW : simple_fibonacci -PASS prove-NEW-verify-OLD : fibonacci_2_columns -PASS prove-OLD-verify-NEW : fibonacci_2_columns -PASS prove-NEW-verify-OLD : fibonacci_2_cols_shifted -PASS prove-OLD-verify-NEW : fibonacci_2_cols_shifted -PASS prove-NEW-verify-OLD : fibonacci_multi_column -PASS prove-OLD-verify-NEW : fibonacci_multi_column -PASS prove-NEW-verify-OLD : quadratic_air -PASS prove-OLD-verify-NEW : quadratic_air -PASS prove-NEW-verify-OLD : fibonacci_rap -PASS prove-OLD-verify-NEW : fibonacci_rap -PASS prove-NEW-verify-OLD : dummy_air -PASS prove-OLD-verify-NEW : dummy_air -PASS prove-NEW-verify-OLD : simple_addition -PASS prove-OLD-verify-NEW : simple_addition -PASS prove-NEW-verify-OLD : read_only_memory -PASS prove-OLD-verify-NEW : read_only_memory -PASS prove-NEW-verify-OLD : read_only_memory_logup -PASS prove-OLD-verify-NEW : read_only_memory_logup -PASS prove-NEW-verify-OLD : multi_table_lookup -PASS prove-OLD-verify-NEW : multi_table_lookup - -==> RESULT: all 11 examples cross-verify in both directions. -EXIT=0 diff --git a/reports/sscs/cross_verify_vm_final-tip.log b/reports/sscs/cross_verify_vm_final-tip.log deleted file mode 100644 index cf86bd3bd..000000000 --- a/reports/sscs/cross_verify_vm_final-tip.log +++ /dev/null @@ -1,17 +0,0 @@ -==> Refs - OLD 2499b2a8 -> 2499b2a821 - NEW 5d725904 -> 5d7259041a -==> ELFs: /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/sub.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/add.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/arith_8.elf -Preparing worktree (detached HEAD 2499b2a8) -==> Building cli @ 2499b2a821 -> cli_old -==> Building cli @ 5d7259041a -> cli_new -==> Cross-verifying 3 ELFs, both directions -PASS prove-NEW-verify-OLD : sub -PASS prove-OLD-verify-NEW : sub -PASS prove-NEW-verify-OLD : add -PASS prove-OLD-verify-NEW : add -PASS prove-NEW-verify-OLD : arith_8 -PASS prove-OLD-verify-NEW : arith_8 - -==> RESULT: all 3 ELFs cross-verify in both directions. -EXIT=0 diff --git a/reports/sscs/cross_verify_vm_post-deletion.log b/reports/sscs/cross_verify_vm_post-deletion.log deleted file mode 100644 index fd98fb7c9..000000000 --- a/reports/sscs/cross_verify_vm_post-deletion.log +++ /dev/null @@ -1,17 +0,0 @@ -==> Refs - OLD 2499b2a8 -> 2499b2a821 - NEW 734faae0 -> 734faae04c -==> ELFs: /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/sub.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/add.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/arith_8.elf -Preparing worktree (detached HEAD 2499b2a8) -==> Building cli @ 2499b2a821 -> cli_old -==> Building cli @ 734faae04c -> cli_new -==> Cross-verifying 3 ELFs, both directions -PASS prove-NEW-verify-OLD : sub -PASS prove-OLD-verify-NEW : sub -PASS prove-NEW-verify-OLD : add -PASS prove-OLD-verify-NEW : add -PASS prove-NEW-verify-OLD : arith_8 -PASS prove-OLD-verify-NEW : arith_8 - -==> RESULT: all 3 ELFs cross-verify in both directions. -EXIT=0 diff --git a/reports/sscs/cross_verify_vm_pre-deletion.log b/reports/sscs/cross_verify_vm_pre-deletion.log deleted file mode 100644 index a67a2ef34..000000000 --- a/reports/sscs/cross_verify_vm_pre-deletion.log +++ /dev/null @@ -1,16 +0,0 @@ -==> Refs - OLD 2499b2a8 -> 2499b2a821 - NEW 3239eb8a -> 3239eb8af6 -==> ELFs: /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/sub.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/add.elf /Users/maurofab/workspace/lambda_vm/.claude/worktrees/agent-af77afe42beea24bf/executor/program_artifacts/asm/arith_8.elf -Preparing worktree (detached HEAD 2499b2a8) -==> Building cli @ 2499b2a821 -> cli_old -==> Building cli @ 3239eb8af6 -> cli_new -==> Cross-verifying 3 ELFs, both directions -PASS prove-NEW-verify-OLD : sub -PASS prove-OLD-verify-NEW : sub -PASS prove-NEW-verify-OLD : add -PASS prove-OLD-verify-NEW : add -PASS prove-NEW-verify-OLD : arith_8 -PASS prove-OLD-verify-NEW : arith_8 - -==> RESULT: all 3 ELFs cross-verify in both directions. From aa2f6493482d54d317c5504560d9f6399cf705fc Mon Sep 17 00:00:00 2001 From: Mauro Toscano <12560266+MauroToscano@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:57:54 -0300 Subject: [PATCH 52/52] refactor(stark): derive constraint metadata from the single eval body (#772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(stark): derive constraint metadata from the single eval body Metadata (kind, degree, end-exemptions) is now DERIVED from each table's `eval` body instead of hand-maintained in a parallel `meta()` method, so the two can no longer drift out of sync. - `ConstraintBuilder::emit_base/emit_ext` now take the constraint's `degree`; new `emit_*_exempt` variants also take `end_exemptions`. The prover and verifier folders ignore both (dead args -> no hot-path cost); only metadata derivation reads them. - `ConstraintSet::meta()` becomes a provided default that runs the body through a new no-op `MetaBuilder` (records {idx, kind, degree, end_exemptions}). Kind is implied by which sink is called; degree stays hand-declared, now at the emit site next to the expression it describes. - Every per-table `meta()` override, every `*_meta` helper twin, and `logup_meta` are deleted; LogUp metadata is derived from `emit_logup_constraints` the same way. Net -217 LoC across all tables + LogUp + the crypto/stark examples. Verified: workspace + all test targets compile; 169 stark tests and 96 prover constraint tests pass (including constraint_program_tests:: all_table_programs_match_folders and the per-table constraint counts); the capture->interpreter differential and declared-vs-measured-degree gates hold; `make lint` clean on all feature sets. * refactor(stark): split constraint degree (per-table) from row-domain Removes per-constraint `degree` and the positional-int emit args, replacing them with two orthogonal, self-describing concepts: - Degree: only the per-table MAX is consumed (by composition_poly_degree_bound), so it is declared once via `ConstraintSet::max_degree()` (default 2, overridden to 3 on the degree-3 tables) instead of on every emit call. `ConstraintMeta` drops its `degree` field; the bound now reads `max_degree().max(logup_max_degree(layout))`. - Row-domain: `emit_base_exempt(idx, degree, n, e)` becomes `emit_base_rows(idx, RowDomain::except_last(n), e)` — a named type, so the rare end-exemption reads in plain language and is no longer welded onto degree (three unlabeled ints -> one named argument). Only the crypto/stark example AIRs use it; every production table's emit is now `emit_base(idx, expr)`. The composition-poly bound stays byte-identical: each degree-3 table declares `max_degree()` == its former per-constraint max, and the framework folds in the LogUp max via `logup_max_degree`. The capture path asserts each constraint's measured degree is `<= max_degree()` — which caught keccak_rnd's mu-gated (degree-3) IS_BIT during migration. Verified: workspace + all test targets compile; 169 stark tests and 96 prover constraint tests pass (including constraint_program_tests:: all_table_programs_match_folders and the per-table measured<=max_degree gates); `make lint` clean on all feature sets. * perf(stark): inline the emit forwarding onto the per-row hot path The RowDomain refactor turned emit_base/emit_ext into provided defaults that forward to emit_base_rows/emit_ext_rows, adding a call hop on the per-constraint-per-row prover path (vs #764's direct folder emit). A paired ABBA vs #764 (12 pairs) showed a real ~1.1% regression: paired-t 95% CI [-1.99%, -0.27%], Wilcoxon W=10 (significant), 10/12 pairs slower. Marking the forwarding defaults and ProverEvalFolder's emit_*_rows #[inline] collapses the hop back to a direct slice write. Pure codegen hint — no value or wire change, so cross-verification stays green. --- crypto/stark/src/constraints/builder.rs | 231 +++++++++++++++--- crypto/stark/src/constraints/builder_tests.rs | 71 +++--- crypto/stark/src/examples/dummy_air.rs | 18 +- .../src/examples/fibonacci_2_cols_shifted.rs | 19 +- .../stark/src/examples/fibonacci_2_columns.rs | 23 +- .../src/examples/fibonacci_multi_column.rs | 15 +- crypto/stark/src/examples/fibonacci_rap.rs | 23 +- crypto/stark/src/examples/quadratic_air.rs | 10 +- crypto/stark/src/examples/read_only_memory.rs | 35 +-- .../src/examples/read_only_memory_logup.rs | 32 +-- crypto/stark/src/examples/simple_addition.rs | 7 +- crypto/stark/src/examples/simple_fibonacci.rs | 10 +- crypto/stark/src/lookup.rs | 106 ++++---- prover/src/constraints/cpu.rs | 118 +-------- prover/src/constraints/templates.rs | 17 +- prover/src/continuation.rs | 8 +- prover/src/tables/branch.rs | 12 +- prover/src/tables/commit.rs | 18 +- prover/src/tables/cpu32.rs | 31 +-- prover/src/tables/dvrm.rs | 7 +- prover/src/tables/ec_scalar.rs | 7 +- prover/src/tables/ecdas.rs | 32 +-- prover/src/tables/ecsm.rs | 52 +--- prover/src/tables/eq.rs | 13 +- prover/src/tables/keccak.rs | 15 +- prover/src/tables/keccak_rnd.rs | 10 +- prover/src/tables/load.rs | 20 +- prover/src/tables/lt.rs | 14 +- prover/src/tables/memw.rs | 10 +- prover/src/tables/memw_aligned.rs | 6 +- prover/src/tables/memw_register.rs | 6 +- prover/src/tables/mul.rs | 15 +- prover/src/tables/shift.rs | 25 +- prover/src/tables/store.rs | 15 +- prover/src/tests/branch_constraints_tests.rs | 6 +- prover/src/tests/commit_tests.rs | 5 +- prover/src/tests/constraint_emit_tests.rs | 83 +++---- prover/src/tests/constraint_set_tests_a.rs | 25 +- prover/src/tests/constraint_set_tests_b.rs | 10 +- 39 files changed, 485 insertions(+), 695 deletions(-) diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs index 56b10efd0..5395c7228 100644 --- a/crypto/stark/src/constraints/builder.rs +++ b/crypto/stark/src/constraints/builder.rs @@ -119,10 +119,25 @@ pub trait ConstraintBuilder { } // ---- sinks ---------------------------------------------------------- - /// Record base-field constraint `constraint_idx`'s value. - fn emit_base(&mut self, constraint_idx: usize, e: Self::Expr); - /// Record extension-field (LogUp) constraint `constraint_idx`'s value. - fn emit_ext(&mut self, constraint_idx: usize, e: Self::ExprE); + /// Record base-field constraint `constraint_idx`'s value over the trace + /// `rows` it applies to (see [`RowDomain`]). Recording it here is what lets + /// [`ConstraintSet::meta`] be *derived* from this single body (via + /// [`MetaBuilder`]) instead of hand-maintained as a parallel list. The + /// constraint's polynomial degree is NOT declared per-constraint — only the + /// per-table max matters, declared once via [`ConstraintSet::max_degree`]. + fn emit_base_rows(&mut self, constraint_idx: usize, rows: RowDomain, e: Self::Expr); + /// Extension-field (LogUp) counterpart of [`Self::emit_base_rows`]. + fn emit_ext_rows(&mut self, constraint_idx: usize, rows: RowDomain, e: Self::ExprE); + /// Record a base-field constraint that applies to every row (common case). + #[inline] + fn emit_base(&mut self, constraint_idx: usize, e: Self::Expr) { + self.emit_base_rows(constraint_idx, RowDomain::ALL, e); + } + /// Record an extension-field (LogUp) constraint that applies to every row. + #[inline] + fn emit_ext(&mut self, constraint_idx: usize, e: Self::ExprE) { + self.emit_ext_rows(constraint_idx, RowDomain::ALL, e); + } // ---- folds ---------------------------------------------------------- /// Fold one α·value term into a running LogUp fingerprint: @@ -159,41 +174,55 @@ pub enum RootKind { Ext, } -/// Per-constraint metadata: plain data replacing the per-constraint trait -/// objects. `Base` entries MUST form a prefix of an idx-ordered, dense list — -/// see [`num_base_from_meta`]. -/// -/// Every constraint applies to every row (up to `end_exemptions` rows at the -/// end of the trace); there is no per-constraint period/offset/periodic -/// exemption machinery. +/// Which trace rows a transition constraint applies to. `ALL` = every row; +/// `except_last(n)` skips the final `n` rows — used by constraints that read +/// `n` rows ahead (the last `n` rows have no valid "next" to check). Passed at +/// the emit site; degree is NOT here (it's a per-table property, see +/// [`ConstraintSet::max_degree`]) — the two are orthogonal. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct RowDomain { + /// Number of exempted rows at the end of the trace. + pub end_exemptions: usize, +} + +impl RowDomain { + /// Every row (no exemptions). + pub const ALL: RowDomain = RowDomain { end_exemptions: 0 }; + /// Every row except the last `n`. + pub const fn except_last(n: usize) -> RowDomain { + RowDomain { end_exemptions: n } + } +} + +/// Per-constraint metadata, DERIVED from the body (via [`MetaBuilder`]). `Base` +/// entries MUST form a prefix of an idx-ordered, dense list — see +/// [`num_base_from_meta`]. Degree is intentionally absent: only the per-table +/// max is consumed (by `composition_poly_degree_bound`), declared once via +/// [`ConstraintSet::max_degree`]. #[derive(Clone, Debug)] pub struct ConstraintMeta { pub constraint_idx: usize, /// Base | Ext; Base entries MUST be a prefix. pub kind: RootKind, - /// Declared degree; asserted == tree-measured degree (host-side test). - pub degree: usize, /// Number of exempted rows at the end of the trace (default 0). pub end_exemptions: usize, } impl ConstraintMeta { - /// A base-field constraint with default zerofier shape (every row, no - /// exemptions). - pub fn base(constraint_idx: usize, degree: usize) -> Self { + /// A base-field constraint applying to every row. + pub fn base(constraint_idx: usize) -> Self { Self { constraint_idx, kind: RootKind::Base, - degree, end_exemptions: 0, } } - /// An extension-field (LogUp) constraint with default zerofier shape. - pub fn ext(constraint_idx: usize, degree: usize) -> Self { + /// An extension-field (LogUp) constraint applying to every row. + pub fn ext(constraint_idx: usize) -> Self { Self { kind: RootKind::Ext, - ..Self::base(constraint_idx, degree) + ..Self::base(constraint_idx) } } @@ -225,19 +254,37 @@ pub fn num_base_from_meta(meta: &[ConstraintMeta]) -> usize { num_base } -/// One table's constraints: metadata + THE single body. +/// One table's constraints: THE single body. /// -/// `meta()` and `eval` are parallel index walks that must agree entry for -/// entry: `meta()[i]` describes the constraint `eval` emits at index `i` -/// (kind, declared degree). The differential tests enforce the pairing -/// (exact-once emission, declared == measured degree); keep the two methods -/// side by side when editing a set. +/// `eval` is the sole source of truth — it emits every constraint once, +/// declaring each one's kind (via `emit_base`/`emit_ext`), degree, and +/// end-exemptions at the emit site. `meta()` is DERIVED from it by running the +/// same body through a [`MetaBuilder`], so there is no parallel list to keep in +/// sync. See [`num_base_from_meta`] for the invariants the derived metadata +/// upholds. pub trait ConstraintSet: Send + Sync { - /// Idx-ordered metadata (see [`num_base_from_meta`] for the invariants). - fn meta(&self) -> Vec; - /// The single constraint body: emits every constraint in `meta()` exactly - /// once. + /// The single constraint body: emits every constraint exactly once. fn eval>(&self, b: &mut B); + + /// The maximum multivariate degree over this set's base constraints — the + /// only degree info the proof consumes (via `composition_poly_degree_bound`, + /// which takes the per-table max). Declared once here instead of per + /// constraint; default 2 covers most tables, override to 3 for the few that + /// have a degree-3 constraint. Hand-declared, never auto-measured (that + /// would change the composition bound); the capture path asserts every + /// constraint's measured degree is `<=` this. + fn max_degree(&self) -> usize { + 2 + } + + /// Idx-ordered metadata, derived by running [`Self::eval`] through a + /// [`MetaBuilder`] (which records the `{kind, end_exemptions}` at each + /// `emit_*`). Never overridden — the body is the source. + fn meta(&self) -> Vec { + let mut mb = MetaBuilder::new(); + self.eval(&mut mb); + mb.into_meta() + } } /// A [`ConstraintSet`] with no transition constraints — for tables whose @@ -248,12 +295,114 @@ pub trait ConstraintSet: Send + Sync { pub struct EmptyConstraints; impl ConstraintSet for EmptyConstraints { - fn meta(&self) -> Vec { - Vec::new() - } fn eval>(&self, _b: &mut B) {} } +// ============================================================================= +// MetaBuilder — derive ConstraintMeta by running the body with no arithmetic +// ============================================================================= + +/// No-op expression for [`MetaBuilder`]: every leaf and operator yields `Nil`, +/// so running a constraint body over it does no field work — it only drives the +/// `emit_*` calls, which is all metadata derivation needs. +#[derive(Clone, Copy)] +pub struct Nil; + +impl core::ops::Add for Nil { + type Output = Nil; + fn add(self, _rhs: Nil) -> Nil { + Nil + } +} +impl core::ops::Sub for Nil { + type Output = Nil; + fn sub(self, _rhs: Nil) -> Nil { + Nil + } +} +impl core::ops::Mul for Nil { + type Output = Nil; + fn mul(self, _rhs: Nil) -> Nil { + Nil + } +} +impl core::ops::Neg for Nil { + type Output = Nil; + fn neg(self) -> Nil { + Nil + } +} + +/// Derives [`ConstraintMeta`] from a [`ConstraintSet`] body: a metadata-only +/// [`ConstraintBuilder`] whose leaves/operators are no-ops and whose `emit_*` +/// sinks record `{constraint_idx, kind, degree, end_exemptions}`. Runs once at +/// setup — never on the per-row prover path. +pub struct MetaBuilder { + metas: Vec, +} + +impl MetaBuilder { + pub fn new() -> Self { + Self { metas: Vec::new() } + } + + /// The recorded metadata, sorted by `constraint_idx` (emission order need + /// not match index order; the sort restores the dense idx-ordering + /// [`num_base_from_meta`] expects). + pub fn into_meta(mut self) -> Vec { + self.metas.sort_by_key(|m| m.constraint_idx); + self.metas + } +} + +impl Default for MetaBuilder { + fn default() -> Self { + Self::new() + } +} + +impl ConstraintBuilder for MetaBuilder { + type Expr = Nil; + type ExprE = Nil; + + fn main(&self, _offset: usize, _col: usize) -> Nil { + Nil + } + fn aux(&self, _offset: usize, _col: usize) -> Nil { + Nil + } + fn challenge(&self, _idx: usize) -> Nil { + Nil + } + fn alpha_pow(&self, _idx: usize) -> Nil { + Nil + } + fn table_offset(&self) -> Nil { + Nil + } + fn const_base(&self, _v: u64) -> Nil { + Nil + } + fn const_signed(&self, _v: i64) -> Nil { + Nil + } + + fn emit_base_rows(&mut self, constraint_idx: usize, rows: RowDomain, _e: Nil) { + self.metas.push(ConstraintMeta { + constraint_idx, + kind: RootKind::Base, + end_exemptions: rows.end_exemptions, + }); + } + fn emit_ext_rows(&mut self, constraint_idx: usize, rows: RowDomain, _e: Nil) { + self.metas.push(ConstraintMeta { + constraint_idx, + kind: RootKind::Ext, + end_exemptions: rows.end_exemptions, + }); + } +} + // ============================================================================= // Shared AIR plumbing: run a ConstraintSet through the folders // ============================================================================= @@ -460,11 +609,13 @@ where FieldElement::::from(v) } - fn emit_base(&mut self, constraint_idx: usize, e: FieldElement) { + #[inline] + fn emit_base_rows(&mut self, constraint_idx: usize, _rows: RowDomain, e: FieldElement) { self.tracker.mark(constraint_idx); self.base_out[constraint_idx] = e; } - fn emit_ext(&mut self, constraint_idx: usize, e: FieldElement) { + #[inline] + fn emit_ext_rows(&mut self, constraint_idx: usize, _rows: RowDomain, e: FieldElement) { debug_assert!( constraint_idx >= self.base_out.len(), "emit_ext with a base-prefix index {constraint_idx}" @@ -593,11 +744,11 @@ where FieldElement::::from(v).to_extension::() } - fn emit_base(&mut self, constraint_idx: usize, e: FieldElement) { + fn emit_base_rows(&mut self, constraint_idx: usize, _rows: RowDomain, e: FieldElement) { self.tracker.mark(constraint_idx); self.ext_out[constraint_idx] = e; } - fn emit_ext(&mut self, constraint_idx: usize, e: FieldElement) { + fn emit_ext_rows(&mut self, constraint_idx: usize, _rows: RowDomain, e: FieldElement) { self.tracker.mark(constraint_idx); self.ext_out[constraint_idx] = e; } @@ -814,13 +965,15 @@ impl ConstraintBuilder for CaptureBuilder { IrExpr::leaf(TreeKind::ConstSigned(v), Dim::Base, 0) } - fn emit_base(&mut self, constraint_idx: usize, e: IrExpr) { + fn emit_base_rows(&mut self, constraint_idx: usize, _rows: RowDomain, e: IrExpr) { debug_assert_eq!(e.0.dim, Dim::Base, "emit_base on an extension expression"); let root = self.flatten(&e); self.ir.emit(constraint_idx, root); + // Record the TREE-MEASURED degree so the host-side test can assert + // measured <= the table's declared max_degree(). self.degrees.push((constraint_idx, e.degree())); } - fn emit_ext(&mut self, constraint_idx: usize, e: IrExpr) { + fn emit_ext_rows(&mut self, constraint_idx: usize, _rows: RowDomain, e: IrExpr) { let root = self.flatten(&e); self.ir.emit(constraint_idx, root); self.degrees.push((constraint_idx, e.degree())); diff --git a/crypto/stark/src/constraints/builder_tests.rs b/crypto/stark/src/constraints/builder_tests.rs index 61b494cf2..fa6eba59c 100644 --- a/crypto/stark/src/constraints/builder_tests.rs +++ b/crypto/stark/src/constraints/builder_tests.rs @@ -18,7 +18,7 @@ use math::field::goldilocks::GoldilocksField as Fp; use crate::constraint_ir::{Dim, eval_program, eval_program_verifier}; use crate::constraints::builder::{ CaptureBuilder, ConstraintBuilder, ConstraintMeta, ConstraintSet, ProverEvalFolder, RootKind, - VerifierEvalFolder, num_base_from_meta, + RowDomain, VerifierEvalFolder, num_base_from_meta, }; use crate::frame::Frame; use crate::table::TableView; @@ -78,25 +78,20 @@ fn inv_shift_32() -> u64 { struct SampleSet; impl ConstraintSet for SampleSet { - fn meta(&self) -> Vec { - vec![ - ConstraintMeta::base(0, 2), // EqXor - ConstraintMeta::base(1, 2), // IsBit - ConstraintMeta::base(2, 3), // add carry 0 (cond-gated bit check) - ConstraintMeta::base(3, 3), // add carry 1 - ConstraintMeta::ext(4, 1), // LogUp-shaped - ] + // idx 2,3 are degree-3 carry constraints. + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { - // idx 0 — EqXor: res − (eq + invert − 2·eq·invert). + // idx 0 — EqXor (degree 2): res − (eq + invert − 2·eq·invert). let res = b.main(0, cols::RES); let eq = b.main(0, cols::EQ); let invert = b.main(0, cols::INVERT); let two = b.const_base(2); b.emit_base(0, res - (eq.clone() + invert.clone() - two * eq * invert)); - // idx 1 — IsBit: x·(1 − x). + // idx 1 — IsBit (degree 2): x·(1 − x). let x = b.main(0, cols::BIT); let one = b.one(); b.emit_base(1, x.clone() * (one - x)); @@ -116,10 +111,11 @@ impl ConstraintSet for SampleSet { let one = b.one(); let carry_0 = (lhs_lo + rhs_lo - sum_lo) * inv_2_32.clone(); let carry_1 = (lhs_hi + rhs_hi + carry_0.clone() - sum_hi) * inv_2_32; + // idx 2, 3 — degree 3 (cond·carry·(1−carry)). b.emit_base(2, cond.clone() * carry_0.clone() * (one.clone() - carry_0)); b.emit_base(3, cond * carry_1.clone() * (one - carry_1)); - // idx 4 — LogUp-shaped: (challenge₀ + aux₀)·alpha₀ − L/N. + // idx 4 — LogUp-shaped (degree 1): (challenge₀ + aux₀)·alpha₀ − L/N. let ch = b.challenge(0); let au = b.aux(0, 0); let alpha = b.alpha_pow(0); @@ -288,12 +284,12 @@ fn capture_measured_degrees_match_declared_meta() { let meta = SampleSet.meta(); assert_eq!(degrees.len(), meta.len()); + let max_degree = SampleSet.max_degree(); for (i, &(idx, measured)) in degrees.iter().enumerate() { assert_eq!(idx, i, "emit order != idx order"); - assert_eq!( - measured, meta[idx].degree, - "constraint {idx}: tree-measured degree {measured} != declared {}", - meta[idx].degree + assert!( + measured <= max_degree, + "constraint {idx}: tree-measured degree {measured} EXCEEDS max_degree() {max_degree}" ); } } @@ -303,9 +299,9 @@ fn meta_base_prefix_gives_num_base() { assert_eq!(num_base_from_meta(&SampleSet.meta()), NUM_BASE); // Pure-base and pure-ext lists. - let pure_base = vec![ConstraintMeta::base(0, 1), ConstraintMeta::base(1, 2)]; + let pure_base = vec![ConstraintMeta::base(0), ConstraintMeta::base(1)]; assert_eq!(num_base_from_meta(&pure_base), 2); - let pure_ext = vec![ConstraintMeta::ext(0, 1), ConstraintMeta::ext(1, 1)]; + let pure_ext = vec![ConstraintMeta::ext(0), ConstraintMeta::ext(1)]; assert_eq!(num_base_from_meta(&pure_ext), 0); assert_eq!(num_base_from_meta(&[]), 0); @@ -320,9 +316,9 @@ fn meta_base_prefix_gives_num_base() { #[should_panic(expected = "must form a prefix")] fn meta_base_after_ext_panics() { let bad = vec![ - ConstraintMeta::base(0, 1), - ConstraintMeta::ext(1, 1), - ConstraintMeta::base(2, 1), + ConstraintMeta::base(0), + ConstraintMeta::ext(1), + ConstraintMeta::base(2), ]; num_base_from_meta(&bad); } @@ -331,7 +327,7 @@ fn meta_base_after_ext_panics() { #[test] #[should_panic(expected = "dense and idx-ordered")] fn meta_non_dense_panics() { - let bad = vec![ConstraintMeta::base(0, 1), ConstraintMeta::base(2, 1)]; + let bad = vec![ConstraintMeta::base(0), ConstraintMeta::base(2)]; num_base_from_meta(&bad); } @@ -447,13 +443,13 @@ impl ConstraintBuilder for CountingCapture { fn const_signed(&self, v: i64) -> Self::Expr { self.inner.const_signed(v) } - fn emit_base(&mut self, constraint_idx: usize, e: Self::Expr) { + fn emit_base_rows(&mut self, constraint_idx: usize, rows: RowDomain, e: Self::Expr) { self.base_idxs.push(constraint_idx); - self.inner.emit_base(constraint_idx, e); + self.inner.emit_base_rows(constraint_idx, rows, e); } - fn emit_ext(&mut self, constraint_idx: usize, e: Self::ExprE) { + fn emit_ext_rows(&mut self, constraint_idx: usize, rows: RowDomain, e: Self::ExprE) { self.ext_idxs.push(constraint_idx); - self.inner.emit_ext(constraint_idx, e); + self.inner.emit_ext_rows(constraint_idx, rows, e); } } @@ -524,20 +520,13 @@ mod lcols { } impl ConstraintSet for NextRowLogUpSet { - fn meta(&self) -> Vec { - vec![ - ConstraintMeta::base(0, 1), - ConstraintMeta::ext(1, 1).with_end_exemptions(1), - ] - } - fn eval>(&self, b: &mut B) { - // idx 0 (base): next-row main read — main(1, VAL) − main(0, VAL). + // idx 0 (base, degree 1): next-row main read — main(1, VAL) − main(0, VAL). let cur = b.main(0, lcols::VAL); let next = b.main(1, lcols::VAL); b.emit_base(0, next - cur); - // idx 1 (ext): acc' − acc − (challenge₀·α₀ + term·α₁) + L/N, + // idx 1 (ext, degree 1, 1 end exemption): acc' − acc − (challenge₀·α₀ + term·α₁) + L/N, // with acc' read from the NEXT row (aux offset 1). let acc = b.aux(0, lcols::ACC); let acc_next = b.aux(1, lcols::ACC); @@ -546,7 +535,11 @@ impl ConstraintSet for NextRowLogUpSet { let a0 = b.alpha_pow(0); let a1 = b.alpha_pow(1); let off = b.table_offset(); - b.emit_ext(1, acc_next - acc - (ch * a0 + term * a1) + off); + b.emit_ext_rows( + 1, + RowDomain::except_last(1), + acc_next - acc - (ch * a0 + term * a1) + off, + ); } } @@ -560,8 +553,12 @@ fn next_row_aux_and_multi_alpha_folder_matches_capture() { let mut cb = CaptureBuilder::::new(); NextRowLogUpSet.eval(&mut cb); let (prog, degrees) = cb.finish(num_base); + let max_degree = NextRowLogUpSet.max_degree(); for &(idx, measured) in °rees { - assert_eq!(measured, meta[idx].degree, "constraint {idx} degree"); + assert!( + measured <= max_degree, + "constraint {idx}: tree degree {measured} EXCEEDS max_degree() {max_degree}" + ); } let mut rng = SplitMix64(0x0004_F01D_u64 ^ 0xABCD); for trial in 0..TRIALS { diff --git a/crypto/stark/src/examples/dummy_air.rs b/crypto/stark/src/examples/dummy_air.rs index e898ade9f..9decb9a53 100644 --- a/crypto/stark/src/examples/dummy_air.rs +++ b/crypto/stark/src/examples/dummy_air.rs @@ -2,7 +2,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -22,23 +22,15 @@ type StarkField = GoldilocksField; pub struct DummyConstraints; impl ConstraintSet for DummyConstraints { - fn meta(&self) -> Vec { - vec![ - // idx 0: fibonacci on column 1; reads two next rows ⇒ 2 end exemptions. - ConstraintMeta::base(0, 1).with_end_exemptions(2), - // idx 1: IS_BIT on column 0, every row. - ConstraintMeta::base(1, 2), - ] - } - fn eval>(&self, b: &mut B) { - // a_{i+2} = a_{i+1} + a_i on column 1. + // idx 0: a_{i+2} = a_{i+1} + a_i on column 1; reads two next rows ⇒ 2 + // end exemptions. let a0 = b.main(0, 1); let a1 = b.main(1, 1); let a2 = b.main(2, 1); - b.emit_base(0, a2 - a1 - a0); + b.emit_base_rows(0, RowDomain::except_last(2), a2 - a1 - a0); - // bit * (bit - 1) = 0 on column 0. + // idx 1: IS_BIT on column 0, every row. bit * (bit - 1) = 0. let bit = b.main(0, 0); let one = b.one(); b.emit_base(1, bit.clone() * (bit - one)); diff --git a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs index a1b88a586..855469e4b 100644 --- a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs +++ b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs @@ -2,7 +2,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -57,25 +57,16 @@ impl ConstraintSet for Fibonacci2ColsShiftedConstraints where F: IsFFTField + Send + Sync, { - fn meta(&self) -> Vec { - vec![ - // idx 0: Col0_{i+1} = Col1_i; reads the next row ⇒ 1 end exemption. - ConstraintMeta::base(0, 1).with_end_exemptions(1), - // idx 1: Col1_{i+1} = Col0_i + Col1_i; reads the next row ⇒ 1 end exemption. - ConstraintMeta::base(1, 1).with_end_exemptions(1), - ] - } - fn eval>(&self, b: &mut B) { let a0_0 = b.main(0, 0); let a0_1 = b.main(0, 1); let a1_0 = b.main(1, 0); let a1_1 = b.main(1, 1); - // Col0_{i+1} = Col1_i - b.emit_base(0, a1_0 - a0_1.clone()); - // Col1_{i+1} = Col0_i + Col1_i - b.emit_base(1, a1_1 - a0_0 - a0_1); + // idx 0: Col0_{i+1} = Col1_i; reads the next row ⇒ 1 end exemption. + b.emit_base_rows(0, RowDomain::except_last(1), a1_0 - a0_1.clone()); + // idx 1: Col1_{i+1} = Col0_i + Col1_i; reads the next row ⇒ 1 end exemption. + b.emit_base_rows(1, RowDomain::except_last(1), a1_1 - a0_0 - a0_1); } } diff --git a/crypto/stark/src/examples/fibonacci_2_columns.rs b/crypto/stark/src/examples/fibonacci_2_columns.rs index 0e568c1f4..beb9c999f 100644 --- a/crypto/stark/src/examples/fibonacci_2_columns.rs +++ b/crypto/stark/src/examples/fibonacci_2_columns.rs @@ -5,7 +5,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -34,25 +34,20 @@ impl ConstraintSet for Fibonacci2ColsConstraints where F: IsFFTField + Send + Sync, { - fn meta(&self) -> Vec { - vec![ - // idx 0: s_{0, i+1} = s_{0, i} + s_{1, i}; reads the next row ⇒ 1 end exemption. - ConstraintMeta::base(0, 1).with_end_exemptions(1), - // idx 1: s_{1, i+1} = s_{1, i} + s_{0, i+1}; reads the next row ⇒ 1 end exemption. - ConstraintMeta::base(1, 1).with_end_exemptions(1), - ] - } - fn eval>(&self, b: &mut B) { let s0_0 = b.main(0, 0); let s0_1 = b.main(0, 1); let s1_0 = b.main(1, 0); let s1_1 = b.main(1, 1); - // s_{0, i+1} = s_{0, i} + s_{1, i} - b.emit_base(0, s1_0.clone() - s0_0 - s0_1.clone()); - // s_{1, i+1} = s_{1, i} + s_{0, i+1} - b.emit_base(1, s1_1 - s0_1 - s1_0); + // idx 0: s_{0, i+1} = s_{0, i} + s_{1, i}; reads the next row ⇒ 1 end exemption. + b.emit_base_rows( + 0, + RowDomain::except_last(1), + s1_0.clone() - s0_0 - s0_1.clone(), + ); + // idx 1: s_{1, i+1} = s_{1, i} + s_{0, i+1}; reads the next row ⇒ 1 end exemption. + b.emit_base_rows(1, RowDomain::except_last(1), s1_1 - s0_1 - s1_0); } } diff --git a/crypto/stark/src/examples/fibonacci_multi_column.rs b/crypto/stark/src/examples/fibonacci_multi_column.rs index d5824b3a4..ae6e61527 100644 --- a/crypto/stark/src/examples/fibonacci_multi_column.rs +++ b/crypto/stark/src/examples/fibonacci_multi_column.rs @@ -4,7 +4,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -39,21 +39,14 @@ where F: IsSubFieldOf + IsFFTField + Send + Sync, E: IsField + Send + Sync, { - fn meta(&self) -> Vec { - // idx i: column i's a_{j+2} = a_{j+1} + a_j; reads two next rows ⇒ 2 - // end exemptions. - (0..self.num_columns) - .map(|i| ConstraintMeta::base(i, 1).with_end_exemptions(2)) - .collect() - } - fn eval>(&self, b: &mut B) { for col in 0..self.num_columns { let a0 = b.main(0, col); let a1 = b.main(1, col); let a2 = b.main(2, col); - // Constraint: a2 = a1 + a0 => a2 - a1 - a0 = 0 - b.emit_base(col, a2 - a1 - a0); + // idx col: column col's a_{j+2} = a_{j+1} + a_j; reads two next rows + // ⇒ 2 end exemptions. + b.emit_base_rows(col, RowDomain::except_last(2), a2 - a1 - a0); } } } diff --git a/crypto/stark/src/examples/fibonacci_rap.rs b/crypto/stark/src/examples/fibonacci_rap.rs index e32709caf..22003952d 100644 --- a/crypto/stark/src/examples/fibonacci_rap.rs +++ b/crypto/stark/src/examples/fibonacci_rap.rs @@ -4,7 +4,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -39,31 +39,24 @@ impl ConstraintSet for FibonacciRAPConstraints where F: IsFFTField + Send + Sync, { - fn meta(&self) -> Vec { - vec![ - // idx 0: fibonacci; end exemptions hard-coded for the steps = 16 - // integration tests. - ConstraintMeta::base(0, 1).with_end_exemptions(3 + 32 - 16 - 1), - // idx 1: permutation; reads the next row ⇒ 1 end exemption. - ConstraintMeta::ext(1, 2).with_end_exemptions(1), - ] - } - fn eval>(&self, b: &mut B) { - // a_{i+2} = a_{i+1} + a_i on column 0. + // idx 0: a_{i+2} = a_{i+1} + a_i on column 0. End exemptions hard-coded + // for the steps = 16 integration tests. let a0 = b.main(0, 0); let a1 = b.main(1, 0); let a2 = b.main(2, 0); - b.emit_base(0, a2 - a1 - a0); + b.emit_base_rows(0, RowDomain::except_last(3 + 32 - 16 - 1), a2 - a1 - a0); - // z_{i+1} * (b_i + gamma) = z_i * (a_i + gamma) + // idx 1: permutation; z_{i+1} * (b_i + gamma) = z_i * (a_i + gamma); + // reads the next row ⇒ 1 end exemption. let z_i = b.aux(0, 0); let z_i_plus_one = b.aux(1, 0); let gamma = b.challenge(0); let a_i = b.main(0, 0); let b_i = b.main(0, 1); - b.emit_ext( + b.emit_ext_rows( 1, + RowDomain::except_last(1), z_i_plus_one * (b_i + gamma.clone()) - z_i * (a_i + gamma), ); } diff --git a/crypto/stark/src/examples/quadratic_air.rs b/crypto/stark/src/examples/quadratic_air.rs index 4818baed1..aedaf1d72 100644 --- a/crypto/stark/src/examples/quadratic_air.rs +++ b/crypto/stark/src/examples/quadratic_air.rs @@ -4,7 +4,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -33,15 +33,11 @@ impl ConstraintSet for QuadraticConstraints where F: IsFFTField + Send + Sync, { - fn meta(&self) -> Vec { - // idx 0: x_{i+1} = x_i²; reads the next row ⇒ 1 end exemption. - vec![ConstraintMeta::base(0, 2).with_end_exemptions(1)] - } - fn eval>(&self, b: &mut B) { let x = b.main(0, 0); let x_squared = b.main(1, 0); - b.emit_base(0, x_squared - x.clone() * x); + // idx 0: x_{i+1} = x_i²; reads the next row ⇒ 1 end exemption. + b.emit_base_rows(0, RowDomain::except_last(1), x_squared - x.clone() * x); } } diff --git a/crypto/stark/src/examples/read_only_memory.rs b/crypto/stark/src/examples/read_only_memory.rs index dd74a1b37..521bd7ca9 100644 --- a/crypto/stark/src/examples/read_only_memory.rs +++ b/crypto/stark/src/examples/read_only_memory.rs @@ -4,7 +4,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -31,15 +31,6 @@ impl ConstraintSet for ReadOnlyRAPConstraints where F: IsFFTField + Send + Sync, { - fn meta(&self) -> Vec { - // All three read the next row ⇒ 1 end exemption each. - vec![ - ConstraintMeta::base(0, 2).with_end_exemptions(1), // continuity - ConstraintMeta::base(1, 2).with_end_exemptions(1), // single value - ConstraintMeta::ext(2, 2).with_end_exemptions(1), // permutation - ] - } - fn eval>(&self, b: &mut B) { let a_sorted_0 = b.main(0, 2); let a_sorted_1 = b.main(1, 2); @@ -48,10 +39,19 @@ where let one = b.one(); let addr_diff = a_sorted_1 - a_sorted_0; - // (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address - b.emit_base(0, addr_diff.clone() * (addr_diff.clone() - one.clone())); - // (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 - b.emit_base(1, (v_sorted_1 - v_sorted_0) * (addr_diff - one)); + // All three read the next row ⇒ degree 2, 1 end exemption each. + // idx 0 — continuity: (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address + b.emit_base_rows( + 0, + RowDomain::except_last(1), + addr_diff.clone() * (addr_diff.clone() - one.clone()), + ); + // idx 1 — single value: (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 + b.emit_base_rows( + 1, + RowDomain::except_last(1), + (v_sorted_1 - v_sorted_0) * (addr_diff - one), + ); // (z - (a'_{i+1} + α * v'_{i+1})) * p_{i+1} = (z - (a_{i+1} + α * v_{i+1})) * p_i let p0 = b.aux(0, 0); @@ -64,7 +64,12 @@ where let v_sorted_1 = b.main(1, 3); let sorted_fp = z.clone() - (a_sorted_1 + v_sorted_1 * alpha.clone()); let unsorted_fp = z - (a1 + v1 * alpha); - b.emit_ext(2, sorted_fp * p1 - unsorted_fp * p0); + // idx 2 — permutation (degree 2, 1 end exemption). + b.emit_ext_rows( + 2, + RowDomain::except_last(1), + sorted_fp * p1 - unsorted_fp * p0, + ); } } diff --git a/crypto/stark/src/examples/read_only_memory_logup.rs b/crypto/stark/src/examples/read_only_memory_logup.rs index 2ad9a28c3..5090098bd 100644 --- a/crypto/stark/src/examples/read_only_memory_logup.rs +++ b/crypto/stark/src/examples/read_only_memory_logup.rs @@ -8,7 +8,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -39,15 +39,6 @@ where F: IsFFTField + IsSubFieldOf + Send + Sync, E: IsField + Send + Sync, { - fn meta(&self) -> Vec { - // All three read the next row ⇒ 1 end exemption each. - vec![ - ConstraintMeta::base(0, 2).with_end_exemptions(1), // continuity - ConstraintMeta::base(1, 2).with_end_exemptions(1), // single value - ConstraintMeta::ext(2, 3).with_end_exemptions(1), // LogUp permutation - ] - } - fn eval>(&self, b: &mut B) { let a_sorted_0 = b.main(0, 2); let a_sorted_1 = b.main(1, 2); @@ -56,10 +47,19 @@ where let one = b.one(); let addr_diff = a_sorted_1 - a_sorted_0; - // (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address - b.emit_base(0, addr_diff.clone() * (addr_diff.clone() - one.clone())); - // (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 - b.emit_base(1, (v_sorted_1 - v_sorted_0) * (addr_diff - one)); + // All three read the next row ⇒ 1 end exemption each. + // idx 0 — continuity (degree 2): (a'_{i+1} - a'_i)(a'_{i+1} - a'_i - 1) = 0 where a' is the sorted address + b.emit_base_rows( + 0, + RowDomain::except_last(1), + addr_diff.clone() * (addr_diff.clone() - one.clone()), + ); + // idx 1 — single value (degree 2): (v'_{i+1} - v'_i) * (a'_{i+1} - a'_i - 1) = 0 + b.emit_base_rows( + 1, + RowDomain::except_last(1), + (v_sorted_1 - v_sorted_0) * (addr_diff - one), + ); // We are using the following LogUp equation: // s1 = s0 + m / sorted_term - 1/unsorted_term. @@ -76,8 +76,10 @@ where let m = b.main(1, 4); let unsorted_term = -(a1 + v1 * alpha.clone()) + z.clone(); let sorted_term = -(a_sorted_1 + v_sorted_1 * alpha) + z; - b.emit_ext( + // idx 2 — LogUp permutation (degree 3, 1 end exemption). + b.emit_ext_rows( 2, + RowDomain::except_last(1), s0 * unsorted_term.clone() * sorted_term.clone() + m * unsorted_term.clone() - sorted_term.clone() - s1 * unsorted_term * sorted_term, diff --git a/crypto/stark/src/examples/simple_addition.rs b/crypto/stark/src/examples/simple_addition.rs index c9eb019ff..d064acd55 100644 --- a/crypto/stark/src/examples/simple_addition.rs +++ b/crypto/stark/src/examples/simple_addition.rs @@ -36,16 +36,11 @@ impl ConstraintSet for SimpleAdditionConstraints where F: IsFFTField + Send + Sync, { - fn meta(&self) -> Vec { - // idx 0: col0 + col1 = col2, applied at every row (no exemptions). - vec![ConstraintMeta::base(0, 1)] - } - fn eval>(&self, b: &mut B) { let col0 = b.main(0, 0); let col1 = b.main(0, 1); let col2 = b.main(0, 2); - // Constraint: col0 + col1 - col2 = 0 + // idx 0: col0 + col1 - col2 = 0, applied at every row (degree 1, no exemptions). b.emit_base(0, col0 + col1 - col2); } } diff --git a/crypto/stark/src/examples/simple_fibonacci.rs b/crypto/stark/src/examples/simple_fibonacci.rs index bcb5ff5b6..db84ab439 100644 --- a/crypto/stark/src/examples/simple_fibonacci.rs +++ b/crypto/stark/src/examples/simple_fibonacci.rs @@ -2,7 +2,7 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, @@ -32,16 +32,12 @@ impl ConstraintSet for SimpleFibonacciConstraints where F: IsFFTField + Send + Sync, { - fn meta(&self) -> Vec { - // idx 0: a_{i+2} = a_{i+1} + a_i; reads two next rows ⇒ 2 end exemptions. - vec![ConstraintMeta::base(0, 1).with_end_exemptions(2)] - } - fn eval>(&self, b: &mut B) { let a0 = b.main(0, 0); let a1 = b.main(1, 0); let a2 = b.main(2, 0); - b.emit_base(0, a2 - a1 - a0); + // idx 0: a_{i+2} = a_{i+1} + a_i; reads two next rows ⇒ 2 end exemptions. + b.emit_base_rows(0, RowDomain::except_last(2), a2 - a1 - a0); } } diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index f06d45e01..cd41ac15a 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -825,8 +825,9 @@ pub struct AirWithBuses< /// The LogUp layout: the framework generates the LogUp (extension) /// constraints from this and appends them after the `constraint_set` ones. logup: LogUpLayout, - /// Idx-ordered metadata for all transition constraints: - /// `constraint_set.meta()` (base prefix) followed by `logup_meta` (ext). + /// Idx-ordered metadata for all transition constraints, DERIVED at + /// construction: `constraint_set.meta()` (base prefix) followed by the + /// LogUp emission's derived metadata (ext). meta: Vec, /// Number of base-field constraints (the `RootKind::Base` prefix length of /// `meta`) — these use the cheaper F×E accumulation path. @@ -879,12 +880,16 @@ impl< let logup = LogUpLayout::from_interactions(auxiliary_trace_build_data.interactions.clone()); let num_term_columns = logup.num_term_columns; - // meta = constraint_set base-prefix meta + appended LogUp ext meta. + // meta = constraint_set base-prefix meta + appended LogUp ext meta, + // both DERIVED by running the respective bodies through a MetaBuilder + // (the `{degree, end_exemptions}` declared at each emit). let mut meta = constraint_set.meta(); let num_base = num_base_from_meta(&meta); // The set is entirely base-field (its meta is a Base prefix). debug_assert_eq!(num_base, meta.len(), "constraint set meta must be all-base"); - meta.extend(logup_meta(&logup, meta.len())); + let mut logup_mb = crate::constraints::builder::MetaBuilder::new(); + emit_logup_constraints::(&mut logup_mb, &logup, num_base); + meta.extend(logup_mb.into_meta()); // Layout: num_committed_pairs term columns + 1 accumulated = ⌈N/2⌉ let num_aux_columns = if num_interactions > 0 { @@ -1007,7 +1012,14 @@ where } fn composition_poly_degree_bound(&self, trace_length: usize) -> usize { - let max_degree = self.meta.iter().map(|m| m.degree).max().unwrap_or(1); + // Only the per-table MAX degree is consumed. Base constraints declare it + // once via `ConstraintSet::max_degree()`; the framework's LogUp + // constraints contribute their own known max (batched terms degree 3, + // accumulator `1 + absorbed`). + let max_degree = self + .constraint_set + .max_degree() + .max(logup_max_degree(&self.logup)); // The composition polynomial is the constraint QUOTIENT H = Σ βᵢ·Cᵢ/Zᵢ. Its degree is // deg(Cᵢ) − deg(Zᵢ) = (max_degree−1)·N − max_degree + eᵢ, so with the end-exemptions // eᵢ < max_degree (the max-degree LogUp constraints have eᵢ = 0) it fits in @@ -1729,7 +1741,8 @@ where // accumulated); there are no per-constraint objects. // // All LogUp constraints use the default zerofier shape (every row, no -// exemptions), so [`logup_meta`] emits plain [`RootKind::Ext`] entries. +// exemptions) and are [`RootKind::Ext`]; their metadata is derived from this +// same emission (via `MetaBuilder`), not hand-listed. // // The data-dependent "skip the multiply when the row value is zero" // optimization IS reproduced, through the [`ConstraintBuilder::fold_fingerprint_term`] @@ -1744,7 +1757,7 @@ use crate::constraints::builder::ConstraintBuilder; /// computed by [`AirWithBuses::new`] from the interaction list (via /// `split_interactions`). This is the plain-data source for the LogUp /// constraints: [`emit_logup_constraints`] reads it to generate every LogUp -/// constraint, and [`logup_meta`] reads it for the metadata. +/// constraint (its metadata is derived from that same emission). #[derive(Clone)] pub struct LogUpLayout { /// All interactions, in the order they were registered. The first @@ -2042,7 +2055,8 @@ where -term_b }; - // c · fp_a · fp_b: c is aux (ext), so this is ext throughout. + // c · fp_a · fp_b: c is aux (ext), so this is ext throughout (degree 3; + // see `logup_max_degree`). let main = c * fp_a * fp_b; b.emit_ext(idx, main - term_a - term_b); } @@ -2097,9 +2111,28 @@ where _ => unreachable!("absorbed must contain 1 or 2 interactions"), }; + // Degree 1 + absorbed count (2 for one absorbed, 3 for two); folded into + // the composition bound via `logup_max_degree`. b.emit_ext(idx, root); } +/// The maximum degree among a layout's framework-generated LogUp constraints: +/// batched committed terms are degree 3, the accumulator is `1 + absorbed`. +/// Zero when there are no interactions. Folded into +/// `composition_poly_degree_bound` alongside the base constraints' max_degree. +pub fn logup_max_degree(layout: &LogUpLayout) -> usize { + if layout.interactions.is_empty() { + return 0; + } + // Accumulated constraint: 1 + number of absorbed interactions. + let mut m = 1 + layout.absorbed().len(); + // Batched committed terms (if any) are degree 3. + if layout.num_committed_pairs > 0 { + m = m.max(3); + } + m +} + /// Emit every LogUp transition constraint for `layout` through the builder, /// starting at absolute constraint index `idx_base` (the table's base-constraint /// count). Committed batched terms come first (one per committed pair), then the @@ -2121,26 +2154,6 @@ where emit_logup_accumulated::(b, layout, idx); } -/// The idx-ordered [`ConstraintMeta`] for `layout`'s LogUp constraints, starting -/// at `idx_start`: batched terms are degree 3; the accumulated constraint is -/// degree `1 + absorbed.len()` (2 for one absorbed, 3 for two). All are -/// [`RootKind::Ext`] with the default zerofier shape (period 1, offset 0, no -/// exemptions). -pub fn logup_meta(layout: &LogUpLayout, idx_start: usize) -> Vec { - let mut meta = Vec::with_capacity(layout.num_constraints()); - if layout.interactions.is_empty() { - return meta; - } - let mut idx = idx_start; - for _ in 0..layout.num_committed_pairs { - meta.push(ConstraintMeta::ext(idx, 3)); // c · fp_a · fp_b - idx += 1; - } - let absorbed_len = layout.absorbed().len(); - meta.push(ConstraintMeta::ext(idx, 1 + absorbed_len)); - meta -} - /// Run an [`AirWithBuses`] table's transition constraints through the /// [`ProverEvalFolder`] in ONE pass: the constraint set's base-field body /// followed by the appended LogUp constraints (idx offset by `num_base`, the @@ -2277,24 +2290,25 @@ mod logup_single_source_tests { let n_base = 0usize; // LogUp constraints are all extension-rooted. let n = layout.num_constraints(); - // Metadata self-consistency: all-ext, dense, and the batched/accumulated - // degree formula (3 per batched term; 1 + absorbed for the accumulator). - let meta = logup_meta(layout, n_base); + // Metadata self-consistency: derived from the LogUp emission itself + // (MetaBuilder), it must be all-ext, dense, and match the + // batched/accumulated degree formula (3 per batched term; 1 + absorbed + // for the accumulator). + let meta = { + let mut mb = crate::constraints::builder::MetaBuilder::new(); + emit_logup_constraints::(&mut mb, layout, n_base); + mb.into_meta() + }; assert_eq!(meta.len(), n, "[{label}] meta count"); let num_base = num_base_from_meta(&meta); assert_eq!(num_base, 0, "[{label}] LogUp meta is all-ext"); for (i, m) in meta.iter().enumerate() { assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); assert_eq!(m.kind, RootKind::Ext, "[{label}] meta kind {i}"); - let expected_degree = if i < layout.num_committed_pairs { - 3 - } else { - 1 + layout.absorbed().len() - }; - assert_eq!(m.degree, expected_degree, "[{label}] degree {i}"); } - // Capture once; tree-measured degree <= declared. + // Capture once; the tree-measured degree must match the batched/ + // accumulated formula, and `logup_max_degree` must equal their max. let mut cb = CaptureBuilder::::new(); emit_logup_constraints(&mut cb, layout, n_base); let (prog, degrees) = cb.finish(num_base); @@ -2309,12 +2323,18 @@ mod logup_single_source_tests { "[{label}] emitted constraint indices are not exactly 0..{n}: {emitted:?}" ); for &(idx, measured) in °rees { - assert!( - measured <= meta[idx].degree, - "[{label}] constraint {idx}: tree degree {measured} exceeds declared {}", - meta[idx].degree - ); + let expected_degree = if idx < layout.num_committed_pairs { + 3 + } else { + 1 + layout.absorbed().len() + }; + assert_eq!(measured, expected_degree, "[{label}] degree {idx}"); } + assert_eq!( + logup_max_degree(layout), + degrees.iter().map(|&(_, d)| d).max().unwrap_or(0), + "[{label}] logup_max_degree matches max measured degree" + ); let n_aux = num_aux_cols(layout); diff --git a/prover/src/constraints/cpu.rs b/prover/src/constraints/cpu.rs index f27070193..917445b7e 100644 --- a/prover/src/constraints/cpu.rs +++ b/prover/src/constraints/cpu.rs @@ -66,9 +66,9 @@ pub const NUM_CPU_CONSTRAINTS: usize = 12 + 6 + 2 + 2 + 2 + 4 + 2 + 2 + 1 + 2 + // compiled prover folder, the verifier folder and IR capture. All constraints // here use the default zerofier shape (every row, no exemptions). -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; -use super::templates::{INV_SHIFT_32, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta}; +use super::templates::{INV_SHIFT_32, emit_add_pair, emit_is_bit}; /// `col_a · col_b = 0`. pub fn emit_product_zero>( @@ -81,11 +81,6 @@ pub fn emit_product_zero ConstraintMeta { - ConstraintMeta::base(idx, 2) -} - /// `(1 − MEMORY − BRANCH) · read_register2 · imm[i] = 0`. pub fn emit_arg2_exclusive>( b: &mut B, @@ -100,11 +95,6 @@ pub fn emit_arg2_exclusive ConstraintMeta { - ConstraintMeta::base(idx, 3) -} - /// `(1 − MEMORY) · mem_flags · (1 − mem_flags) = 0`. pub fn emit_mem_flags_bit>( b: &mut B, @@ -119,11 +109,6 @@ pub fn emit_mem_flags_bit ConstraintMeta { - ConstraintMeta::base(idx, 3) -} - /// `(1 − flag) · value = 0`. pub fn emit_reg_not_read_is_zero>( b: &mut B, @@ -137,11 +122,6 @@ pub fn emit_reg_not_read_is_zero ConstraintMeta { - ConstraintMeta::base(idx, 2) -} - /// `arg2` multiplex for word index `word_idx ∈ {0, 1}`: /// /// ```text @@ -167,15 +147,10 @@ pub fn emit_arg2>( let expected = memory.clone() * imm.clone() + branch.clone() * rv2.clone() + (one - memory - branch) * (rv2 + imm); + // Degree 2 relies on the live `MEMORY·BRANCH = 0` mutex. b.emit_base(idx, arg2 - expected); } -/// Metadata for [`emit_arg2`] (degree 2 relies on the live `MEMORY·BRANCH = 0` -/// mutex). -pub fn arg2_meta(idx: usize) -> ConstraintMeta { - ConstraintMeta::base(idx, 2) -} - /// `cast(res, DWordWL)` word from the four `res` halves (DWordHL). fn res_word_expr>( b: &B, @@ -205,11 +180,6 @@ pub fn emit_rvd_eq_res ConstraintMeta { - ConstraintMeta::base(idx, 2) -} - /// The `pc + instruction_length` carry pair against a destination dword /// (`rvd` or `next_pc`), gated by `gate`; shared body of /// [`emit_branch_rvd_pair`] and [`emit_next_pc_add_pair`]: @@ -236,6 +206,7 @@ fn emit_pc_len_add_pair [ConstraintMeta; 2] { - [ - ConstraintMeta::base(idx, 3), - ConstraintMeta::base(idx + 1, 3), - ] -} - /// `branch_cond − (BRANCH·JALR + BRANCH·(1−JALR)·res[0])`. pub fn emit_branch_cond>( b: &mut B, @@ -278,11 +241,6 @@ pub fn emit_branch_cond ConstraintMeta { - ConstraintMeta::base(idx, 3) -} - /// `(1 − branch_cond) · carry · (1 − carry) = 0` for /// `next_pc = pc + instruction_length` (two instances at `idx`, `idx + 1`). pub fn emit_next_pc_add_pair>( @@ -295,14 +253,6 @@ pub fn emit_next_pc_add_pair [ConstraintMeta; 2] { - [ - ConstraintMeta::base(idx, 3), - ConstraintMeta::base(idx + 1, 3), - ] -} - // ========================================================================= // Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= @@ -326,62 +276,10 @@ pub fn next_pc_add_meta(idx: usize) -> [ConstraintMeta; 2] { pub struct CpuConstraints; impl ConstraintSet for CpuConstraints { - fn meta(&self) -> Vec { - let mut m = Vec::with_capacity(NUM_CPU_CONSTRAINTS); - // idx 0..11: IS_BIT on each BIT_FLAG_COLUMNS entry (unconditional). - for i in 0..BIT_FLAG_COLUMNS.len() { - m.push(is_bit_meta(i, false)); - } - let mut idx = BIT_FLAG_COLUMNS.len(); - // idx 12,13: ADD pair (conditional on ADD). - m.extend(add_pair_meta(idx, true)); - idx += 2; - // idx 14,15: SUB pair (conditional on SUB). - m.extend(add_pair_meta(idx, true)); - idx += 2; - // idx 16..21: word_instr mutexes + register-read gates. - for _ in 0..6 { - m.push(product_zero_meta(idx)); - idx += 1; - } - // idx 22,23: arg2 multiplex. - m.push(arg2_meta(idx)); - idx += 1; - m.push(arg2_meta(idx)); - idx += 1; - // idx 24..27: register zero-forcing. - for _ in 0..4 { - m.push(reg_not_read_is_zero_meta(idx)); - idx += 1; - } - // idx 28,29: rvd = cast(res, WL). - m.push(rvd_eq_res_meta(idx)); - idx += 1; - m.push(rvd_eq_res_meta(idx)); - idx += 1; - // idx 30,31: branch rvd = pc + len. - m.extend(branch_rvd_meta(idx)); - idx += 2; - // idx 32: branch_cond. - m.push(branch_cond_meta(idx)); - idx += 1; - // idx 33,34: next_pc = pc + len. - m.extend(next_pc_add_meta(idx)); - idx += 2; - // idx 35: MEMORY · BRANCH = 0. - m.push(product_zero_meta(idx)); - idx += 1; - // idx 36,37: arg2 exclusivity. - m.push(arg2_exclusive_meta(idx)); - idx += 1; - m.push(arg2_exclusive_meta(idx)); - idx += 1; - // idx 38: IS_BIT(mem_flags) on non-MEMORY rows. - m.push(mem_flags_bit_meta(idx)); - idx += 1; - debug_assert_eq!(idx, NUM_CPU_CONSTRAINTS); - debug_assert_eq!(m.len(), NUM_CPU_CONSTRAINTS); - m + // The conditional ADD/SUB carry pairs, arg2 exclusivity, mem-flags bit and + // branch constraints are degree 3. + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { diff --git a/prover/src/constraints/templates.rs b/prover/src/constraints/templates.rs index f8f0ec0ea..04932eab8 100644 --- a/prover/src/constraints/templates.rs +++ b/prover/src/constraints/templates.rs @@ -252,7 +252,7 @@ impl AddOperand { // zerofier shape — none of these templates override period/offset/ // exemptions). -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta}; +use stark::constraints::builder::ConstraintBuilder; /// IS_BIT: `x·(1−x)`, optionally gated by a condition column: /// `cond·x·(1−x)`. @@ -274,11 +274,6 @@ pub fn emit_is_bit>( b.emit_base(idx, root); } -/// Metadata for [`emit_is_bit`]: degree 3 gated, 2 ungated. -pub fn is_bit_meta(idx: usize, conditional: bool) -> ConstraintMeta { - ConstraintMeta::base(idx, if conditional { 3 } else { 2 }) -} - /// One [`AddLinearTerm`]: `column · coefficient` or a constant. fn add_term_expr>( b: &B, @@ -377,13 +372,3 @@ pub fn emit_add_pair> let root_1 = bit(b, c1, carry_1); b.emit_base(idx + 1, root_1); } - -/// Metadata for [`emit_add_pair`]: two constraints at `idx`, `idx + 1`; -/// degree 3 gated, 2 ungated. -pub fn add_pair_meta(idx: usize, conditional: bool) -> [ConstraintMeta; 2] { - let degree = if conditional { 3 } else { 2 }; - [ - ConstraintMeta::base(idx, degree), - ConstraintMeta::base(idx + 1, degree), - ] -} diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index 48f963fac..77092d0e4 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -42,9 +42,7 @@ use executor::vm::execution::Executor; use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; use math::field::element::FieldElement; use stark::config::Commitment; -use stark::constraints::builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, EmptyConstraints, -}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet, EmptyConstraints}; use stark::lookup::{AirWithBuses, AuxiliaryTraceBuildData, NullBoundaryConstraintBuilder}; use stark::proof::options::ProofOptions; use stark::proof::stark::MultiProof; @@ -115,10 +113,6 @@ fn global_transcript(elf_bytes: &[u8], num_epochs: usize) -> DefaultTranscript for L2gMemoryConstraints { - fn meta(&self) -> Vec { - // IS_BIT(MU), unconditional → degree 2. - vec![ConstraintMeta::base(0, 2)] - } fn eval>(&self, b: &mut B) { crate::constraints::templates::emit_is_bit(b, 0, local_to_global::cols::MU, None); } diff --git a/prover/src/tables/branch.rs b/prover/src/tables/branch.rs index d45618329..d4baf10c5 100644 --- a/prover/src/tables/branch.rs +++ b/prover/src/tables/branch.rs @@ -26,7 +26,7 @@ //! - Sender: IS_HALFWORD (×3 for next_pc_high[0..3]) //! - Receiver: BRANCH (provides branch targets to CPU) -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; @@ -413,14 +413,8 @@ fn carry_1_expr>( pub struct BranchConstraints; impl ConstraintSet for BranchConstraints { - fn meta(&self) -> Vec { - vec![ - ConstraintMeta::base(0, 3), // PcCarry0IsBit - ConstraintMeta::base(1, 3), // PcCarry1IsBit - ConstraintMeta::base(2, 3), // RegCarry0IsBit - ConstraintMeta::base(3, 3), // RegCarry1IsBit - ConstraintMeta::base(4, 2), // JalrIsBit - ] + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { diff --git a/prover/src/tables/commit.rs b/prover/src/tables/commit.rs index ce27d5d80..cd1ca264b 100644 --- a/prover/src/tables/commit.rs +++ b/prover/src/tables/commit.rs @@ -46,11 +46,9 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; -use crate::constraints::templates::{ - AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, -}; +use crate::constraints::templates::{AddOperand, emit_add_pair, emit_is_bit}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; @@ -737,18 +735,6 @@ pub fn bus_interactions() -> Vec { pub struct CommitConstraints; impl ConstraintSet for CommitConstraints { - fn meta(&self) -> Vec { - let mut m = vec![ - is_bit_meta(0, false), // first - is_bit_meta(1, false), // end - is_bit_meta(2, false), // μ - ConstraintMeta::base(3, 2), // (first + end)·(1 − μ) - ]; - m.extend(add_pair_meta(4, false)); // idx 4,5: address + 1 = address_incr - m.extend(add_pair_meta(6, false)); // idx 6,7: count_decr + 1 = count - m - } - fn eval>(&self, b: &mut B) { // idx 0-2: IS_BIT for first, end, mu emit_is_bit(b, 0, cols::FIRST, None); diff --git a/prover/src/tables/cpu32.rs b/prover/src/tables/cpu32.rs index 3786228ae..e0931ff3a 100644 --- a/prover/src/tables/cpu32.rs +++ b/prover/src/tables/cpu32.rs @@ -24,11 +24,9 @@ use super::types::{ BusId, FE, GoldilocksExtension, GoldilocksField, SHIFT_16, VmTable, alu_op, packed_decode_shrunk, }; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; -use crate::constraints::templates::{ - AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, -}; +use crate::constraints::templates::{AddOperand, emit_add_pair, emit_is_bit}; // ========================================================================= // Column indices for CPU32 table @@ -602,29 +600,8 @@ pub fn bus_interactions() -> Vec { pub struct Cpu32Constraints; impl ConstraintSet for Cpu32Constraints { - fn meta(&self) -> Vec { - let mut m = Vec::with_capacity(32); - // idx 0-6: IS_BIT (unconditional, degree 2). - for i in 0..7 { - m.push(is_bit_meta(i, false)); - } - // idx 7,8: ADD pair gated on ADD (conditional, degree 3). - m.extend(add_pair_meta(7, true)); - // idx 9,10: SUB pair gated on SUB (conditional, degree 3). - m.extend(add_pair_meta(9, true)); - // idx 11-16: RegZero (degree 2). - for i in 11..17 { - m.push(ConstraintMeta::base(i, 2)); - } - // idx 17-22: sign-extension arithmetic (linear, degree 1). - for i in 17..23 { - m.push(ConstraintMeta::base(i, 1)); - } - // idx 23-31: sign-zero, arg2-exclusive, flag ⇒ μ (all degree 2). - for i in 23..32 { - m.push(ConstraintMeta::base(i, 2)); - } - m + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { diff --git a/prover/src/tables/dvrm.rs b/prover/src/tables/dvrm.rs index 4a6f3d75b..032963c82 100644 --- a/prover/src/tables/dvrm.rs +++ b/prover/src/tables/dvrm.rs @@ -976,7 +976,7 @@ pub fn bus_interactions() -> Vec { // One body against the generic `ConstraintBuilder` serves the compiled prover // folder, the verifier folder and IR capture. Constraint indices 0..19. -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; /// DVRM table constraints as a single-source [`ConstraintSet`]. No column /// configuration is needed (the DVRM layout is fixed via `cols`). @@ -1059,11 +1059,6 @@ impl DvrmConstraints { } impl ConstraintSet for DvrmConstraints { - fn meta(&self) -> Vec { - // All DVRM constraints are declared degree 2. - (0..19).map(|i| ConstraintMeta::base(i, 2)).collect() - } - fn eval>(&self, b: &mut B) { // idx 0: SignedIsBit — signed * (1 - signed) let signed = b.main(0, cols::SIGNED); diff --git a/prover/src/tables/ec_scalar.rs b/prover/src/tables/ec_scalar.rs index da3eecf04..5589a14e5 100644 --- a/prover/src/tables/ec_scalar.rs +++ b/prover/src/tables/ec_scalar.rs @@ -276,18 +276,13 @@ pub fn bus_interactions() -> Vec { // One body against the generic `ConstraintBuilder` serves the compiled prover // folder, the verifier folder and IR capture. Constraint indices 0..20. -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; /// EC_SCALAR transition constraints as a single-source [`ConstraintSet`] (20 /// total). No column configuration needed (the layout is fixed via `cols`). pub struct EcScalarConstraints; impl ConstraintSet for EcScalarConstraints { - fn meta(&self) -> Vec { - // 10 unconditional IS_BIT (degree 2) + 10 MulZero (degree 2). - (0..20).map(|i| ConstraintMeta::base(i, 2)).collect() - } - fn eval>(&self, b: &mut B) { // idx 0..10: unconditional IS_BIT `x·(1−x)` for // [mu, limb_bit(0..8), last_limb], in that column order. Iterator diff --git a/prover/src/tables/ecdas.rs b/prover/src/tables/ecdas.rs index 8b30fbe08..26bbe44e4 100644 --- a/prover/src/tables/ecdas.rs +++ b/prover/src/tables/ecdas.rs @@ -269,7 +269,7 @@ pub enum Relation { // 4 : NEXT_OP · (1 − MU) // then for (Lambda,C0),(Xr,C1),(Yr,C2): 64 ConvCarry (i=0..64) + 1 ColIsZero. -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; /// ECDAS transition constraints as a single-source [`ConstraintSet`] (200 /// total). No column configuration needed (the layout is fixed via `cols`). @@ -405,31 +405,9 @@ impl EcdasConstraints { } impl ConstraintSet for EcdasConstraints { - fn meta(&self) -> Vec { - let mut m = Vec::with_capacity(200); - // idx 0,1,2: IS_BIT(MU/OP/NEXT_OP), degree 2. - for i in 0..3 { - m.push(ConstraintMeta::base(i, 2)); - } - // idx 3: OP·NEXT_OP, idx 4: NEXT_OP·(1−MU) — degree 2. - m.push(ConstraintMeta::base(3, 2)); - m.push(ConstraintMeta::base(4, 2)); - // Per relation: 64 ConvCarry + 1 ColIsZero. - let mut idx = 5; - for relation in [Relation::Lambda, Relation::Xr, Relation::Yr] { - let conv_degree = match relation { - Relation::Lambda => 3, // op · (λ · Δx) - Relation::Xr | Relation::Yr => 2, - }; - for _ in 0..64 { - m.push(ConstraintMeta::base(idx, conv_degree)); - idx += 1; - } - m.push(ConstraintMeta::base(idx, 1)); // ColIsZero c_63 - idx += 1; - } - debug_assert_eq!(m.len(), 200); - m + // The Lambda ConvCarry has the op·(λ·Δx) term, making it degree 3. + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { @@ -464,7 +442,7 @@ impl ConstraintSet for EcdasConstraints { idx += 1; } let c_last = b.main(0, c_base + 63); - b.emit_base(idx, c_last); + b.emit_base(idx, c_last); // ColIsZero c_63 idx += 1; } } diff --git a/prover/src/tables/ecsm.rs b/prover/src/tables/ecsm.rs index cc87224e4..0dba13910 100644 --- a/prover/src/tables/ecsm.rs +++ b/prover/src/tables/ecsm.rs @@ -641,7 +641,7 @@ impl OverflowKind { // 140..147 : CarryBit(XrLtP, 0..7) // 147 : OverflowRequired(XrLtP) -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; /// ECSM transition constraints as a single-source [`ConstraintSet`] (148 /// total). No column configuration needed (the layout is fixed via `cols`). @@ -760,54 +760,20 @@ impl EcsmConstraints { } impl ConstraintSet for EcsmConstraints { - fn meta(&self) -> Vec { - let mut m = Vec::with_capacity(148); - m.push(ConstraintMeta::base(0, 2)); // IS_BIT(MU) - let mut idx = 1; - // X2 convolution: 64 carries (deg 2) + closing (deg 1). - for _ in 0..64 { - m.push(ConstraintMeta::base(idx, 2)); - idx += 1; - } - m.push(ConstraintMeta::base(idx, 1)); - idx += 1; - // Yg convolution: 64 carries (deg 2) + closing (deg 1). - for _ in 0..64 { - m.push(ConstraintMeta::base(idx, 2)); - idx += 1; - } - m.push(ConstraintMeta::base(idx, 1)); - idx += 1; - // IS_BIT(q1[32]) (deg 2). - m.push(ConstraintMeta::base(idx, 2)); - idx += 1; - // k < N: 7 carry bits (deg 3) + overflow-required (deg 2). - for _ in 0..7 { - m.push(ConstraintMeta::base(idx, 3)); - idx += 1; - } - m.push(ConstraintMeta::base(idx, 2)); - idx += 1; - // xR < p: 7 carry bits (deg 3) + overflow-required (deg 2). - for _ in 0..7 { - m.push(ConstraintMeta::base(idx, 3)); - idx += 1; - } - m.push(ConstraintMeta::base(idx, 2)); - idx += 1; - debug_assert_eq!(idx, 148); - m + // The k usize { + 3 } fn eval>(&self, b: &mut B) { - // idx 0: IS_BIT(MU): mu·(1−mu). + // idx 0: IS_BIT(MU): mu·(1−mu). (deg 2) let mu = b.main(0, cols::MU); let one = b.one(); b.emit_base(0, mu.clone() * (one - mu)); let mut idx = 1; - // X2 convolution: 64 carries + closing c0(63). + // X2 convolution: 64 carries (deg 2) + closing c0(63) (deg 1). for i in 0..64 { let root = Self::conv_carry(b, Relation::X2, i); b.emit_base(idx, root); @@ -817,7 +783,7 @@ impl ConstraintSet for EcsmConstraints { b.emit_base(idx, c0_last); idx += 1; - // Yg convolution: 64 carries + closing c1(63). + // Yg convolution: 64 carries (deg 2) + closing c1(63) (deg 1). for i in 0..64 { let root = Self::conv_carry(b, Relation::Yg, i); b.emit_base(idx, root); @@ -827,13 +793,13 @@ impl ConstraintSet for EcsmConstraints { b.emit_base(idx, c1_last); idx += 1; - // idx 131: IS_BIT(q1[32]): x·(1−x). + // idx 131: IS_BIT(q1[32]): x·(1−x). (deg 2) let q1_32 = b.main(0, cols::q1(32)); let one = b.one(); b.emit_base(idx, q1_32.clone() * (one - q1_32)); idx += 1; - // k < N and xR < p: 7 carry bits + overflow-required each. + // k < N and xR < p: 7 carry bits (deg 3) + overflow-required (deg 2) each. for kind in [OverflowKind::KLtN, OverflowKind::XrLtP] { let c = Self::carry_chain(b, kind); for ci in c.iter().take(7) { diff --git a/prover/src/tables/eq.rs b/prover/src/tables/eq.rs index 945dfd094..117d8426b 100644 --- a/prover/src/tables/eq.rs +++ b/prover/src/tables/eq.rs @@ -24,12 +24,10 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::{ - AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, -}; +use crate::constraints::templates::{AddOperand, emit_add_pair, emit_is_bit}; // ========================================================================= // Column indices for EQ table @@ -255,13 +253,6 @@ pub fn bus_interactions() -> Vec { pub struct EqConstraints; impl ConstraintSet for EqConstraints { - fn meta(&self) -> Vec { - let mut m = add_pair_meta(0, false).to_vec(); // idx 0,1: b + diff = a - m.push(is_bit_meta(2, false)); // idx 2: IS_BIT(invert) - m.push(ConstraintMeta::base(3, 2)); // idx 3: res = eq XOR invert - m - } - fn eval>(&self, b: &mut B) { // diff = a - b, encoded as b + diff = a (unconditional). emit_add_pair( diff --git a/prover/src/tables/keccak.rs b/prover/src/tables/keccak.rs index 31cc4b824..832869012 100644 --- a/prover/src/tables/keccak.rs +++ b/prover/src/tables/keccak.rs @@ -19,7 +19,7 @@ use executor::vm::instruction::execution::KECCAK_SYSCALL_NUMBER; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; use crate::constraints::templates::{AddOperand, INV_SHIFT_32}; @@ -464,17 +464,8 @@ pub fn bus_interactions() -> Vec { pub struct KeccakConstraints; impl ConstraintSet for KeccakConstraints { - fn meta(&self) -> Vec { - let mut m = Vec::with_capacity(51); - for lane_idx in 0..25 { - // ADD pair is conditional on μ → degree 3. - m.extend(crate::constraints::templates::add_pair_meta( - lane_idx * 2, - true, - )); - } - m.push(ConstraintMeta::base(50, 2)); // μ · carry_1 - m + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { diff --git a/prover/src/tables/keccak_rnd.rs b/prover/src/tables/keccak_rnd.rs index a69acb763..30a50e0b2 100644 --- a/prover/src/tables/keccak_rnd.rs +++ b/prover/src/tables/keccak_rnd.rs @@ -29,7 +29,7 @@ //! produces a single-bit carry, range-checked via IS_BIT polynomial constraints. use executor::vm::instruction::execution::{KECCAK_RC, KECCAK_RHO}; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; @@ -906,11 +906,9 @@ pub fn bus_interactions() -> Vec { pub struct KeccakRndConstraints; impl ConstraintSet for KeccakRndConstraints { - fn meta(&self) -> Vec { - // 20 conditional IS_BIT constraints → degree 3. - (0..20) - .map(|i| crate::constraints::templates::is_bit_meta(i, true)) - .collect() + // The IS_BIT constraints are gated by μ (cond·x·(1−x)), so degree 3. + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { diff --git a/prover/src/tables/load.rs b/prover/src/tables/load.rs index 7b2f8e833..c2bf389dc 100644 --- a/prover/src/tables/load.rs +++ b/prover/src/tables/load.rs @@ -481,7 +481,7 @@ pub fn bus_interactions() -> Vec { // 5: ReadImpliesMu 6..10: ExtensionHigh(4..8) // 10..12: ExtensionMid(2..4) 12: ExtensionLow -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; /// LOAD table constraints as a single-source [`ConstraintSet`]. No column /// configuration is needed (the LOAD layout is fixed via `cols`). @@ -513,22 +513,8 @@ impl LoadConstraints { } impl ConstraintSet for LoadConstraints { - fn meta(&self) -> Vec { - vec![ - ConstraintMeta::base(0, 2), // FlagIsBit(SIGNED) - ConstraintMeta::base(1, 2), // FlagIsBit(READ2) - ConstraintMeta::base(2, 2), // FlagIsBit(READ4) - ConstraintMeta::base(3, 2), // FlagIsBit(READ8) - ConstraintMeta::base(4, 2), // WidthSumIsBit - ConstraintMeta::base(5, 2), // ReadImpliesMu - ConstraintMeta::base(6, 3), // ExtensionHigh(4) - ConstraintMeta::base(7, 3), // ExtensionHigh(5) - ConstraintMeta::base(8, 3), // ExtensionHigh(6) - ConstraintMeta::base(9, 3), // ExtensionHigh(7) - ConstraintMeta::base(10, 3), // ExtensionMid(2) - ConstraintMeta::base(11, 3), // ExtensionMid(3) - ConstraintMeta::base(12, 3), // ExtensionLow (res[1]) - ] + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { diff --git a/prover/src/tables/lt.rs b/prover/src/tables/lt.rs index b4032b8e2..a68191f37 100644 --- a/prover/src/tables/lt.rs +++ b/prover/src/tables/lt.rs @@ -352,7 +352,7 @@ pub fn bus_interactions() -> Vec { // One body against the generic `ConstraintBuilder` serves the compiled prover // folder, the verifier folder and IR capture. Constraint indices 0..6. -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; /// LT table constraints as a single-source [`ConstraintSet`]. No column /// configuration is needed (the LT layout is fixed via `cols`). @@ -398,15 +398,9 @@ impl LtConstraints { } impl ConstraintSet for LtConstraints { - fn meta(&self) -> Vec { - vec![ - ConstraintMeta::base(0, 2), // Carry0IsBit - ConstraintMeta::base(1, 2), // Carry1IsBit - ConstraintMeta::base(2, 3), // LtFormula - ConstraintMeta::base(3, 2), // OutXorInvert - ConstraintMeta::base(4, 2), // InvertIsBit - ConstraintMeta::base(5, 2), // SignedIsBit - ] + // The LT formula (idx 2) is degree 3; the rest are degree 2. + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { diff --git a/prover/src/tables/memw.rs b/prover/src/tables/memw.rs index 30e27fb9a..4f775f535 100644 --- a/prover/src/tables/memw.rs +++ b/prover/src/tables/memw.rs @@ -33,7 +33,7 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; use crate::constraints::templates::emit_is_bit; @@ -860,14 +860,6 @@ fn w2_expr>(b: &B) -> pub struct MemwConstraints; impl ConstraintSet for MemwConstraints { - fn meta(&self) -> Vec { - let mut m = Vec::with_capacity(15); - for i in 0..15 { - m.push(ConstraintMeta::base(i, 2)); - } - m - } - fn eval>(&self, b: &mut B) { // idx 0: IS_BIT<μ_sum> = μ_sum * (1 - μ_sum) let one = b.one(); diff --git a/prover/src/tables/memw_aligned.rs b/prover/src/tables/memw_aligned.rs index ec8b8832c..b9517ec91 100644 --- a/prover/src/tables/memw_aligned.rs +++ b/prover/src/tables/memw_aligned.rs @@ -39,7 +39,7 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use super::memw::MemwOperation; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; @@ -670,10 +670,6 @@ fn w2_expr>(b: &B) -> pub struct MemwAlignedConstraints; impl ConstraintSet for MemwAlignedConstraints { - fn meta(&self) -> Vec { - (0..8).map(|i| ConstraintMeta::base(i, 2)).collect() - } - fn eval>(&self, b: &mut B) { // idx 0: IS_BIT<μ_sum> = μ_sum * (1 - μ_sum) let one = b.one(); diff --git a/prover/src/tables/memw_register.rs b/prover/src/tables/memw_register.rs index 5e2e8ba76..c02380c5f 100644 --- a/prover/src/tables/memw_register.rs +++ b/prover/src/tables/memw_register.rs @@ -41,7 +41,7 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use super::memw::MemwOperation; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; @@ -367,10 +367,6 @@ pub fn bus_interactions() -> Vec { pub struct MemwRegisterConstraints; impl ConstraintSet for MemwRegisterConstraints { - fn meta(&self) -> Vec { - (0..3).map(|i| ConstraintMeta::base(i, 2)).collect() - } - fn eval>(&self, b: &mut B) { // idx 0,1: IS_BIT<μ_read>, IS_BIT<μ_write> emit_is_bit(b, 0, cols::MU_READ, None); diff --git a/prover/src/tables/mul.rs b/prover/src/tables/mul.rs index 08f1c749c..2f0fa1d0e 100644 --- a/prover/src/tables/mul.rs +++ b/prover/src/tables/mul.rs @@ -689,7 +689,7 @@ pub fn bus_interactions() -> Vec { // 2: LhsSign 3: RhsSign // 4..8: RawProduct(0..4) -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; /// MUL table constraints as a single-source [`ConstraintSet`]. No column /// configuration is needed (the MUL layout is fixed via `cols`). @@ -787,19 +787,6 @@ impl MulConstraints { } impl ConstraintSet for MulConstraints { - fn meta(&self) -> Vec { - vec![ - ConstraintMeta::base(0, 2), // SignedIsBit(LHS_SIGNED) - ConstraintMeta::base(1, 2), // SignedIsBit(RHS_SIGNED) - ConstraintMeta::base(2, 2), // LhsSign - ConstraintMeta::base(3, 2), // RhsSign - ConstraintMeta::base(4, 2), // RawProduct(0) - ConstraintMeta::base(5, 2), // RawProduct(1) - ConstraintMeta::base(6, 2), // RawProduct(2) - ConstraintMeta::base(7, 2), // RawProduct(3) - ] - } - fn eval>(&self, b: &mut B) { // idx 0,1: IS_BIT range checks on the sign-flag multiplicities. let is_bit_lhs = Self::signed_is_bit(b, cols::LHS_SIGNED); diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index 202b67033..77a8ae32a 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -735,7 +735,7 @@ pub const NUM_SHIFT_CONSTRAINTS: usize = 19; // One body against the generic `ConstraintBuilder` serves the compiled prover // folder, the verifier folder and IR capture. Constraint indices 0..19. -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; /// SHIFT table constraints as a single-source [`ConstraintSet`]. No column /// configuration is needed (the SHIFT layout is fixed via `cols`). @@ -829,27 +829,8 @@ impl ShiftConstraints { } impl ConstraintSet for ShiftConstraints { - fn meta(&self) -> Vec { - let mut m = Vec::with_capacity(NUM_SHIFT_CONSTRAINTS); - m.push(ConstraintMeta::base(0, 2)); // DirectionImpliesMu - for i in 0..4 { - m.push(ConstraintMeta::base(1 + i, 3)); // ZbsOverrideX - } - m.push(ConstraintMeta::base(5, 2)); // ZbsOverrideX4 - for i in 0..4 { - m.push(ConstraintMeta::base(6 + i, 3)); // ZbsOverrideY - } - for i in 0..4 { - m.push(ConstraintMeta::base(10 + i, 2)); // LimbShiftIsBit - } - for i in 0..2 { - m.push(ConstraintMeta::base(14 + i, 3)); // OutputMatchesShifted - } - for i in 0..3 { - m.push(ConstraintMeta::base(16 + i, 2)); // FlagIsBit - } - debug_assert_eq!(m.len(), NUM_SHIFT_CONSTRAINTS); - m + fn max_degree(&self) -> usize { + 3 } fn eval>(&self, b: &mut B) { diff --git a/prover/src/tables/store.rs b/prover/src/tables/store.rs index 044f97662..c1dfc937a 100644 --- a/prover/src/tables/store.rs +++ b/prover/src/tables/store.rs @@ -22,10 +22,10 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; -use stark::constraints::builder::{ConstraintBuilder, ConstraintMeta, ConstraintSet}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; -use crate::constraints::templates::{emit_is_bit, is_bit_meta}; +use crate::constraints::templates::emit_is_bit; // ========================================================================= // Column indices for STORE table @@ -261,17 +261,6 @@ pub fn bus_interactions() -> Vec { pub struct StoreConstraints; impl ConstraintSet for StoreConstraints { - fn meta(&self) -> Vec { - vec![ - is_bit_meta(0, false), // write2 - is_bit_meta(1, false), // write4 - is_bit_meta(2, false), // write8 - is_bit_meta(3, false), // μ - ConstraintMeta::base(4, 2), // width sum is bit - ConstraintMeta::base(5, 2), // width ⇒ μ - ] - } - fn eval>(&self, b: &mut B) { emit_is_bit(b, 0, cols::WRITE2, None); emit_is_bit(b, 1, cols::WRITE4, None); diff --git a/prover/src/tests/branch_constraints_tests.rs b/prover/src/tests/branch_constraints_tests.rs index 950b21cb9..ed9001ec5 100644 --- a/prover/src/tests/branch_constraints_tests.rs +++ b/prover/src/tests/branch_constraints_tests.rs @@ -22,10 +22,8 @@ fn test_branch_constraint_set_meta() { for (i, m) in meta.iter().enumerate() { assert_eq!(m.constraint_idx, i); } - for m in &meta[..4] { - assert_eq!(m.degree, 3); - } - assert_eq!(meta[4].degree, 2); + // 4 conditional carry IS_BIT constraints are degree 3, so the table max is 3. + assert_eq!(BranchConstraints.max_degree(), 3); } // ========================================================================= diff --git a/prover/src/tests/commit_tests.rs b/prover/src/tests/commit_tests.rs index 876bdc04b..fdaf4d2cd 100644 --- a/prover/src/tests/commit_tests.rs +++ b/prover/src/tests/commit_tests.rs @@ -438,9 +438,10 @@ fn test_constraints_count_and_indices() { use stark::constraints::builder::ConstraintSet; let meta = CommitConstraints.meta(); assert_eq!(meta.len(), 8); - // Dense, idx-ordered, all degree 2 (unconditional). + // Dense, idx-ordered. for (i, m) in meta.iter().enumerate() { assert_eq!(m.constraint_idx, i); - assert_eq!(m.degree, 2); } + // All constraints are degree 2 (unconditional). + assert_eq!(CommitConstraints.max_degree(), 2); } diff --git a/prover/src/tests/constraint_emit_tests.rs b/prover/src/tests/constraint_emit_tests.rs index 21e9e8fb4..64f03da51 100644 --- a/prover/src/tests/constraint_emit_tests.rs +++ b/prover/src/tests/constraint_emit_tests.rs @@ -12,24 +12,18 @@ use math::field::element::FieldElement; use stark::constraint_ir::eval_program_base; use stark::constraints::builder::{ - CaptureBuilder, ConstraintBuilder, ConstraintMeta, ProverEvalFolder, RootKind, - VerifierEvalFolder, num_base_from_meta, + CaptureBuilder, ConstraintBuilder, MetaBuilder, ProverEvalFolder, RootKind, VerifierEvalFolder, + num_base_from_meta, }; use stark::frame::Frame; use stark::table::TableView; use stark::traits::TransitionEvaluationContext; -use crate::constraints::cpu::{ - arg2_exclusive_meta, arg2_meta, branch_cond_meta, branch_rvd_meta, mem_flags_bit_meta, - next_pc_add_meta, product_zero_meta, reg_not_read_is_zero_meta, rvd_eq_res_meta, -}; use crate::constraints::cpu::{ emit_arg2, emit_arg2_exclusive, emit_branch_cond, emit_branch_rvd_pair, emit_mem_flags_bit, emit_next_pc_add_pair, emit_product_zero, emit_reg_not_read_is_zero, emit_rvd_eq_res, }; -use crate::constraints::templates::{ - AddLinearTerm, AddOperand, add_pair_meta, emit_add_pair, emit_is_bit, is_bit_meta, -}; +use crate::constraints::templates::{AddLinearTerm, AddOperand, emit_add_pair, emit_is_bit}; use crate::tables::cpu::cols; use crate::tables::types::{FE, GoldilocksExtension, GoldilocksField}; @@ -78,18 +72,23 @@ macro_rules! emit_body { /// this pins that capture/interpretation stays faithful to the compiled folder. /// `meta` must be dense, idx-ordered, all-base, with each declared degree equal /// to the tree-measured degree. -fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { +fn check_emit(label: &str, body: &T, max_degree: usize) { let n = body.n(); - assert_eq!(meta.len(), n, "[{label}] meta length"); - // --- meta invariants --- - assert_eq!(num_base_from_meta(meta), n, "[{label}] all-base num_base"); + // --- meta invariants (DERIVED from the body): dense, idx-ordered, all-base --- + let meta = { + let mut mb = MetaBuilder::new(); + body.eval(&mut mb); + mb.into_meta() + }; + assert_eq!(meta.len(), n, "[{label}] meta length"); + assert_eq!(num_base_from_meta(&meta), n, "[{label}] all-base num_base"); for (i, m) in meta.iter().enumerate() { assert_eq!(m.constraint_idx, i, "[{label}] meta idx {i}"); assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); } - // --- capture once; tree-measured degree == declared --- + // --- capture once; tree-measured degree matches the declared max --- let mut cb = CaptureBuilder::::new(); body.eval(&mut cb); let (prog, degrees) = cb.finish(n); @@ -104,13 +103,18 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { emitted.iter().enumerate().all(|(i, &idx)| i == idx), "[{label}] emitted constraint indices are not exactly 0..{n}: {emitted:?}" ); - for &(idx, measured) in °rees { - assert_eq!( - measured, meta[idx].degree, - "[{label}] constraint {idx}: tree degree {measured} != declared {}", - meta[idx].degree + let mut max_measured = 0; + for &(_, measured) in °rees { + assert!( + measured <= max_degree, + "[{label}] tree degree {measured} EXCEEDS declared max {max_degree}" ); + max_measured = max_measured.max(measured); } + assert_eq!( + max_measured, max_degree, + "[{label}] max tree-measured degree {max_measured} != declared {max_degree}" + ); let no_ch: Vec = vec![]; let offset_e = Fp3::zero(); @@ -167,10 +171,10 @@ fn check_emit(label: &str, body: &T, meta: &[ConstraintMeta]) { #[test] fn emit_is_bit_folder_capture_agree() { emit_body!(Uncond, 1, |b| { emit_is_bit(b, 0, 7, None) }); - check_emit("is_bit_unconditional", &Uncond, &[is_bit_meta(0, false)]); + check_emit("is_bit_unconditional", &Uncond, 2); emit_body!(Cond, 1, |b| { emit_is_bit(b, 0, 5, Some(3)) }); - check_emit("is_bit_conditional", &Cond, &[is_bit_meta(0, true)]); + check_emit("is_bit_conditional", &Cond, 3); } // ============================================================================= @@ -179,7 +183,8 @@ fn emit_is_bit_folder_capture_agree() { /// Run the pair check for one `conditional` flag. fn check_add_pair_case(label: &str, body: &T, conditional: bool) { - check_emit(label, body, &add_pair_meta(0, conditional)); + let max_degree = if conditional { 3 } else { 2 }; + check_emit(label, body, max_degree); } #[test] @@ -250,22 +255,22 @@ fn emit_add_pair_multi_cond_bytes() { #[test] fn emit_product_zero_folder_capture_agree() { emit_body!(Body, 1, |b| { emit_product_zero(b, 0, 12, 17) }); - check_emit("product_zero", &Body, &[product_zero_meta(0)]); + check_emit("product_zero", &Body, 2); } #[test] fn emit_arg2_exclusive_folder_capture_agree() { emit_body!(Body0, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_0) }); - check_emit("arg2_exclusive_imm0", &Body0, &[arg2_exclusive_meta(0)]); + check_emit("arg2_exclusive_imm0", &Body0, 3); emit_body!(Body1, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_1) }); - check_emit("arg2_exclusive_imm1", &Body1, &[arg2_exclusive_meta(0)]); + check_emit("arg2_exclusive_imm1", &Body1, 3); } #[test] fn emit_mem_flags_bit_folder_capture_agree() { emit_body!(Body, 1, |b| { emit_mem_flags_bit(b, 0) }); - check_emit("mem_flags_bit", &Body, &[mem_flags_bit_meta(0)]); + check_emit("mem_flags_bit", &Body, 3); } #[test] @@ -273,20 +278,12 @@ fn emit_reg_not_read_is_zero_folder_capture_agree() { emit_body!(Body, 1, |b| { emit_reg_not_read_is_zero(b, 0, cols::READ_REGISTER1, cols::RV1_0) }); - check_emit( - "reg_not_read_is_zero_rv1", - &Body, - &[reg_not_read_is_zero_meta(0)], - ); + check_emit("reg_not_read_is_zero_rv1", &Body, 2); emit_body!(Body2, 1, |b| { emit_reg_not_read_is_zero(b, 0, cols::READ_REGISTER2, cols::RV2_1) }); - check_emit( - "reg_not_read_is_zero_rv2", - &Body2, - &[reg_not_read_is_zero_meta(0)], - ); + check_emit("reg_not_read_is_zero_rv2", &Body2, 2); } // ============================================================================= @@ -296,33 +293,33 @@ fn emit_reg_not_read_is_zero_folder_capture_agree() { #[test] fn emit_arg2_folder_capture_agree() { emit_body!(Body0, 1, |b| { emit_arg2(b, 0, 0) }); - check_emit("arg2_word0", &Body0, &[arg2_meta(0)]); + check_emit("arg2_word0", &Body0, 2); emit_body!(Body1, 1, |b| { emit_arg2(b, 0, 1) }); - check_emit("arg2_word1", &Body1, &[arg2_meta(0)]); + check_emit("arg2_word1", &Body1, 2); } #[test] fn emit_rvd_eq_res_folder_capture_agree() { emit_body!(Body0, 1, |b| { emit_rvd_eq_res(b, 0, 0) }); - check_emit("rvd_eq_res_word0", &Body0, &[rvd_eq_res_meta(0)]); + check_emit("rvd_eq_res_word0", &Body0, 2); emit_body!(Body1, 1, |b| { emit_rvd_eq_res(b, 0, 1) }); - check_emit("rvd_eq_res_word1", &Body1, &[rvd_eq_res_meta(0)]); + check_emit("rvd_eq_res_word1", &Body1, 2); } #[test] fn emit_branch_rvd_pair_folder_capture_agree() { emit_body!(Body, 2, |b| { emit_branch_rvd_pair(b, 0) }); - check_emit("branch_rvd_pair", &Body, &branch_rvd_meta(0)); + check_emit("branch_rvd_pair", &Body, 3); } #[test] fn emit_branch_cond_folder_capture_agree() { emit_body!(Body, 1, |b| { emit_branch_cond(b, 0) }); - check_emit("branch_cond", &Body, &[branch_cond_meta(0)]); + check_emit("branch_cond", &Body, 3); } #[test] fn emit_next_pc_add_pair_folder_capture_agree() { emit_body!(Body, 2, |b| { emit_next_pc_add_pair(b, 0) }); - check_emit("next_pc_add_pair", &Body, &next_pc_add_meta(0)); + check_emit("next_pc_add_pair", &Body, 3); } diff --git a/prover/src/tests/constraint_set_tests_a.rs b/prover/src/tests/constraint_set_tests_a.rs index fafaedfe9..89a75864a 100644 --- a/prover/src/tests/constraint_set_tests_a.rs +++ b/prover/src/tests/constraint_set_tests_a.rs @@ -63,18 +63,15 @@ where assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); } - // --- capture once; tree-measured degree <= declared --- + // --- capture once; tree-measured degree <= the table's declared max --- // - // The declared `meta.degree` is what the engine uses as the composition-poly - // degree bound. For most tables that bound is tight, so tree-measured == - // declared. A few constraints (the ecsm/ecdas convolution TAILS — `ConvCarry` - // at large `i`) legitimately have a lower EXACT degree than their uniform - // declared bound: at limb `i` near the top, every remaining product has a - // zeroed (constant) factor, so the surviving expression is degree 1 while the - // meta declares 2 (resp. 3). The soundness-relevant invariant is therefore - // `measured <= declared` (the real degree must never EXCEED the bound the - // composition polynomial is sized for) — over-declaration is safe, - // under-declaration is not. + // `max_degree()` is what the engine uses as the composition-poly degree + // bound. Most constraints hit it; a few (the ecsm/ecdas convolution TAILS — + // `ConvCarry` at large `i`) legitimately have a lower EXACT degree (a zeroed + // factor drops the surviving product). The soundness-relevant invariant is + // therefore `measured <= max_degree()` — the real degree must never EXCEED + // the bound the composition polynomial is sized for; over-declaration is + // safe, under-declaration is not. let mut cb = CaptureBuilder::::new(); set.eval(&mut cb); let (prog, degrees) = cb.finish(num_base); @@ -89,11 +86,11 @@ where emitted.iter().enumerate().all(|(i, &idx)| i == idx), "[{label}] emitted constraint indices are not exactly 0..{n}: {emitted:?}" ); + let max_degree = set.max_degree(); for &(idx, measured) in °rees { assert!( - measured <= meta[idx].degree, - "[{label}] constraint {idx}: tree degree {measured} EXCEEDS declared {}", - meta[idx].degree + measured <= max_degree, + "[{label}] constraint {idx}: tree degree {measured} EXCEEDS max_degree() {max_degree}" ); } let no_ch: Vec = vec![]; diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index 87f1d3101..0348c2b70 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -67,7 +67,7 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz assert_eq!(m.kind, RootKind::Base, "[{label}] meta kind {i}"); } - // --- capture once; tree-measured degree == declared --- + // --- capture once; tree-measured degree <= the table's declared max --- let mut cb = CaptureBuilder::::new(); set.eval(&mut cb); let (prog, degrees) = cb.finish(n); @@ -82,11 +82,11 @@ fn check_table>(label: &str, set: &CS, num_cols: usiz emitted.iter().enumerate().all(|(i, &idx)| i == idx), "[{label}] emitted constraint indices are not exactly 0..{n}: {emitted:?}" ); + let max_degree = set.max_degree(); for &(idx, measured) in °rees { - assert_eq!( - measured, meta[idx].degree, - "[{label}] constraint {idx}: tree degree {measured} != declared {}", - meta[idx].degree + assert!( + measured <= max_degree, + "[{label}] constraint {idx}: tree degree {measured} EXCEEDS max_degree() {max_degree}" ); } let no_ch: Vec = vec![];