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/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()] } diff --git a/crypto/stark/Cargo.toml b/crypto/stark/Cargo.toml index 3a3b95068..9e90e789e 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"] } @@ -78,6 +79,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..58afa0d5f --- /dev/null +++ b/crypto/stark/examples/examples_cli.rs @@ -0,0 +1,709 @@ +//! 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 +//! (`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. + +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(&[]), + #[cfg(feature = "disk-spill")] + stark::storage_mode::StorageMode::Ram, + ) + .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/constraint_ir/builder.rs b/crypto/stark/src/constraint_ir/builder.rs new file mode 100644 index 000000000..57d09e2bd --- /dev/null +++ b/crypto/stark/src/constraint_ir/builder.rs @@ -0,0 +1,286 @@ +//! 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, +} + +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(), + }; + // 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 + } + + /// 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 { + assert!( + u16::try_from(col).is_ok(), + "column {col} exceeds the IR's u16 index" + ); + 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 { + assert!( + u16::try_from(col).is_ok(), + "column {col} exceeds the IR's u16 index" + ); + self.push( + Op::Var { + main: false, + offset, + row: 0, + col: col as u16, + }, + Dim::Ext, + ) + } + + /// 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) + } + + /// 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. + /// + /// 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) + } + + /// 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`]). + /// + /// 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) + } + + /// 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, + } + } +} diff --git a/crypto/stark/src/constraint_ir/interp.rs b/crypto/stark/src/constraint_ir/interp.rs new file mode 100644 index 000000000..a03044066 --- /dev/null +++ b/crypto/stark/src/constraint_ir/interp.rs @@ -0,0 +1,257 @@ +//! 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; 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_challenge: FChallenge, + resolve_alpha: FAlpha, + resolve_offset: FOffset, +) -> Vec> +where + F: IsSubFieldOf, + E: IsField, + FVar: Fn(bool, u8, u8, 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::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!("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 { + rows, + 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| { + debug_assert_eq!(row, 0, "tables read row 0 of each frame step"); + if main { + Value::Base(rows.main(offset as usize, col as usize).clone()) + } else { + Value::Ext(rows.aux(offset as usize, col 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, + 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| 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..cc770fd06 --- /dev/null +++ b/crypto/stark/src/constraint_ir/ir.rs @@ -0,0 +1,114 @@ +//! 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 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, +} + +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..258aa23b7 --- /dev/null +++ b/crypto/stark/src/constraint_ir/mod.rs @@ -0,0 +1,33 @@ +//! 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; + +#[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..4950bfc31 --- /dev/null +++ b/crypto/stark/src/constraint_ir/tests.rs @@ -0,0 +1,526 @@ +//! 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::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 rap: Vec = vec![]; + let alpha: Vec = vec![]; + let offset = ExtE::zero(); + 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![]; + 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 rap: Vec = vec![]; + let alpha: Vec = vec![]; + let offset = ExtE::zero(); + 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()]; + 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 rap: Vec = vec![]; + let alpha: Vec = vec![]; + let offset = ExtE::zero(); + 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()]; + eval_program(&prog, &ctx, &mut base_evals, &mut ext_evals); + assert_eq!(ext_evals[0], fp(9).to_extension::() - aux_val); +} + +// ------------------------------------------------------------------------ +// 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: + // main(0,0) * challenge(0) + alpha_pow(1) * aux(0,3) - table_offset() + let mut b = IrBuilder::::new(); + 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!(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(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); + assert_eq!(res.dim(), Dim::Ext); + b.emit(0, res); + let prog = b.finish(0); + + 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 = 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![main_row], vec![aux_row]); + let frame = Frame::::new(vec![step]); + 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()]; + 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 rap: Vec = vec![]; + let alpha = vec![ext3(3, 3, 3)]; + let offset = ExtE::zero(); + 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()]; + 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 rap: Vec = vec![]; + let alpha = vec![ext3(3, 3, 3)]; + let offset = ExtE::zero(); + 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); + + // 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)); +} + +// ------------------------------------------------------------------------ +// 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 rap: Vec = vec![]; + let alpha: Vec = vec![]; + let offset = g(0); + let ctx = TransitionEvaluationContext::::new_prover( + frame.as_row_frame(), + &rap, + &alpha, + &offset, + ); + 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, &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)); + 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); + } +} diff --git a/crypto/stark/src/constraints/builder.rs b/crypto/stark/src/constraints/builder.rs new file mode 100644 index 000000000..5395c7228 --- /dev/null +++ b/crypto/stark/src/constraints/builder.rs @@ -0,0 +1,981 @@ +//! 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, RowFrame}; +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; + /// `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 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: + /// `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) + } +} + +// ============================================================================= +// 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, +} + +/// 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, + /// Number of exempted rows at the end of the trace (default 0). + pub end_exemptions: usize, +} + +impl ConstraintMeta { + /// A base-field constraint applying to every row. + pub fn base(constraint_idx: usize) -> Self { + Self { + constraint_idx, + kind: RootKind::Base, + end_exemptions: 0, + } + } + + /// An extension-field (LogUp) constraint applying to every row. + pub fn ext(constraint_idx: usize) -> Self { + Self { + kind: RootKind::Ext, + ..Self::base(constraint_idx) + } + } + + 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: THE single body. +/// +/// `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 { + /// 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 +/// 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 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 +// ============================================================================= + +/// 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 into the extension. +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) +// ============================================================================= + +/// 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, +{ + rows: RowFrame<'a, F, E>, + 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 { + rows, + 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 { + rows: *rows, + 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.rows.main(offset, col).clone() + } + fn aux(&self, offset: usize, col: usize) -> FieldElement { + self.rows.aux(offset, col).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) + } + + #[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; + } + #[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}" + ); + 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] + } + } +} + +// ============================================================================= +// 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, + 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, + 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, + 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 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_rows(&mut self, constraint_idx: usize, _rows: RowDomain, e: FieldElement) { + self.tracker.mark(constraint_idx); + self.ext_out[constraint_idx] = e; + } + fn emit_ext_rows(&mut self, constraint_idx: usize, _rows: RowDomain, 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 = 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, + }, + 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::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 { + // 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, + col: col as u16, + }, + Dim::Base, + 1, + ) + } + 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, + col: col as u16, + }, + Dim::Ext, + 1, + ) + } + 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 { + 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_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_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 new file mode 100644 index 000000000..fa6eba59c --- /dev/null +++ b/crypto/stark/src/constraints/builder_tests.rs @@ -0,0 +1,634 @@ +//! 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::{Dim, eval_program, eval_program_verifier}; +use crate::constraints::builder::{ + CaptureBuilder, ConstraintBuilder, ConstraintMeta, ConstraintSet, ProverEvalFolder, RootKind, + RowDomain, VerifierEvalFolder, num_base_from_meta, +}; +use crate::frame::Frame; +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 { + // idx 2,3 are degree-3 carry constraints. + fn max_degree(&self) -> usize { + 3 + } + + fn eval>(&self, b: &mut B) { + // 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 (degree 2): 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; + // 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 (degree 1): (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 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 challenges = vec![t.challenge0]; + let alphas = vec![t.alpha0]; + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &t.offset, + ); + + 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 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 challenges = vec![t.challenge0]; + let alphas = vec![t.alpha0]; + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &t.offset, + ); + + 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 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 challenges = vec![t.challenge0]; + let alphas = vec![t.alpha0]; + let ctx = TransitionEvaluationContext::::new_verifier( + &frame, + &challenges, + &alphas, + &t.offset, + ); + + 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_eq!(prog.roots.len(), NUM_CONSTRAINTS); + + 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!( + measured <= max_degree, + "constraint {idx}: tree-measured degree {measured} EXCEEDS max_degree() {max_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), ConstraintMeta::base(1)]; + assert_eq!(num_base_from_meta(&pure_base), 2); + 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); + + // 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), + ConstraintMeta::ext(1), + ConstraintMeta::base(2), + ]; + 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), ConstraintMeta::base(2)]; + 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 step = TableView::::new(vec![vec![FpE::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_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &offset, + ); + + 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 step = TableView::::new(vec![vec![FpE::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_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &offset, + ); + + 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 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); + + 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(); +} + +// ============================================================================= +// 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 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_rows(&mut self, constraint_idx: usize, rows: RowDomain, e: Self::Expr) { + self.base_idxs.push(constraint_idx); + self.inner.emit_base_rows(constraint_idx, rows, e); + } + fn emit_ext_rows(&mut self, constraint_idx: usize, rows: RowDomain, e: Self::ExprE) { + self.ext_idxs.push(constraint_idx); + self.inner.emit_ext_rows(constraint_idx, rows, 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 eval>(&self, b: &mut B) { + // 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, 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); + 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_rows( + 1, + RowDomain::except_last(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); + let max_degree = NextRowLogUpSet.max_degree(); + for &(idx, measured) in °rees { + 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 { + // 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 ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &challenges, + &alphas, + &offset, + ); + + 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 vctx = TransitionEvaluationContext::::new_verifier( + &frame_e, + &challenges, + &alphas, + &offset, + ); + + 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"); + } +} diff --git a/crypto/stark/src/constraints/evaluator.rs b/crypto/stark/src/constraints/evaluator.rs index 6e94473b7..48434b6e1 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::lookup::{BusPublicInputs, LOGUP_CHALLENGE_ALPHA, PackingShifts, compute_alpha_powers}; +use crate::frame::RowFrame; +use crate::lookup::{BusPublicInputs, LOGUP_CHALLENGE_ALPHA, 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> { @@ -60,42 +58,24 @@ where Vec::new() }; - // 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], - periodic_buf: &mut [FieldElement], - frame: &mut Frame| + base_buf: &mut [FieldElement]| -> 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 rows = RowFrame::from_lde(lde_trace, i, offsets); let ctx = TransitionEvaluationContext::new_prover( - frame, - periodic_buf, + rows, rap_challenges, &logup_alpha_powers, logup_table_offset, - &packing_shifts, ); air.compute_transition_prover(&ctx, base_buf, transition_buf); @@ -144,17 +124,10 @@ where ( vec![FieldElement::::zero(); num_transition], vec![FieldElement::::zero(); num_base], - vec![FieldElement::::zero(); num_periodic], - Frame::preallocate( - num_offsets, - rows_per_step, - num_main_cols, - num_aux_cols, - ), ) }, - |(transition_buf, base_buf, periodic_buf, frame), (i, boundary)| { - eval_row(i, boundary, transition_buf, base_buf, periodic_buf, frame) + |(transition_buf, base_buf), (i, boundary)| { + eval_row(i, boundary, transition_buf, base_buf) }, ) .collect() @@ -164,23 +137,11 @@ 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); boundary_evaluation .into_iter() .enumerate() - .map(|(i, boundary)| { - eval_row( - i, - boundary, - &mut transition_buf, - &mut base_buf, - &mut periodic_buf, - &mut frame, - ) - }) + .map(|(i, boundary)| eval_row(i, boundary, &mut transition_buf, &mut base_buf)) .collect() } } @@ -247,21 +208,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 +244,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/mod.rs b/crypto/stark/src/constraints/mod.rs index 3811523b5..0deee0d41 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; +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 new file mode 100644 index 000000000..ba22098de --- /dev/null +++ b/crypto/stark/src/constraints/zerofier.rs @@ -0,0 +1,142 @@ +//! Zerofier evaluation as free functions of [`ConstraintMeta`]. +//! +//! 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. 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). + +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(); + } + // 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 = decrement.clone(); + 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 `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 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(); + + // 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(); + + // 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 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. +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()) + }); + + // 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 bf1a454a7..24a4fba23 100644 --- a/crypto/stark/src/debug.rs +++ b/crypto/stark/src/debug.rs @@ -2,16 +2,13 @@ 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; -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) @@ -89,12 +73,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 { @@ -117,20 +100,13 @@ 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 periodic_values: Vec<_> = periodic_columns - .iter() - .map(|col| col[step].clone()) - .collect(); let transition_evaluation_context = TransitionEvaluationContext::new_prover( - &frame, - &periodic_values, + frame.as_row_frame(), rap_challenges, &logup_alpha_powers, &logup_table_offset, - &packing_shifts, ); let evaluations = air.compute_transition(&transition_evaluation_context); 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/dummy_air.rs b/crypto/stark/src/examples/dummy_air.rs index 1409f96ba..9decb9a53 100644 --- a/crypto/stark/src/examples/dummy_air.rs +++ b/crypto/stark/src/examples/dummy_air.rs @@ -1,9 +1,10 @@ -use std::marker::PhantomData; - use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -14,125 +15,31 @@ 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`]: a fibonacci recurrence on +/// column 1 and an IS_BIT on column 0, written once against the +/// [`ConstraintBuilder`]. +#[derive(Default)] +pub struct DummyConstraints; + +impl ConstraintSet for DummyConstraints { + fn eval>(&self, b: &mut B) { + // 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_rows(0, RowDomain::except_last(2), a2 - a1 - a0); + + // 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)); } } pub struct DummyAIR { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, } impl AIR for DummyAIR { @@ -145,24 +52,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( @@ -178,10 +77,33 @@ impl AIR for DummyAIR { BoundaryConstraints::from_constraints(vec![a0, a1]) } - fn transition_constraints( + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + 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, - ) -> &Vec>> { - &self.transition_constraints + 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 { diff --git a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs index 76c8ea11f..855469e4b 100644 --- a/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs +++ b/crypto/stark/src/examples/fibonacci_2_cols_shifted.rs @@ -1,7 +1,10 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -13,148 +16,57 @@ use math::{ traits::AsBytes, }; use std::marker::PhantomData; - -#[derive(Clone)] -struct ShiftedFibTransition1 { - phantom: PhantomData, -} - -impl ShiftedFibTransition1 { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] +pub struct PublicInputs +where + F: IsFFTField, +{ + pub claimed_value: FieldElement, + pub claimed_index: usize, } -impl TransitionConstraintEvaluator for ShiftedFibTransition1 +impl AsBytes for PublicInputs where - F: IsFFTField + Send + Sync, + F: IsFFTField, + FieldElement: AsBytes, { - 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; + fn as_bytes(&self) -> Vec { + let mut transcript_init_seed = self.claimed_index.to_be_bytes().to_vec(); + transcript_init_seed.extend_from_slice(&self.claimed_value.as_bytes()); + transcript_init_seed } } -#[derive(Clone)] -struct ShiftedFibTransition2 { +/// Single-body [`ConstraintSet`] for [`Fibonacci2ColsShifted`]: the two +/// shifted-Fibonacci recurrences, written once against the +/// [`ConstraintBuilder`]. +pub struct Fibonacci2ColsShiftedConstraints { phantom: PhantomData, } -impl ShiftedFibTransition2 { - pub fn new() -> Self { +impl Default for Fibonacci2ColsShiftedConstraints { + fn default() -> Self { Self { phantom: PhantomData, } } } -impl TransitionConstraintEvaluator for ShiftedFibTransition2 +impl ConstraintSet for Fibonacci2ColsShiftedConstraints where F: IsFFTField + Send + Sync, { - fn degree(&self) -> usize { - 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); - 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)] -pub struct PublicInputs -where - F: IsFFTField, -{ - pub claimed_value: FieldElement, - pub claimed_index: usize, -} - -impl AsBytes for PublicInputs -where - F: IsFFTField, - FieldElement: AsBytes, -{ - fn as_bytes(&self) -> Vec { - let mut transcript_init_seed = self.claimed_index.to_be_bytes().to_vec(); - transcript_init_seed.extend_from_slice(&self.claimed_value.as_bytes()); - transcript_init_seed + // 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); } } @@ -163,7 +75,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 @@ -183,23 +96,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, } } @@ -220,10 +129,38 @@ where BoundaryConstraints::from_constraints(vec![initial_condition, claimed_value_constraint]) } - fn transition_constraints( + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + 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, - ) -> &Vec>> { - &self.transition_constraints + 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 { diff --git a/crypto/stark/src/examples/fibonacci_2_columns.rs b/crypto/stark/src/examples/fibonacci_2_columns.rs index 7662c8f98..beb9c999f 100644 --- a/crypto/stark/src/examples/fibonacci_2_columns.rs +++ b/crypto/stark/src/examples/fibonacci_2_columns.rs @@ -4,7 +4,10 @@ use super::simple_fibonacci::FibonacciPublicInputs; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -13,129 +16,38 @@ use crate::{ }; use math::field::{element::FieldElement, traits::IsFFTField}; -#[derive(Clone)] -struct FibTransition1 { +/// Single-body [`ConstraintSet`] for [`Fibonacci2ColsAIR`]: the two row-major +/// Fibonacci recurrences, written once against the [`ConstraintBuilder`]. +pub struct Fibonacci2ColsConstraints { phantom: PhantomData, } -impl FibTransition1 { - pub fn new() -> Self { +impl Default for Fibonacci2ColsConstraints { + fn default() -> Self { Self { phantom: PhantomData, } } } -impl TransitionConstraintEvaluator for FibTransition1 +impl ConstraintSet for Fibonacci2ColsConstraints 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; + 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); + + // 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); } } @@ -144,7 +56,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 @@ -162,23 +75,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, } } @@ -195,8 +104,38 @@ 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( + &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 { diff --git a/crypto/stark/src/examples/fibonacci_multi_column.rs b/crypto/stark/src/examples/fibonacci_multi_column.rs index ac6069ece..ae6e61527 100644 --- a/crypto/stark/src/examples/fibonacci_multi_column.rs +++ b/crypto/stark/src/examples/fibonacci_multi_column.rs @@ -3,7 +3,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -15,110 +18,39 @@ 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, +/// 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)>, } -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, - } - } +/// Single-body [`ConstraintSet`] for [`FibonacciMultiColumnAIR`]: one +/// Fibonacci constraint per column, written once against the +/// [`ConstraintBuilder`]. +pub struct FibonacciMultiColumnConstraints { + pub num_columns: usize, } -impl TransitionConstraintEvaluator for FibColumnConstraint +impl ConstraintSet for FibonacciMultiColumnConstraints 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; - } + 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); + // 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); } } } -/// 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)] -pub struct FibonacciMultiColumnPublicInputs { - /// Initial values for each column: (a0, a1) pairs - pub initial_values: Vec<(FieldElement, FieldElement)>, -} - /// Multi-column Fibonacci AIR. /// Each column contains an independent Fibonacci sequence. pub struct FibonacciMultiColumnAIR @@ -127,8 +59,9 @@ where E: IsField + Send + Sync, { context: AirContext, - constraints: Vec>>, + meta: Vec, num_columns: usize, + phantom: PhantomData<(F, E)>, } impl AIR for FibonacciMultiColumnAIR @@ -153,8 +86,46 @@ where trace_length } - fn transition_constraints(&self) -> &Vec>> { - &self.constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + 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( @@ -201,25 +172,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 10f1827d2..22003952d 100644 --- a/crypto/stark/src/examples/fibonacci_rap.rs +++ b/crypto/stark/src/examples/fibonacci_rap.rs @@ -3,7 +3,10 @@ use std::{marker::PhantomData, ops::Div}; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -25,134 +28,37 @@ fn resize_to_next_power_of_two(trace_columns: &mut [Vec { - phantom: PhantomData, -} +/// 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. +pub struct FibonacciRAPConstraints; -impl FibConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for FibConstraint +impl ConstraintSet for FibonacciRAPConstraints 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; + fn eval>(&self, b: &mut B) { + // 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_rows(0, RowDomain::except_last(3 + 32 - 16 - 1), a2 - a1 - a0); + + // 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_rows( + 1, + RowDomain::except_last(1), + z_i_plus_one * (b_i + gamma.clone()) - z_i * (a_i + gamma), + ); } } @@ -161,10 +67,12 @@ where F: IsFFTField, { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct FibonacciRAPPublicInputs where F: IsFFTField, @@ -188,23 +96,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, } } @@ -271,10 +175,38 @@ where BoundaryConstraints::from_constraints(vec![a0, a1, a0_aux]) } - fn transition_constraints( + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + 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, - ) -> &Vec>> { - &self.transition_constraints + 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 { 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/multi_table_lookup.rs b/crypto/stark/src/examples/multi_table_lookup.rs index 0504d08cb..5f14530c0 100644 --- a/crypto/stark/src/examples/multi_table_lookup.rs +++ b/crypto/stark/src/examples/multi_table_lookup.rs @@ -1,5 +1,11 @@ +//! 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 example-level `ConstraintSet`; it passes `EmptyConstraints` and +//! runs the single-body path together with `AirWithBuses`. + use crate::{ - constraints::transition::TransitionConstraintEvaluator, + constraints::builder::EmptyConstraints, lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, Multiplicity, NullBoundaryConstraintBuilder, Packing, @@ -27,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) @@ -52,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) @@ -77,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) @@ -102,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 d49b0050d..aedaf1d72 100644 --- a/crypto/stark/src/examples/quadratic_air.rs +++ b/crypto/stark/src/examples/quadratic_air.rs @@ -3,7 +3,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -12,64 +15,29 @@ use crate::{ }; use math::field::{element::FieldElement, traits::IsFFTField}; -#[derive(Clone)] -struct QuadraticConstraint { +/// Single-body [`ConstraintSet`] for [`QuadraticAIR`]: `x_{i+1} = x_i²`, +/// written once against the [`ConstraintBuilder`]. +pub struct QuadraticConstraints { phantom: PhantomData, } -impl QuadraticConstraint { - pub fn new() -> Self { +impl Default for QuadraticConstraints { + fn default() -> Self { Self { phantom: PhantomData, } } } -impl TransitionConstraintEvaluator for QuadraticConstraint +impl ConstraintSet for QuadraticConstraints 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; + fn eval>(&self, b: &mut B) { + let x = b.main(0, 0); + let x_squared = b.main(1, 0); + // 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); } } @@ -78,10 +46,12 @@ where F: IsFFTField, { context: AirContext, - constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct QuadraticPublicInputs where F: IsFFTField, @@ -102,20 +72,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, } } @@ -131,10 +100,38 @@ where BoundaryConstraints::from_constraints(vec![a0]) } - fn transition_constraints( + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + fn compute_transition_prover( &self, - ) -> &Vec>> { - &self.constraints + 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 { diff --git a/crypto/stark/src/examples/read_only_memory.rs b/crypto/stark/src/examples/read_only_memory.rs index 8c3e9efac..521bd7ca9 100644 --- a/crypto/stark/src/examples/read_only_memory.rs +++ b/crypto/stark/src/examples/read_only_memory.rs @@ -3,7 +3,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -17,210 +20,56 @@ 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, -} +/// 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 ContinuityConstraint { - pub fn new() -> Self { - Self { - phantom: PhantomData, - } - } -} - -impl TransitionConstraintEvaluator for ContinuityConstraint +impl ConstraintSet for ReadOnlyRAPConstraints 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); + 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; + + // 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), + ); - // 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; - } + 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); + // idx 2 — permutation (degree 2, 1 end exemption). + b.emit_ext_rows( + 2, + RowDomain::except_last(1), + sorted_fp * p1 - unsorted_fp * p0, + ); } } @@ -229,10 +78,12 @@ where F: IsFFTField, { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct ReadOnlyPublicInputs where F: IsFFTField, @@ -257,24 +108,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, } } @@ -362,10 +208,38 @@ where BoundaryConstraints::from_constraints(vec![c1, c2, c3, c4, c_aux1, c_aux2]) } - fn transition_constraints( + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + 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, - ) -> &Vec>> { - &self.transition_constraints + 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 { diff --git a/crypto/stark/src/examples/read_only_memory_logup.rs b/crypto/stark/src/examples/read_only_memory_logup.rs index e4f25c16c..5090098bd 100644 --- a/crypto/stark/src/examples/read_only_memory_logup.rs +++ b/crypto/stark/src/examples/read_only_memory_logup.rs @@ -7,7 +7,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -24,328 +27,63 @@ 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::, - } - } -} +/// 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 TransitionConstraintEvaluator for SingleValueConstraint +impl ConstraintSet for LogReadOnlyRAPConstraints 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 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; + + // 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), + ); - 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; - } - } - } + // 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; + // 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, + ); } } @@ -357,10 +95,12 @@ where E: IsField + Send + Sync, { context: AirContext, - transition_constraints: Vec>>, + meta: Vec, + phantom: PhantomData<(F, E)>, } -#[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, @@ -388,24 +128,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, } } @@ -502,10 +237,38 @@ where BoundaryConstraints::from_constraints(vec![c1, c2, c3, c4, c5, c_aux1, c_aux2]) } - fn transition_constraints( + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + fn compute_transition_prover( &self, - ) -> &Vec>> { - &self.transition_constraints + 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 { diff --git a/crypto/stark/src/examples/simple_addition.rs b/crypto/stark/src/examples/simple_addition.rs index 78f938838..d064acd55 100644 --- a/crypto/stark/src/examples/simple_addition.rs +++ b/crypto/stark/src/examples/simple_addition.rs @@ -6,7 +6,10 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -15,63 +18,30 @@ 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 { +/// Single-body [`ConstraintSet`] for [`SimpleAdditionAIR`]: `col0 + col1 = col2` +/// (applied at every row), written once against the [`ConstraintBuilder`]. +pub struct SimpleAdditionConstraints { phantom: PhantomData, } -impl AdditionConstraint { - pub fn new() -> Self { +impl Default for SimpleAdditionConstraints { + fn default() -> Self { Self { phantom: PhantomData, } } } -impl TransitionConstraintEvaluator for AdditionConstraint +impl ConstraintSet for SimpleAdditionConstraints 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; + fn eval>(&self, b: &mut B) { + let col0 = b.main(0, 0); + let col1 = b.main(0, 1); + let col2 = b.main(0, 2); + // idx 0: col0 + col1 - col2 = 0, applied at every row (degree 1, no exemptions). + b.emit_base(0, col0 + col1 - col2); } } @@ -80,10 +50,12 @@ where F: IsFFTField, { context: AirContext, - constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct SimpleAdditionPublicInputs where F: IsFFTField, @@ -107,20 +79,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, } } @@ -139,10 +110,38 @@ where BoundaryConstraints::from_constraints(vec![a0, a1]) } - fn transition_constraints( + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + fn compute_transition_prover( &self, - ) -> &Vec>> { - &self.constraints + 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 { diff --git a/crypto/stark/src/examples/simple_fibonacci.rs b/crypto/stark/src/examples/simple_fibonacci.rs index a39064258..db84ab439 100644 --- a/crypto/stark/src/examples/simple_fibonacci.rs +++ b/crypto/stark/src/examples/simple_fibonacci.rs @@ -1,7 +1,10 @@ use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, + run_transition_prover, run_transition_verifier, + }, }, context::AirContext, proof::options::ProofOptions, @@ -11,66 +14,30 @@ use crate::{ use math::field::{element::FieldElement, traits::IsFFTField}; use std::marker::PhantomData; -#[derive(Clone)] -struct FibConstraint { +/// Single-body [`ConstraintSet`] for [`FibonacciAIR`]: `a_{i+2} = a_{i+1} + a_i`, +/// written once against the [`ConstraintBuilder`]. +pub struct SimpleFibonacciConstraints { phantom: PhantomData, } -impl FibConstraint { - pub fn new() -> Self { +impl Default for SimpleFibonacciConstraints { + fn default() -> Self { Self { phantom: PhantomData, } } } -impl TransitionConstraintEvaluator for FibConstraint +impl ConstraintSet for SimpleFibonacciConstraints 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; + fn eval>(&self, b: &mut B) { + let a0 = b.main(0, 0); + let a1 = b.main(1, 0); + let a2 = b.main(2, 0); + // 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); } } @@ -79,10 +46,12 @@ where F: IsFFTField, { context: AirContext, - constraints: Vec>>, + meta: Vec, + phantom: PhantomData, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound = "FieldElement: serde::Serialize + serde::de::DeserializeOwned")] pub struct FibonacciPublicInputs where F: IsFFTField, @@ -104,19 +73,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, } } @@ -124,8 +93,38 @@ where trace_length } - fn transition_constraints(&self) -> &Vec>> { - &self.constraints + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta + } + + 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( 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/frame.rs b/crypto/stark/src/frame.rs index 952a3a110..5300be90d 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 (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, + "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,72 @@ 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 } - } +#[cfg(test)] +mod row_frame_tests { + use super::*; + use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext3; + use math::field::goldilocks::GoldilocksField as Gl; - /// 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(); + type Fp = FieldElement; + type Fp3 = FieldElement; - 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]; + /// 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) + } - 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; + #[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); + } - // 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(); - } + #[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)); + } - // 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(); - } + #[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]); + } - sub_row_idx += 1; - step_row += blowup_factor; + #[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/lib.rs b/crypto/stark/src/lib.rs index 2b93f41ba..92a7a9697 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; diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 5174bf66c..cd41ac15a 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -5,7 +5,9 @@ use std::marker::PhantomData; use crate::{ constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, - transition::TransitionConstraintEvaluator, + builder::{ + ConstraintMeta, ConstraintSet, ProverEvalFolder, VerifierEvalFolder, num_base_from_meta, + }, }, context::AirContext, proof::options::ProofOptions, @@ -540,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 { @@ -589,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(), @@ -801,19 +805,36 @@ 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, 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. + 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 +853,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 +872,24 @@ 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(); - - // 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)); - } + 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, + // 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"); + 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 { @@ -895,7 +900,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 +912,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 +969,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 +1012,14 @@ where } fn composition_poly_degree_bound(&self, trace_length: usize) -> usize { + // 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 - .transition_constraints - .iter() - .map(|c| c.degree()) - .max() - .unwrap_or(1); + .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 @@ -1023,13 +1034,57 @@ where } fn num_base_transition_constraints(&self) -> usize { - self.num_base_constraints + self.num_base + } + + fn constraints_meta(&self) -> &[ConstraintMeta] { + &self.meta } - fn transition_constraints( + 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( @@ -1675,332 +1730,864 @@ 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()) +// ============================================================================= +// 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. 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) 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`] +// 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; + +/// 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 source for the LogUp +/// constraints: [`emit_logup_constraints`] reads it to generate every LogUp +/// constraint (its metadata is derived from that same emission). +#[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, } -/// 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, - ); +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, + } } - z - &linear_combination -} -/// 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, -} + /// 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..] + } -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, + /// 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 } } } -impl TransitionConstraintEvaluator for LookupBatchedTermConstraint +/// Capture a [`Multiplicity`] as a base-field expression, mirroring +/// [`Multiplicity::evaluate_with`]. +fn emit_multiplicity(b: &B, multiplicity: &Multiplicity, offset: usize) -> B::Expr where - F: IsFFTField + IsSubFieldOf + Send + Sync, - E: IsField + Send + Sync, + F: IsField, + E: IsField, + B: ConstraintBuilder, { - fn degree(&self) -> usize { - 3 // c * fp_a * fp_b + 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), } +} - fn constraint_idx(&self) -> usize { - self.constraint_idx +/// 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 +} - 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 +/// 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, + mut fp: B::ExprE, +) -> (B::ExprE, 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 => (fp - col(start_col) * alpha(0), 1), + Packing::Word2L => { + let combined = col(start_col) + col(start_col + 1) * shift_16(); + (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(); + (fp - combined * alpha(0), 1) + } + Packing::DWordWL => { + fp = fp - col(start_col) * alpha(0); + (fp - col(start_col + 1) * alpha(1), 2) + } + Packing::DWordHHW => { + fp = fp - col(start_col) * alpha(0); + let w = col(start_col + 1) + col(start_col + 2) * shift_16(); + (fp - w * alpha(1), 2) + } + Packing::DWordWHH => { + let w = col(start_col) + col(start_col + 1) * shift_16(); + 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(); + fp = fp - w0 * alpha(0); + let w1 = col(start_col + 2) + col(start_col + 3) * shift_16(); + (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(); + 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(); + (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(); + fp = fp - w * alpha(i); + } + (fp, 4) + } + Packing::QuadWL => { + for i in 0..4 { + fp = fp - col(start_col + i) * alpha(i); + } + (fp, 4) + } + } +} - 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; +/// 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, + fp: B::ExprE, +) -> (B::ExprE, 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, + fp, + ), + 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; 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) } } } -/// 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). +/// Capture an interaction's fingerprint as an extension expression, mirroring +/// `z - (bus_id + α·v[0] + α²·v[1] + ...)`. /// -/// For 1 absorbed interaction: -/// `(acc_next - acc_curr - Σ terms + L/N) · f - sign · m = 0` (degree 2) +/// `α⁰ = 1`: the bus-id term needs no multiply and is added as a base constant. /// -/// 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, +/// 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, + E: IsField, + B: ConstraintBuilder, +{ + 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 alpha_idx = 1; + for bv in &interaction.values { + let (next, consumed) = emit_busvalue_fingerprint::(b, bv, offset, alpha_idx, fp); + fp = next; + alpha_idx += consumed; + } + fp } -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, +/// 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 + 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 (degree 3; + // see `logup_max_degree`). + 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). +/// `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"), + }; + + // 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 } -impl TransitionConstraintEvaluator for LookupAccumulatedConstraint +/// 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: IsFFTField + IsSubFieldOf + Send + Sync, - E: IsField + Send + Sync, + F: IsField, + E: IsField, + B: ConstraintBuilder, { - fn degree(&self) -> usize { - 1 + self.absorbed.len() // 2 for 1 absorbed, 3 for 2 absorbed + if layout.interactions.is_empty() { + return; } - - fn constraint_idx(&self) -> usize { - self.constraint_idx + 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); +} - 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"), +/// 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. +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 +} + +#[cfg(test)] +mod logup_single_source_tests { + //! 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 + //! 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) + } + } - 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, - ), + /// 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 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). + 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(); + + // 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}"); + } + + // 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); + 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 { + 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); + + 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.as_row_frame(), + &rap_challenges, + &alpha_powers, + &table_offset, + ); + + // --- 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(); + + 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!( + ext_out[i], ir_ext[i], + "[{label}] prover folder vs 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).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_step(frame.get_evaluation_step(0)), + embed_step(frame.get_evaluation_step(1)), + ]); + let vctx = TransitionEvaluationContext::::new_verifier( + &vframe, + &rap_challenges, + &alpha_powers, + &table_offset, + ); + + // --- 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], ir_vext[i], + "[{label}] verifier folder vs interpreter mismatch, constraint {i}, trial {trial}" + ); + } + + // Prover base-promotion and verifier evaluations must agree + // (the prover frame embedded == the verifier frame). + for i in 0..n { + assert_eq!( + ext_out[i], vext_out[i], + "[{label}] prover vs verifier folder 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() { + // 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); + } - if let Some(eval) = transition_evaluations.get_mut(self.constraint_idx) { - *eval = res; + #[test] + 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. + 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_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(), + ); } } + + #[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 + // 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); + } } diff --git a/crypto/stark/src/tests/air_tests.rs b/crypto/stark/src/tests/air_tests.rs index 8e20f303e..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::{ @@ -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; 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/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/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/tests/transition_tests.rs b/crypto/stark/src/tests/transition_tests.rs deleted file mode 100644 index 17bfaa6cc..000000000 --- a/crypto/stark/src/tests/transition_tests.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::constraints::transition::TransitionConstraintEvaluator; -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 `end_exemptions_roots`. -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 c = DummyConstraint:: { - period: 1, - offset: 0, - end_exemptions: 2, - phantom: PhantomData, - }; - - let roots = c.end_exemptions_roots(&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)]); -} - -#[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 c = DummyConstraint:: { - period: 1, - offset: 0, - end_exemptions: 0, - phantom: PhantomData, - }; - - assert!(c.end_exemptions_roots(&g, trace_length).is_empty()); -} 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 06465b659..c28f831a2 100644 --- a/crypto/stark/src/traits.rs +++ b/crypto/stark/src/traits.rs @@ -1,23 +1,19 @@ 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::{ - constraints::transition::TransitionConstraintEvaluator, - domain::Domain, - lookup::{BusPublicInputs, PackingShifts}, + constraint_ir::ConstraintProgram, constraints::builder::ConstraintMeta, domain::Domain, + lookup::BusPublicInputs, }; 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. @@ -53,13 +49,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, } @@ -75,20 +69,18 @@ where E: IsField, { Prover { - frame: &'a Frame, - periodic_values: &'a [FieldElement], + /// 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, - packing_shifts: &'a PackingShifts, }, Verifier { frame: &'a Frame, - periodic_values: &'a [FieldElement], rap_challenges: &'a [FieldElement], logup_alpha_powers: &'a [FieldElement], logup_table_offset: &'a FieldElement, - packing_shifts: &'a PackingShifts, }, } @@ -98,38 +90,30 @@ where E: IsField, { pub fn new_prover( - frame: &'a Frame, - periodic_values: &'a [FieldElement], + 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, - periodic_values, + rows, rap_challenges, logup_alpha_powers, logup_table_offset, - packing_shifts, } } 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, - packing_shifts: &'a PackingShifts, ) -> Self { Self::Verifier { frame, - periodic_values, rap_challenges, logup_alpha_powers, logup_table_offset, - packing_shifts, } } } @@ -216,22 +200,20 @@ 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) + /// 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 +233,31 @@ 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), 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 + /// 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( @@ -287,34 +278,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 - } - - 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 +287,26 @@ 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(), + 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 5b512c37e..616732e22 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}; @@ -167,12 +167,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,23 +193,24 @@ 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, - &periodic_values, &challenges.rap_challenges, &logup_alpha_powers, &logup_table_offset, - &packing_shifts, ); let transition_ood_frame_evaluations = air.compute_transition(&transition_evaluation_context); 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/constraints/cpu.rs b/prover/src/constraints/cpu.rs index facc9e16d..917445b7e 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,667 +40,338 @@ 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 +// Assembly // ========================================================================= -/// `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 -} +/// Total number of CPU transition constraints (excludes bus lookups): +/// - IS_BIT: 12 +/// - decode mutex: 6 (`word_instr · {MEMORY, BRANCH, ECALL, WRITE_REGISTER, +/// READ_REGISTER1, READ_REGISTER2}`) +/// - ADD pair: 2, SUB pair: 2 +/// - arg2 multiplex: 2 +/// - register zero-forcing: 4 (`rv1[0..1]`, `rv2[0..1]`) +/// - rvd = res: 2 +/// - branch rvd (`pc + len`): 2 +/// - branch_cond: 1 +/// - next_pc: 2 +/// - assumptions: 4 (MEMORY·BRANCH mutex 1 + arg2 exclusivity 2 + mem_flags IS_BIT 1) +pub const NUM_CPU_CONSTRAINTS: usize = 12 + 6 + 2 + 2 + 2 + 4 + 2 + 2 + 1 + 2 + 4; // ========================================================================= -// decode group: word_instr mutex +// Single-body emit functions (ConstraintBuilder front-end) // ========================================================================= +// +// 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). -/// 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, - } - } -} +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; -impl TransitionConstraint for ProductZeroConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx - } +use super::templates::{INV_SHIFT_32, emit_add_pair, emit_is_bit}; - 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) - } +/// `col_a · col_b = 0`. +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); } -/// `(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 { +/// `(1 − MEMORY − BRANCH) · read_register2 · imm[i] = 0`. +pub fn emit_arg2_exclusive>( + b: &mut B, + idx: usize, 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 { +) { + 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); +} + +/// `(1 − MEMORY) · mem_flags · (1 − mem_flags) = 0`. +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), + ); +} + +/// `(1 − flag) · value = 0`. +pub fn emit_reg_not_read_is_zero>( + b: &mut B, + idx: usize, 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 - } +) { + 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); } -// ========================================================================= -// alu group: arg2 multiplex -// ========================================================================= - -/// `arg2` multiplex (`cpu.toml` CPU-A1), for word index -/// `word_idx ∈ {0,1}`: +/// `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]) +/// 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. +pub fn emit_arg2>( + b: &mut B, + idx: usize, 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 - } +) { + 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); + // Degree 2 relies on the live `MEMORY·BRANCH = 0` mutex. + b.emit_base(idx, arg2 - expected); +} + +/// `cast(res, DWordWL)` word from the four `res` halves (DWordHL). +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) } -// ========================================================================= -// 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. +/// `(1 − MEMORY − BRANCH) · (rvd[i] − cast(res, WL)[i]) = 0`. +pub fn emit_rvd_eq_res>( + b: &mut B, + idx: usize, 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`. +) { + 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)); +} + +/// 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`]: /// -/// 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, +/// ```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; + + // gate·carry·(1−carry): degree 3 (both instances). + 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`). +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) + }); } -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 - } +/// `branch_cond − (BRANCH·JALR + BRANCH·(1−JALR)·res[0])`. +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); - 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 - } + let expected = branch.clone() * jalr.clone() + branch * (one - jalr) * res0; + b.emit_base(idx, branch_cond - expected); } -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) - } +/// `(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>( + 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) + }); } // ========================================================================= -// branch group: branch_cond +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// `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 { +/// 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`); +/// - 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 { + // The conditional ADD/SUB carry pairs, arg2 exclusivity, mem-flags bit and + // branch constraints are degree 3. + fn max_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, + 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; } - } - - 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 - } + // idx 22,23: arg2 multiplex (low, high words). + emit_arg2(b, idx, 0); + idx += 1; + emit_arg2(b, idx, 1); + idx += 1; - 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 -// ========================================================================= + // 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; + } -/// 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) -} + // 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; -/// 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) -} + // idx 30,31: BRANCH ⇒ rvd = pc + instruction_length. + emit_branch_rvd_pair(b, idx); + idx += 2; -// ========================================================================= -// Assembly -// ========================================================================= + // idx 32: branch_cond. + emit_branch_cond(b, idx); + idx += 1; -/// Total number of CPU transition constraints (excludes bus lookups): -/// - IS_BIT: 12 -/// - decode mutex: 6 (`word_instr · {MEMORY, BRANCH, ECALL, WRITE_REGISTER, -/// READ_REGISTER1, READ_REGISTER2}`) -/// - ADD pair: 2, SUB pair: 2 -/// - arg2 multiplex: 2 -/// - register zero-forcing: 4 (`rv1[0..1]`, `rv2[0..1]`) -/// - rvd = res: 2 -/// - branch rvd (`pc + len`): 2 -/// - branch_cond: 1 -/// - next_pc: 2 -/// - assumptions: 4 (MEMORY·BRANCH mutex 1 + arg2 exclusivity 2 + mem_flags IS_BIT 1) -pub const NUM_CPU_CONSTRAINTS: usize = 12 + 6 + 2 + 2 + 2 + 4 + 2 + 2 + 1 + 2 + 4; + // idx 33,34: next_pc = pc + instruction_length. + emit_next_pc_add_pair(b, idx); + idx += 2; -/// Creates all CPU transition constraints. -/// -/// Returns `(is_bit_constraints, add_constraints, other_constraints, next_idx)`. -#[allow(clippy::type_complexity)] -pub fn create_all_cpu_constraints() -> ( - 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; - } + // idx 35: MEMORY · BRANCH = 0. + emit_product_zero(b, idx, cols::MEMORY, cols::BRANCH); + 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; + // idx 36,37: arg2 exclusivity. + for &imm_col in &[cols::IMM_0, cols::IMM_1] { + emit_arg2_exclusive(b, idx, imm_col); + 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; - } + // idx 38: IS_BIT(mem_flags) on non-MEMORY rows. + emit_mem_flags_bit(b, idx); + 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; + debug_assert_eq!(idx, NUM_CPU_CONSTRAINTS); } - other.push(MemFlagsBitConstraint::new(next_idx).boxed()); - next_idx += 1; - - (is_bit, add_constraints, other, next_idx) } diff --git a/prover/src/constraints/templates.rs b/prover/src/constraints/templates.rs index ef5b6c036..04932eab8 100644 --- a/prover/src/constraints/templates.rs +++ b/prover/src/constraints/templates.rs @@ -7,14 +7,10 @@ //! - **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 -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) // ========================================================================= @@ -119,7 +37,7 @@ impl TransitionConstraint for IsBitConstra /// /// 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 { @@ -132,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: @@ -144,86 +111,22 @@ 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 }, /// 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: Vec, + lo: AddTerms, /// Terms for the high 32-bit word (empty = zero) - hi: Vec, + hi: AddTerms, }, } -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 // ------------------------------------------------------------------------- @@ -237,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(), } } @@ -246,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(), } } @@ -259,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, @@ -268,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, @@ -278,7 +181,7 @@ impl AddOperand { coefficient: 1 << 16, column: start_column + 3, }, - ], + ]), } } @@ -287,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, @@ -304,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, @@ -322,190 +225,150 @@ 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`. + pub fn linear(lo: &[AddLinearTerm], hi: &[AddLinearTerm]) -> Self { + AddOperand::Linear { + lo: AddTerms::of(lo), + hi: AddTerms::of(hi), + } } } -// ------------------------------------------------------------------------- -// 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, +// ========================================================================= +// Single-body emit functions (ConstraintBuilder front-end) +// ========================================================================= +// +// 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 +// zerofier shape — none of these templates override period/offset/ +// exemptions). + +use stark::constraints::builder::ConstraintBuilder; + +/// IS_BIT: `x·(1−x)`, optionally gated by a condition column: +/// `cond·x·(1−x)`. +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); } -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) - } +/// One [`AddLinearTerm`]: `column · coefficient` or a constant. +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), } } -impl TransitionConstraint for AddConstraint { - fn degree(&self) -> usize { - if self.cond_cols.is_empty() { 2 } else { 3 } +/// Sum of terms, from zero. +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 +} - fn constraint_idx(&self) -> usize { - self.constraint_idx +/// An operand's low word. +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), } +} - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) +/// An operand's high word. +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), } } -// ========================================================================= -// Helper Functions -// ========================================================================= - -/// Creates multiple unconditional IS_BIT constraints for the given columns. +/// The ADD carry pair, emitted from ONE body at `idx` and `idx + 1`: /// -/// # Arguments -/// * `value_cols` - Slice of column indices to constrain -/// * `constraint_idx_start` - Starting index for constraint numbering +/// ```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 +/// ``` /// -/// # 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()) +/// `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); } diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index ccdd5a6f9..77092d0e4 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -42,6 +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, ConstraintSet, EmptyConstraints}; use stark::lookup::{AirWithBuses, AuxiliaryTraceBuildData, NullBoundaryConstraintBuilder}; use stark::proof::options::ProofOptions; use stark::proof::stark::MultiProof; @@ -66,11 +67,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 +108,14 @@ 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 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). @@ -124,7 +123,7 @@ fn l2g_constraints() /// `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* @@ -134,7 +133,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 +141,7 @@ fn l2g_global_air( }, opts, 1, - empty_constraints(), + EmptyConstraints, ) } @@ -155,7 +154,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 +165,7 @@ fn l2g_memory_air( AuxiliaryTraceBuildData { interactions }, opts, 1, - l2g_constraints(), + L2gMemoryConstraints, ) } @@ -181,7 +180,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 +188,7 @@ fn global_memory_air( }, opts, 1, - empty_constraints(), + EmptyConstraints, ); let commitment = if config.init_values.is_some() { page::compute_precomputed_commitment(config, opts) @@ -294,9 +293,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/lib.rs b/prover/src/lib.rs index 41b7d4738..4f891ae4c 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -272,73 +272,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 @@ -347,65 +347,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 @@ -455,68 +455,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 @@ -525,18 +553,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)` @@ -549,24 +579,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/tables/branch.rs b/prover/src/tables/branch.rs index 9443a81a1..d4baf10c5 100644 --- a/prover/src/tables/branch.rs +++ b/prover/src/tables/branch.rs @@ -26,11 +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::transition::TransitionConstraint; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::table::TableView; use stark::trace::TraceTable; use std::collections::HashMap; @@ -357,214 +354,103 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// BRANCH table conditional ADD constraint. +/// `(unmasked_0, unmasked_1)` — the next-pc value repacked into two words, +/// as builder expressions (the constraint-expression form of the value +/// [`BranchOperation::compute_next_pc_unmasked`] computes for trace generation): /// -/// 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, +/// ```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) } -/// 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, +/// `carry_0 = (base_0 + offset_0 − unmasked_0)·2⁻³²`. +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 } -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) - } - } - } +/// `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 +/// 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, +) -> 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 } -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 +/// 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); +/// - 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 max_degree(&self) -> usize { + 3 } - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) + 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)); } } -/// 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) -} - // ========================================================================= // 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..cd1ca264b 100644 --- a/prover/src/tables/commit.rs +++ b/prover/src/tables/commit.rs @@ -43,14 +43,12 @@ //! - `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 crate::constraints::templates::{AddConstraint, AddOperand}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; + +use crate::constraints::templates::{AddOperand, emit_add_pair, emit_is_bit}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; @@ -726,128 +724,48 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// 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) +/// 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); +/// - idx 6,7: `ADD` pair `count_decr + 1 = count` (unconditional). +pub struct CommitConstraints; + +impl ConstraintSet for CommitConstraints { + 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/tables/cpu.rs b/prover/src/tables/cpu.rs index 1752022b9..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 d7dbd5d6f..e0931ff3a 100644 --- a/prover/src/tables/cpu32.rs +++ b/prover/src/tables/cpu32.rs @@ -17,18 +17,16 @@ //! //! 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::{ 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, ConstraintSet}; + +use crate::constraints::templates::{AddOperand, emit_add_pair, emit_is_bit}; // ========================================================================= // Column indices for CPU32 table @@ -585,147 +583,30 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// 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 +/// 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`); +/// - 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 max_degree(&self) -> usize { + 3 } - 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( - &[ + 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, @@ -733,113 +614,110 @@ pub fn cpu32_constraints( cols::ADD, cols::SUB, cols::MU, - ], - constraint_idx_start, - ); - for c in is_bit { - constraints.push(c.boxed()); - } + ] + .iter() + .enumerate() + { + emit_is_bit(b, i, col, None); + } - // 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 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 += 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 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 += 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; - } + // 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; + } - // 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; - } + // 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); + } - (constraints, idx) + // 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/tables/dvrm.rs b/prover/src/tables/dvrm.rs index 3da78dff5..032963c82 100644 --- a/prover/src/tables/dvrm.rs +++ b/prover/src/tables/dvrm.rs @@ -27,13 +27,9 @@ //! - 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 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; @@ -974,339 +970,186 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-body constraint set (ConstraintSet front-end) // ========================================================================= - -/// 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) +// +// 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, 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: + /// `[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 } - 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 + 1 => { + let hw2 = b.main(0, hw[2]); + let hw3 = b.main(0, hw[3]); + hw2 + hw3 * shift_16 } - 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) + _ => { + // 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 } } } - /// 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(), + /// Virtual carry[i] for `n = n_sub_r + r` (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); - 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 + // carry[0] = (ext_nsr[0] + ext_r[0] - ext_n[0]) / 2^32 + (ext_nsr + ext_r - ext_n) * 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 + // 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 } } - /// 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, - ] + /// `r::DWordWL[i]` (i = 0 → lo32, else hi32); 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 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, +impl ConstraintSet for DvrmConstraints { + 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)); } - } - 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; + // 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)); + } - // DVRM-C20b: unsigned sign_d = 0 - constraints.push(DvrmConstraint::new(DvrmConstraintKind::UnsignedSignD, idx)); - idx += 1; + // 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)); + } - // 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; + // 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)); + } } - - (constraints, idx) } diff --git a/prover/src/tables/ec_scalar.rs b/prover/src/tables/ec_scalar.rs index dd8d483a2..5589a14e5 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 @@ -275,102 +270,49 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-body constraint set (ConstraintSet front-end) // ========================================================================= - -/// `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 +// +// 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, 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 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 + // 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)); } - } -} - -/// 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, + // 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)); } - .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; + // 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)); - (constraints, idx) + // 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..26bbe44e4 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,236 +258,192 @@ 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, -} +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// 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) +// then for (Lambda,C0),(Xr,C1),(Yr,C2): 64 ConvCarry (i=0..64) + 1 ColIsZero. + +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`). +pub struct EcdasConstraints; + +impl EcdasConstraints { + /// Byte `m` of the base-point order `P` (zero beyond 32 bytes). + fn p_byte_expr>( + b: &B, + m: usize, + ) -> B::Expr { + if m < 32 { + b.const_base(P_BYTES[m] as u64) + } else { + b.zero() + } + } -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 - }; + /// Byte `m` of `R` (zero beyond 33 bytes). + 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 + } - match self.relation { + /// `S_i` for `relation` at limb `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 += lam(j) * (xg(i - j) - xa(i - j)); + 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 = FieldElement::::zero(); + let mut notop_branch = b.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); + 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 + rq(cols::Q0) + 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 = FieldElement::::zero(); + let mut s = b.zero(); for j in 0..=i { - s += lam(j) * lam(i - j); + s = s + lam(j) * lam(i - j); } - s - xa(i) - xg(i) - xr(i) - (one - op) * (xa(i) - xg(i)) + rq(cols::Q1) + 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 = FieldElement::::zero(); + let mut s = b.zero(); for j in 0..=i { - s += lam(j) * (xa(i - j) - xr(i - j)); + s = s + lam(j) * (xa(i - j) - xr(i - j)); } - s - ya(i) - yr(i) + rq(cols::Q2) + s - ya(i) - yr(i) + Self::rq(b, i, 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 { + /// `256·c_i − c_{i-1} − S_i`. + 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 = step.get_main_evaluation_element(0, c_base + self.i).clone(); - let c_prev = if self.i == 0 { - FieldElement::::zero() + let c_i = b.main(0, c_base + i); + let c_prev = if i == 0 { + b.zero() } else { - step.get_main_evaluation_element(0, c_base + self.i - 1) - .clone() + b.main(0, c_base + i - 1) }; - FieldElement::::from(256u64) * c_i - c_prev - self.s_i(step) + let two_pow_8 = b.const_base(256); + two_pow_8 * c_i - c_prev - Self::s_i(b, relation, i) } } -/// `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 +impl ConstraintSet for EcdasConstraints { + // The Lambda ConvCarry has the op·(λ·Δx) term, making it degree 3. + fn max_degree(&self) -> usize { + 3 } - 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 + 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)); } - } -} - -/// 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 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); // ColIsZero c_63 idx += 1; } - constraints.push( - ColIsZero { - col: c_base + 63, - constraint_idx: idx, - } - .boxed(), - ); - idx += 1; } - - (constraints, idx) } diff --git a/prover/src/tables/ecsm.rs b/prover/src/tables/ecsm.rs index f8ec0859d..0dba13910 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)); +// ========================================================================= +// Single-body constraint set (ConstraintSet front-end) +// ========================================================================= +// +// 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)) +// 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, 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). + fn p_byte_expr>( + b: &B, + m: usize, + ) -> B::Expr { + if m < 32 { + b.const_base(P_BYTES[m] as u64) + } else { + b.zero() } - 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()) + /// `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() + } } -} - -/// `µ · (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 + /// `S_i` for `relation` at limb `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 } - 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()) + + /// `256·c_i − c_{i-1} − S_i`. + 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. + 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 } } -/// 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; +impl ConstraintSet for EcsmConstraints { + // The k usize { + 3 } - constraints.push( - ColIsZero { - col: cols::c0(63), - constraint_idx: idx, + + fn eval>(&self, b: &mut B) { + // 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 (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); + idx += 1; } - .boxed(), - ); - idx += 1; - - // yG convolution: 64 carries + closing. - for i in 0..64 { - constraints.push( - ConvCarry { - relation: Relation::Yg, - i, - constraint_idx: idx, - } - .boxed(), - ); + let c0_last = b.main(0, cols::c0(63)); + b.emit_base(idx, c0_last); idx += 1; - } - constraints.push( - ColIsZero { - col: cols::c1(63), - constraint_idx: idx, + + // 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); + idx += 1; } - .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(), - ); + let c1_last = b.main(0, cols::c1(63)); + b.emit_base(idx, c1_last); 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 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; - } - constraints.push( - OverflowRequired { - kind: OverflowKind::XrLtP, - constraint_idx: idx, + + // 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) { + // µ · 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; } - .boxed(), - ); - idx += 1; - (constraints, idx) + debug_assert_eq!(idx, 148); + } } diff --git a/prover/src/tables/eq.rs b/prover/src/tables/eq.rs index 453caa928..117d8426b 100644 --- a/prover/src/tables/eq.rs +++ b/prover/src/tables/eq.rs @@ -21,15 +21,13 @@ //! 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, 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::{AddOperand, emit_add_pair, emit_is_bit}; // ========================================================================= // Column indices for EQ table @@ -245,82 +243,33 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// 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) +/// 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`. +pub struct EqConstraints; + +impl ConstraintSet for EqConstraints { + 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)); } } - -/// 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) -} diff --git a/prover/src/tables/keccak.rs b/prover/src/tables/keccak.rs index 0f305255b..832869012 100644 --- a/prover/src/tables/keccak.rs +++ b/prover/src/tables/keccak.rs @@ -16,15 +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, 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 @@ -454,110 +452,59 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -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 +/// 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); +/// - 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 max_degree(&self) -> usize { + 3 } - fn constraint_idx(&self) -> usize { - self.constraint_idx - } + fn eval>(&self, b: &mut B) { + use crate::constraints::templates::emit_add_pair; - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) - } -} + // 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)), + ); + } -/// 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; + // 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); } - - constraints.push(KeccakAddressNoOverflowConstraint::new(idx).boxed()); - idx += 1; - - (constraints, idx) } diff --git a/prover/src/tables/keccak_rnd.rs b/prover/src/tables/keccak_rnd.rs index 279b5c152..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::transition::{TransitionConstraint, TransitionConstraintEvaluator}; +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::trace::TraceTable; @@ -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( @@ -897,38 +897,29 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// 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; +/// 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; + +impl ConstraintSet for KeccakRndConstraints { + // 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) { + 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; + } } } - (constraints, idx) } diff --git a/prover/src/tables/load.rs b/prover/src/tables/load.rs index 250d565b2..c2bf389dc 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}; @@ -476,164 +472,107 @@ pub fn bus_interactions() -> Vec { interactions } // ========================================================================= -// Constraints +// Single-body constraint set (ConstraintSet front-end) // ========================================================================= - -/// 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, - } +// +// 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 + +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`). +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) } - 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) - } - } + /// `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); + let ff = b.const_base(255); + signed * sign_bit * ff } } -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) +impl ConstraintSet for LoadConstraints { + fn max_degree(&self) -> usize { + 3 } -} -/// Creates all constraints for the LOAD table. -pub fn constraints() --> Vec>> { - let mut constraints: Vec< - Box>, - > = Vec::new(); + 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); + } - let mut idx = 0; + // 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)); + } - // 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; - } + // 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)); + } - // 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; + // 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)); } - - // Extension constraint for low byte (1): !(read2 + read4 + read8) => res[1] = extended - constraints.push(LoadConstraint::new(LoadConstraintKind::ExtensionLow, idx).boxed()); - - constraints } diff --git a/prover/src/tables/lt.rs b/prover/src/tables/lt.rs index 02ed029bd..a68191f37 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; @@ -350,256 +346,104 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-body constraint set (ConstraintSet front-end) // ========================================================================= - -/// 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; - +// +// 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, 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 = FieldElement::::from(crate::constraints::templates::INV_SHIFT_32); - (&rhs_0 + &sub_lo - &lhs_0) * &inv_2_32 + let inv_2_32 = b.const_base(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]) + /// carry[1] = (rhs_hi + sub_hi + carry_0 - lhs_hi) / 2^32. /// - /// 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); - + /// 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); + 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; - + 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; - + 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; - - // 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) - } - } + 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 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 +impl ConstraintSet for LtConstraints { + // The LT formula (idx 2) is degree 3; the rest are degree 2. + fn max_degree(&self) -> usize { + 3 } - fn evaluate(&self, step: &TableView) -> FieldElement - where - F: IsSubFieldOf, - E: IsField, - { - self.compute(step) + 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)); } } - -/// 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) -} diff --git a/prover/src/tables/memw.rs b/prover/src/tables/memw.rs index 2b240747c..4f775f535 100644 --- a/prover/src/tables/memw.rs +++ b/prover/src/tables/memw.rs @@ -27,17 +27,16 @@ //! - 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 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, ConstraintSet}; + use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::IsBitConstraint; +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. @@ -838,153 +837,61 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// 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) +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// 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, +/// `μ_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) } -/// MEMW table constraint. -pub struct MemwConstraint { - constraint_idx: usize, - kind: MemwConstraintKind, +/// `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) } -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) - } +/// 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`; +/// - 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 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; } - } -} -impl TransitionConstraint for MemwConstraint { - fn degree(&self) -> usize { - match self.kind { - MemwConstraintKind::MuSumIsBit => 2, - MemwConstraintKind::W2ImpliesMuSum => 2, - MemwConstraintKind::WidthSumIsBit => 2, + // 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; } - } - - 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; + // idx 14: IS_BIT = w2 * (1 - w2) + let one = b.one(); + let w2 = w2_expr(b); + b.emit_base(idx, w2.clone() * (one - w2)); } - constraints.push(MemwConstraint::new(MemwConstraintKind::WidthSumIsBit, idx).boxed()); - - constraints } diff --git a/prover/src/tables/memw_aligned.rs b/prover/src/tables/memw_aligned.rs index 8042d9052..b9517ec91 100644 --- a/prover/src/tables/memw_aligned.rs +++ b/prover/src/tables/memw_aligned.rs @@ -24,26 +24,26 @@ //! - 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] //! - 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, ConstraintSet}; + use super::memw::MemwOperation; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; -use crate::constraints::templates::IsBitConstraint; +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; @@ -648,93 +648,52 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints (4 total) +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// 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, +/// `μ_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) } -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) - } - } - } +/// `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) } -impl TransitionConstraint for MemwAlignedConstraint { - fn degree(&self) -> usize { - 2 - } - - fn constraint_idx(&self) -> usize { - self.constraint_idx +/// 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`; +/// - 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 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)); } - - 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(), - ] } diff --git a/prover/src/tables/memw_register.rs b/prover/src/tables/memw_register.rs index 14a696cb9..c02380c5f 100644 --- a/prover/src/tables/memw_register.rs +++ b/prover/src/tables/memw_register.rs @@ -38,15 +38,14 @@ //! - 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, ConstraintSet}; + use super::memw::MemwOperation; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; +use crate::constraints::templates::emit_is_bit; // ========================================================================= // Column indices (10 columns) @@ -359,62 +358,23 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints (3 algebraic) +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// 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 +/// 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; + +impl ConstraintSet for MemwRegisterConstraints { + 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)); } - - 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(), - ] } diff --git a/prover/src/tables/mul.rs b/prover/src/tables/mul.rs index 33679211c..2f0fa1d0e 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; @@ -457,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] @@ -684,153 +680,100 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-body constraint set (ConstraintSet front-end) // ========================================================================= - -/// 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) - } - } +// +// 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) + +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`). +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) } - /// 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, + /// `raw_product[i] − Σ_k 2^(16k)·Σ_j lhs_ext[j]·rhs_ext[idx−j]` (idx = 2i+k). + fn raw_product>( + b: &B, 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(), + ) -> 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), ]; - - // 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(), + 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. + // 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(), + 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, ]; - // 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; + // Convolution sum. + 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 = FieldElement::::zero(); + let mut inner_sum = b.zero(); for j in 0..=idx { if j < 8 && (idx - j) < 8 { - inner_sum = &inner_sum + &(&lhs_ext[j] * &rhs_ext[idx - j]); + inner_sum = inner_sum + lhs_ext[j].clone() * rhs_ext[idx - j].clone(); } } - // Multiply by 2^(16*k) if k == 0 { - sum = &sum + &inner_sum; + sum = sum + inner_sum; } else { - sum = &sum + &(&inner_sum * &shift_16); + sum = sum + inner_sum * shift_16.clone(); } } } - // Constraint: raw_product[i] - sum = 0 let raw_col = match i { 0 => cols::RAW_PRODUCT_0, 1 => cols::RAW_PRODUCT_1, @@ -838,71 +781,37 @@ impl MulConstraint { 3 => cols::RAW_PRODUCT_3, _ => unreachable!(), }; - let raw_product = step.get_main_evaluation_element(0, raw_col).clone(); - + let raw_product = b.main(0, raw_col); 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, +impl ConstraintSet for MulConstraints { + 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); } } - - 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) } // ========================================================================= diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index 3115784f6..77a8ae32a 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -6,22 +6,19 @@ //! 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 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}; @@ -222,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; @@ -541,7 +538,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, @@ -728,264 +725,174 @@ pub fn bus_interactions() -> Vec { interactions } +/// Total number of SHIFT transition constraints. +pub const NUM_SHIFT_CONSTRAINTS: usize = 19; + // ========================================================================= -// Constraints +// Single-body constraint set (ConstraintSet front-end) // ========================================================================= - -/// 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, +// +// 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, 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]`). + 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 } } - /// 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. + 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_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]. + 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 + } - // intra_limb_right[i]: Y[i]+X[i+1] - let intra_right = |i: usize| -> FieldElement { get_y(i) + get_x(i + 1) }; + /// The `shifted` virtual column at index `half_idx` (0..4). + 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); - let i = half_idx; - let zero = FieldElement::::zero(); + // 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 = zero.clone(); + // 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 += &get_ls(j) * intra_left(i - j); + left_part = left_part + Self::limb_shift(b, j) * Self::intra_left(b, i - j); } - left_part = &left * left_part; + let 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(); + // 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 += &get_ls(j) * intra_right(i + j); + right_shift_part = + right_shift_part + Self::limb_shift(b, j) * Self::intra_right(b, i + j); } - // right_ext_part = right * extension * Σ_j=(4-i)^3 limb_shift[j] - let mut ext_sum = zero.clone(); + // 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 += get_ls(j); + ext_sum = ext_sum + Self::limb_shift(b, j); } } - let right_ext_part = &extension * ext_sum; + let right_ext_part = extension * ext_sum; - let right_part = &right * (right_shift_part + right_ext_part); + 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, - } +impl ConstraintSet for ShiftConstraints { + fn max_degree(&self) -> usize { + 3 } - fn constraint_idx(&self) -> usize { - self.constraint_idx - } + 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)); - 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 -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)); - } + // 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)); + } - // C7: zbs * X[4] = 0 - push(ShiftConstraintKind::ZbsOverrideX4); + // 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); - // C9.i: zbs * (Y[i] - in[i] * right) = 0 - for i in 0..4 { - push(ShiftConstraintKind::ZbsOverrideY(i)); - } + // 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)); + } - // C10.i: IS_BIT - for i in 0..4 { - push(ShiftConstraintKind::LimbShiftIsBit(i)); - } + // 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)); + } - // C12.i: out[i] - (shifted::DWordWL)[i] = 0 - for i in 0..2 { - push(ShiftConstraintKind::OutputMatchesShifted(i)); - } + // 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); + } - // 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)); + // 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)); + } } - - debug_assert_eq!(constraints.len(), NUM_SHIFT_CONSTRAINTS); - (constraints, idx) } // ========================================================================= diff --git a/prover/src/tables/store.rs b/prover/src/tables/store.rs index 1cdf0334e..c1dfc937a 100644 --- a/prover/src/tables/store.rs +++ b/prover/src/tables/store.rs @@ -19,15 +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, ConstraintSet}; + use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; -use crate::constraints::templates::new_is_bit_constraints; +use crate::constraints::templates::emit_is_bit; // ========================================================================= // Column indices for STORE table @@ -253,85 +251,34 @@ pub fn bus_interactions() -> Vec { } // ========================================================================= -// Constraints +// Single-source constraint set (ConstraintBuilder front-end) // ========================================================================= -/// 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 - } +/// 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 ⇒ μ). +pub struct StoreConstraints; - fn constraint_idx(&self) -> usize { - self.constraint_idx - } +impl ConstraintSet for StoreConstraints { + 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); - 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) - } - } - } -} + 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; -/// 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(); + // width sum is bit: sum * (1 - sum) + let one = b.one(); + b.emit_base(4, sum.clone() * (one - sum.clone())); - 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()); + // width ⇒ μ: sum * (1 - μ) + let one = b.one(); + let mu = b.main(0, cols::MU); + b.emit_base(5, sum * (one - mu)); } - - constraints.push(StoreConstraint::new(StoreConstraintKind::WidthSumIsBit, idx).boxed()); - idx += 1; - constraints.push(StoreConstraint::new(StoreConstraintKind::WidthImpliesMu, idx).boxed()); - idx += 1; - - (constraints, idx) } 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/branch_constraints_tests.rs b/prover/src/tests/branch_constraints_tests.rs index af0b3aadb..ed9001ec5 100644 --- a/prover/src/tests/branch_constraints_tests.rs +++ b/prover/src/tests/branch_constraints_tests.rs @@ -5,50 +5,25 @@ //! - 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); + // 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 f23405b0e..fdaf4d2cd 100644 --- a/prover/src/tests/commit_tests.rs +++ b/prover/src/tests/commit_tests.rs @@ -434,27 +434,14 @@ 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. + for (i, m) in meta.iter().enumerate() { + assert_eq!(m.constraint_idx, i); } - - // 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); + // 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 new file mode 100644 index 000000000..64f03da51 --- /dev/null +++ b/prover/src/tests/constraint_emit_tests.rs @@ -0,0 +1,325 @@ +//! Folder-vs-capture-interpret regression tests for the single-body `emit_*` +//! constraint functions in `constraints::{templates, cpu}`. +//! +//! 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; +use stark::constraints::builder::{ + 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::{ + 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, emit_add_pair, emit_is_bit}; +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 + } + } + }; +} + +/// 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, max_degree: usize) { + let n = body.n(); + + // --- 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 matches the declared max --- + 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"); + // 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:?}" + ); + 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(); + + 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(); + + // --- ProverEvalFolder (base) --- + let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_ch, + &no_ch, + &offset_e, + ); + 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(); + + // --- VerifierEvalFolder (ext) --- + 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, + ); + 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(); + + // Prover folder (promoted) == verifier folder == interpreter. + for i in 0..n { + assert_eq!( + base_out[i].to_extension(), + vext_out[i], + "[{label}] prover-vs-verifier folder mismatch, constraint {i}, trial {trial}" + ); + assert_eq!( + eval_program_base(&prog, i, &row), + base_out[i], + "[{label}] interpreter mismatch, constraint {i}, trial {trial}" + ); + } + } +} + +// ============================================================================= +// templates.rs: IS_BIT +// ============================================================================= + +#[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, 2); + + emit_body!(Cond, 1, |b| { emit_is_bit(b, 0, 5, Some(3)) }); + check_emit("is_bit_conditional", &Cond, 3); +} + +// ============================================================================= +// templates.rs: ADD pair +// ============================================================================= + +/// Run the pair check for one `conditional` flag. +fn check_add_pair_case(label: &str, body: &T, conditional: bool) { + let max_degree = if conditional { 3 } else { 2 }; + check_emit(label, body, max_degree); +} + +#[test] +fn emit_add_pair_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, true); +} + +#[test] +fn emit_add_pair_linear_unconditional() { + // DWordHL repack lhs; negative-coefficient + constant linear rhs — + // exercises const_signed on both signs. + fn rhs() -> AddOperand { + AddOperand::linear( + &[ + AddLinearTerm::Column { + coefficient: -2, + column: 2, + }, + AddLinearTerm::Constant(4), + ], + &[], + ) + } + 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, false); +} + +#[test] +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| { + 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, true); +} + +// ============================================================================= +// cpu.rs: decode / assumption constraints +// ============================================================================= + +#[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, 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, 3); + + emit_body!(Body1, 1, |b| { emit_arg2_exclusive(b, 0, cols::IMM_1) }); + 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, 3); +} + +#[test] +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, 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, 2); +} + +// ============================================================================= +// cpu.rs: alu / mem / branch groups +// ============================================================================= + +#[test] +fn emit_arg2_folder_capture_agree() { + emit_body!(Body0, 1, |b| { emit_arg2(b, 0, 0) }); + check_emit("arg2_word0", &Body0, 2); + emit_body!(Body1, 1, |b| { emit_arg2(b, 0, 1) }); + 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, 2); + emit_body!(Body1, 1, |b| { emit_rvd_eq_res(b, 0, 1) }); + 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, 3); +} + +#[test] +fn emit_branch_cond_folder_capture_agree() { + emit_body!(Body, 1, |b| { emit_branch_cond(b, 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, 3); +} 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 new file mode 100644 index 000000000..89a75864a --- /dev/null +++ b/prover/src/tests/constraint_set_tests_a.rs @@ -0,0 +1,259 @@ +//! 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 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; +use stark::constraints::builder::{ + CaptureBuilder, ConstraintSet, ProverEvalFolder, RootKind, VerifierEvalFolder, + num_base_from_meta, +}; +use stark::frame::Frame; +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) + } +} + +/// 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(label: &str, set: &CS, num_cols: usize) +where + CS: ConstraintSet, +{ + let meta = set.meta(); + let n = meta.len(); + + // --- 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"); + 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 <= the table's declared max --- + // + // `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); + 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:?}" + ); + let max_degree = set.max_degree(); + for &(idx, measured) in °rees { + assert!( + measured <= max_degree, + "[{label}] constraint {idx}: tree degree {measured} EXCEEDS max_degree() {max_degree}" + ); + } + 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(); + + // --- ProverEvalFolder (base) --- + let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_ch, + &no_ch, + &offset_e, + ); + 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(); + + // --- VerifierEvalFolder (ext) --- + 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, + ); + 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!( + base_out[i].to_extension(), + vext_out[i], + "[{label}] prover-vs-verifier folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- capture → flatten → interpret == ProverEvalFolder (base) --- + for (i, expected) in base_out.iter().enumerate() { + assert_eq!( + &eval_program_base(&prog, i, &row), + expected, + "[{label}] interpreter mismatch, constraint {i}, trial {trial}" + ); + } + } +} + +// ============================================================================= +// lt.rs +// ============================================================================= + +mod lt { + use super::*; + use crate::tables::lt::{LtConstraints, cols}; + + #[test] + fn lt_constraint_set_folder_capture_agree() { + check_set("lt", &LtConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// dvrm.rs +// ============================================================================= + +mod dvrm { + use super::*; + use crate::tables::dvrm::{DvrmConstraints, cols}; + + #[test] + fn dvrm_constraint_set_folder_capture_agree() { + check_set("dvrm", &DvrmConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// shift.rs +// ============================================================================= + +mod shift { + use super::*; + use crate::tables::shift::{ShiftConstraints, cols}; + + #[test] + fn shift_constraint_set_folder_capture_agree() { + check_set("shift", &ShiftConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// mul.rs +// ============================================================================= + +mod mul { + use super::*; + use crate::tables::mul::{MulConstraints, cols}; + + #[test] + fn mul_constraint_set_folder_capture_agree() { + check_set("mul", &MulConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// load.rs +// ============================================================================= + +mod load { + use super::*; + use crate::tables::load::{LoadConstraints, cols}; + + #[test] + fn load_constraint_set_folder_capture_agree() { + check_set("load", &LoadConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// ecsm.rs +// ============================================================================= + +mod ecsm { + use super::*; + use crate::tables::ecsm::{EcsmConstraints, cols}; + + #[test] + fn ecsm_constraint_set_folder_capture_agree() { + check_set("ecsm", &EcsmConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// ecdas.rs +// ============================================================================= + +mod ecdas { + use super::*; + use crate::tables::ecdas::{EcdasConstraints, cols}; + + #[test] + fn ecdas_constraint_set_folder_capture_agree() { + check_set("ecdas", &EcdasConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// ec_scalar.rs +// ============================================================================= + +mod ec_scalar { + use super::*; + use crate::tables::ec_scalar::{EcScalarConstraints, cols}; + + #[test] + 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 new file mode 100644 index 000000000..0348c2b70 --- /dev/null +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -0,0 +1,301 @@ +//! Folder-vs-capture-interpret regression tests for the per-table +//! [`ConstraintSet`] single bodies (group B tables). +//! +//! 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 entry point. + +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::frame::Frame; +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) + } +} + +/// 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. +/// +/// * `set` — the table's [`ConstraintSet`]. +/// * `num_cols` — the table's `cols::NUM_COLUMNS`. +fn check_table>(label: &str, set: &CS, num_cols: usize) { + let meta = set.meta(); + let n = meta.len(); + + // --- meta invariants: dense, idx-ordered, all-base (group-B tables). --- + 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}"); + } + + // --- 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); + 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:?}" + ); + let max_degree = set.max_degree(); + for &(idx, measured) in °rees { + assert!( + measured <= max_degree, + "[{label}] constraint {idx}: tree degree {measured} EXCEEDS max_degree() {max_degree}" + ); + } + 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(); + + // --- ProverEvalFolder (base) --- + let frame = Frame::::new(vec![TableView::new(vec![row.clone()], vec![vec![]])]); + let ctx = TransitionEvaluationContext::new_prover( + frame.as_row_frame(), + &no_ch, + &no_ch, + &offset_e, + ); + 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(); + + // --- VerifierEvalFolder (ext) --- + 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, + ); + 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: 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!( + &b.to_extension(), + v, + "[{label}] prover-vs-verifier folder mismatch, constraint {i}, trial {trial}" + ); + } + + // --- capture → flatten → interpret == ProverEvalFolder (base) --- + for (i, want) in base_out.iter().enumerate() { + assert_eq!( + &eval_program_base(&prog, i, &row), + want, + "[{label}] interpreter mismatch, constraint {i}, trial {trial}" + ); + } + } +} + +// ============================================================================= +// eq.rs +// ============================================================================= + +mod eq { + use super::*; + use crate::tables::eq::{EqConstraints, cols}; + + #[test] + fn eq_constraint_set_folder_capture_agree() { + check_table("eq", &EqConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// store.rs +// ============================================================================= + +mod store { + use super::*; + use crate::tables::store::{StoreConstraints, cols}; + + #[test] + fn store_constraint_set_folder_capture_agree() { + check_table("store", &StoreConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// memw.rs +// ============================================================================= + +mod memw { + use super::*; + use crate::tables::memw::{MemwConstraints, cols}; + + #[test] + fn memw_constraint_set_folder_capture_agree() { + check_table("memw", &MemwConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// memw_aligned.rs +// ============================================================================= + +mod memw_aligned { + use super::*; + use crate::tables::memw_aligned::{MemwAlignedConstraints, cols}; + + #[test] + fn memw_aligned_constraint_set_folder_capture_agree() { + check_table("memw_aligned", &MemwAlignedConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// memw_register.rs +// ============================================================================= + +mod memw_register { + use super::*; + use crate::tables::memw_register::{MemwRegisterConstraints, cols}; + + #[test] + fn memw_register_constraint_set_folder_capture_agree() { + check_table("memw_register", &MemwRegisterConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// branch.rs +// ============================================================================= + +mod branch { + use super::*; + use crate::tables::branch::{BranchConstraints, cols}; + + #[test] + fn branch_constraint_set_folder_capture_agree() { + check_table("branch", &BranchConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// commit.rs +// ============================================================================= + +mod commit { + use super::*; + use crate::tables::commit::{CommitConstraints, cols}; + + #[test] + fn commit_constraint_set_folder_capture_agree() { + check_table("commit", &CommitConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// keccak.rs +// ============================================================================= + +mod keccak { + use super::*; + use crate::tables::keccak::{KeccakConstraints, cols}; + + #[test] + fn keccak_constraint_set_folder_capture_agree() { + check_table("keccak", &KeccakConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// keccak_rnd.rs +// ============================================================================= + +mod keccak_rnd { + use super::*; + use crate::tables::keccak_rnd::{KeccakRndConstraints, cols}; + + #[test] + fn keccak_rnd_constraint_set_folder_capture_agree() { + check_table("keccak_rnd", &KeccakRndConstraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// cpu32.rs +// ============================================================================= + +mod cpu32 { + use super::*; + use crate::tables::cpu32::{Cpu32Constraints, cols}; + + #[test] + fn cpu32_constraint_set_folder_capture_agree() { + check_table("cpu32", &Cpu32Constraints, cols::NUM_COLUMNS); + } +} + +// ============================================================================= +// 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}; + use crate::tables::cpu::cols; + + #[test] + 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..0caf71264 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 // ========================================================================= @@ -366,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 } => { @@ -403,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, @@ -417,7 +358,7 @@ fn test_add_operand_linear_with_nonzero_hi() { column: 2, }, ], - vec![ + &[ AddLinearTerm::Column { coefficient: 1 << 16, column: 3, @@ -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..2b683cdfd 100644 --- a/prover/src/tests/cpu32_tests.rs +++ b/prover/src/tests/cpu32_tests.rs @@ -2,14 +2,35 @@ //! 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::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 no_e: Vec> = vec![]; + let offset_e = FieldElement::::zero(); + 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); + Cpu32Constraints.eval(&mut folder); + base +} #[test] fn test_aux_signed_input_extension() { @@ -124,13 +145,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 +162,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 +186,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 6dfbe34c5..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::{ - 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::{ @@ -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!( @@ -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..f8a19cf79 100644 --- a/prover/src/tests/ec_scalar_tests.rs +++ b/prover/src/tests/ec_scalar_tests.rs @@ -1,24 +1,37 @@ //! 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::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 no_e: Vec> = vec![]; + let offset_e = FieldElement::::zero(); + 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); + EcScalarConstraints.eval(&mut folder); + base } #[test] @@ -32,41 +45,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 +66,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..d50cf9abd 100644 --- a/prover/src/tests/ecdas_tests.rs +++ b/prover/src/tests/ecdas_tests.rs @@ -1,16 +1,16 @@ -//! 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::table::TableView; use stark::trace::TraceTable; +use stark::traits::TransitionEvaluationContext; fn gx_le() -> [u8; 32] { let mut be = [ @@ -43,14 +43,38 @@ 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 no_e: Vec> = vec![]; + let offset_e = FieldElement::::zero(); + 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); + 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 +87,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 +107,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..9b98f8934 100644 --- a/prover/src/tests/ecsm_tests.rs +++ b/prover/src/tests/ecsm_tests.rs @@ -1,16 +1,15 @@ -//! 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::table::TableView; use stark::trace::TraceTable; +use stark::traits::TransitionEvaluationContext; fn gx_le() -> [u8; 32] { // secp256k1 Gx, little-endian. @@ -40,17 +39,30 @@ 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 no_e: Vec> = vec![]; + let offset_e = FieldElement::::zero(); + 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); + 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 +72,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/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..d7f707a13 100644 --- a/prover/src/tests/lt_tests.rs +++ b/prover/src/tests/lt_tests.rs @@ -3,7 +3,7 @@ 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}; use crate::tables::types::FE; use crate::test_utils::{busless_air, create_lt_air, in_chip_constraint_count, validate_busless}; @@ -182,7 +182,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), @@ -206,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`, @@ -217,7 +218,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/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/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" 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" 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)." 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..3bbdeae28 --- /dev/null +++ b/thoughts/gpu-constraint-eval/impl-plan-single-source-constraints.md @@ -0,0 +1,562 @@ +# 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. **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 + 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) | + +## 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. 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.