From 1072a11259e203084fa59ff7d5f9efebf5dd0d64 Mon Sep 17 00:00:00 2001 From: Nicole Date: Fri, 29 May 2026 16:53:22 -0300 Subject: [PATCH 1/4] Add optional decode commitment parameter to verify_with_options and VmAirs::new --- bin/cli/src/main.rs | 2 +- prover/src/lib.rs | 40 ++++++++++++++++-- prover/src/tables/decode.rs | 24 +++++++++-- prover/src/tests/decode_tests.rs | 54 +++++++++++++++++++++++- prover/src/tests/disk_spill_tests.rs | 8 ++-- prover/src/tests/prove_elfs_tests.rs | 63 ++++++++++++++++++++-------- 6 files changed, 160 insertions(+), 31 deletions(-) diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index bdcea9518..66ee8325a 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -500,7 +500,7 @@ fn cmd_verify(proof_path: PathBuf, elf_path: PathBuf, blowup: Option, time: return ExitCode::FAILURE; } }; - prover::verify_with_options(&proof, &elf_data, &opts) + prover::verify_with_options(&proof, &elf_data, &opts, None) } None => prover::verify(&proof, &elf_data), }; diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 14f35cdf8..8ac7d4be6 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -30,6 +30,7 @@ use crypto::fiat_shamir::is_transcript::IsTranscript; use executor::elf::Elf; use executor::vm::execution::Executor; use math::field::element::FieldElement; +use stark::config::Commitment; use stark::prover::{IsStarkProver, Prover}; #[cfg(feature = "disk-spill")] use stark::storage_mode::StorageMode; @@ -328,12 +329,25 @@ impl VmAirs { /// /// `page_configs` provides the page base addresses for creating PAGE AIRs. /// `table_counts` specifies how many chunks for each split table. + /// + /// `decode_commitment` is an optional precomputed DECODE preprocessed + /// commitment. When `Some`, the supplied value is used directly and the + /// FFT + Merkle build is skipped — useful for callers who have already + /// computed the commitment offline and hardcoded it (e.g. the recursion + /// guest, where the in-VM recompute is too expensive). When `None`, the + /// commitment is computed from the ELF. + /// + /// The trust anchor for `decode_commitment` is the caller's compiled + /// binary — never accept prover-supplied bytes here. Wrong values + /// surface as Fiat-Shamir transcript divergence (proof rejected), + /// never as silently-accepted wrong proofs. pub fn new( elf: &Elf, proof_options: &ProofOptions, minimal_bitwise: bool, page_configs: &[crate::tables::page::PageConfig], table_counts: &TableCounts, + decode_commitment: Option, ) -> Self { let cpus: Vec<_> = (0..table_counts.cpu) .map(|i| create_cpu_air(proof_options).with_name(&format!("CPU[{}]", i))) @@ -361,11 +375,12 @@ impl VmAirs { let loads: Vec<_> = (0..table_counts.load) .map(|i| create_load_air(proof_options).with_name(&format!("LOAD[{}]", i))) .collect(); - let decode = create_decode_air(proof_options).with_preprocessed( + let decode_root = decode_commitment.unwrap_or_else(|| { decode::commitment_from_elf(elf, proof_options) - .expect("Failed to compute decode commitment"), - decode::NUM_PRECOMPUTED_COLS, - ); + .expect("Failed to compute decode commitment") + }); + let decode = 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))) .collect(); @@ -646,6 +661,7 @@ pub fn prove_with_options_and_inputs( false, &traces.page_configs, &table_counts, + None, ); #[cfg(feature = "instruments")] @@ -717,6 +733,7 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result { vm_proof, elf_bytes, &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), + None, ) } @@ -725,10 +742,24 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result { /// The verifier enforces its own `proof_options` (security parameters), /// ignoring the options embedded in the proof bundle. This prevents a /// malicious prover from weakening the security level. +/// +/// `decode_commitment` is an optional precomputed DECODE preprocessed +/// commitment. When `Some`, the supplied value is used directly and the +/// in-verifier FFT + Merkle build for the DECODE preprocessed columns is +/// skipped — useful for callers (e.g. the recursion guest) that hardcode +/// the commitment in their compiled binary to avoid the in-VM recompute +/// cost. When `None`, the verifier computes the commitment from the ELF. +/// +/// Trust model: `decode_commitment`, when supplied, must come from the +/// caller's compiled binary (e.g. a `const [u8; 32]`), never from prover- +/// supplied bytes. Wrong values surface as Fiat-Shamir transcript +/// divergence (proof rejected); they cannot cause silently-accepted wrong +/// proofs. pub fn verify_with_options( vm_proof: &VmProof, elf_bytes: &[u8], proof_options: &ProofOptions, + decode_commitment: Option, ) -> Result { // Validate table_counts before constructing AIRs. // A malicious prover could set counts to 0, removing entire constraint sets. @@ -774,6 +805,7 @@ pub fn verify_with_options( false, &page_configs, &vm_proof.table_counts, + decode_commitment, ); // Recompute the COMMIT output bus offset from VmProof.public_output. diff --git a/prover/src/tables/decode.rs b/prover/src/tables/decode.rs index e68191002..95ea2744a 100644 --- a/prover/src/tables/decode.rs +++ b/prover/src/tables/decode.rs @@ -238,8 +238,21 @@ pub fn bus_interactions() -> Vec { /// columns (PC_0, PC_1, PACKED_DECODE, IMM_0, IMM_1), matching exactly how the prover /// commits to traces. /// -/// Used by both prover (sanity check) and verifier (soundness check). The verifier -/// computes this from the program and checks that the proof's commitment matches. +/// Used by both prover (sanity check) and verifier (soundness check). Pure +/// library function — no caching, no side effects. Callers manage their own +/// caching, hardcoding, or recomputation policy as needed: +/// +/// * **Always recompute**: call this function (or [`commitment_from_elf`]) +/// on every verify. Simple and slow. +/// * **Cache once per process**: wrap the call in a `OnceLock` / +/// `HashMap` at the caller site. Useful for native +/// verifiers that check many proofs of the same ELF in one process. +/// * **Hardcoded at compile time**: call this function once offline (e.g. +/// via the `#[ignore]`d `print_decode_commitments_for_basic_program` test +/// in `static_commitments_tests.rs`, or a one-off test in the consumer +/// crate), then paste the resulting bytes as a `const` in the caller's +/// source. Useful for the recursion guest where in-VM recomputation is +/// too expensive. /// /// ## Arguments /// * `instructions` - The program's instruction map (PC → Instruction) @@ -320,7 +333,12 @@ pub fn instructions_from_elf(elf: &Elf) -> Result, Instr /// Compute DECODE commitment directly from an ELF. /// -/// This is what the verifier uses - no executor needed. +/// Thin convenience wrapper around [`instructions_from_elf`] + [`compute_precomputed_commitment`]. +/// Pure library function — no caching, always recomputes. Callers that need +/// caching, hardcoding, or a different policy should wrap this call at their +/// site (see [`compute_precomputed_commitment`] for the policy options). +/// +/// This is what the verifier uses — no executor needed. pub fn commitment_from_elf( elf: &Elf, options: &ProofOptions, diff --git a/prover/src/tests/decode_tests.rs b/prover/src/tests/decode_tests.rs index c6a436c95..22a0eb95f 100644 --- a/prover/src/tests/decode_tests.rs +++ b/prover/src/tests/decode_tests.rs @@ -5,14 +5,18 @@ use executor::vm::instruction::decoding::{ArithOp, Instruction}; use executor::vm::memory::U64HashMap; use math::field::element::FieldElement; +use stark::proof::options::GoldilocksCubicProofOptions; + use crate::tables::decode::{ - DecodeEntry, bus_interactions, cols, generate_decode_trace, instructions_from_elf, - tables_from_elf, update_multiplicities, + DecodeEntry, bus_interactions, cols, commitment_from_elf, generate_decode_trace, + instructions_from_elf, tables_from_elf, update_multiplicities, }; use crate::tables::trace_builder::Traces; use crate::tables::types::{FE, packed_decode as bits}; +use crate::test_utils::asm_elf_bytes; use crate::test_utils::multi_prove_ram; use crate::test_utils::run_asm_elf; +use crate::{prove, verify_with_options}; // ========================================================================= // Packed decode tests @@ -1046,6 +1050,7 @@ fn test_decode_soundness_same_elf_accepted() { false, &traces.page_configs, &table_counts, + None, ); let proof = multi_prove_ram( @@ -1062,6 +1067,7 @@ fn test_decode_soundness_same_elf_accepted() { false, &traces.page_configs, &table_counts, + None, ); let verifier_air_refs = verifier_airs.air_refs(); let mut replay_transcript = DefaultTranscript::::new(&[]); @@ -1155,3 +1161,47 @@ fn test_tables_from_elf_empty() { .contains_key(&crate::tables::cpu::CPU_PADDING_PC) ); } + +// ========================================================================= +// verify_with_options: optional decode_commitment parameter +// ========================================================================= + +#[test] +fn decode_commitment_some_matches_default_path() { + let elf_bytes = asm_elf_bytes("sub"); + let vm_proof = prove(&elf_bytes).expect("prove failed"); + let elf = Elf::load(&elf_bytes).expect("ELF load"); + let options = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 valid"); + + let decode_c = commitment_from_elf(&elf, &options).expect("decode commitment"); + + let default_ok = verify_with_options(&vm_proof, &elf_bytes, &options, None) + .expect("verify with None should not error"); + let explicit_ok = verify_with_options(&vm_proof, &elf_bytes, &options, Some(decode_c)) + .expect("verify with Some(correct) should not error"); + + assert!(default_ok, "default path must accept the proof"); + assert!( + explicit_ok, + "Some(correct_commitment) must accept the proof" + ); +} + +#[test] +fn decode_commitment_wrong_value_rejects() { + let elf_bytes = asm_elf_bytes("sub"); + let vm_proof = prove(&elf_bytes).expect("prove failed"); + let elf = Elf::load(&elf_bytes).expect("ELF load"); + let options = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 valid"); + + // Flip a byte in the correct commitment so the Fiat-Shamir transcripts diverge. + let mut wrong = commitment_from_elf(&elf, &options).expect("decode commitment"); + wrong[0] ^= 0xFF; + + let result = verify_with_options(&vm_proof, &elf_bytes, &options, Some(wrong)) + .expect("verify must not return Err — Fiat-Shamir mismatch is Ok(false)"); + assert!( + !result, + "tampered decode commitment must cause Fiat-Shamir rejection", + ); +} diff --git a/prover/src/tests/disk_spill_tests.rs b/prover/src/tests/disk_spill_tests.rs index e019fa456..2d55a35b9 100644 --- a/prover/src/tests/disk_spill_tests.rs +++ b/prover/src/tests/disk_spill_tests.rs @@ -25,14 +25,14 @@ fn test_disk_spill_prove_verify_and_roundtrip_small() { let proof = crate::prove_with_options(&elf_bytes, &opts, &MaxRowsConfig::default()) .expect("prove failed"); assert!( - crate::verify_with_options(&proof, &elf_bytes, &opts).expect("verify failed"), + crate::verify_with_options(&proof, &elf_bytes, &opts, None).expect("verify failed"), "verification returned false" ); let bytes = bincode::serialize(&proof).expect("serialize failed"); let proof2: VmProof = bincode::deserialize(&bytes).expect("deserialize failed"); assert!( - crate::verify_with_options(&proof2, &elf_bytes, &opts).expect("verify failed"), + crate::verify_with_options(&proof2, &elf_bytes, &opts, None).expect("verify failed"), "verification failed after serialization roundtrip" ); } @@ -45,14 +45,14 @@ fn test_disk_spill_prove_verify_and_roundtrip_chunked() { let proof = crate::prove_with_options(&elf_bytes, &opts, &MaxRowsConfig::small()) .expect("prove failed"); assert!( - crate::verify_with_options(&proof, &elf_bytes, &opts).expect("verify failed"), + crate::verify_with_options(&proof, &elf_bytes, &opts, None).expect("verify failed"), "verification returned false" ); let bytes = bincode::serialize(&proof).expect("serialize failed"); let proof2: VmProof = bincode::deserialize(&bytes).expect("deserialize failed"); assert!( - crate::verify_with_options(&proof2, &elf_bytes, &opts).expect("verify failed"), + crate::verify_with_options(&proof2, &elf_bytes, &opts, None).expect("verify failed"), "verification failed after serialization roundtrip (chunked)" ); } diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 1cc1de8af..01a096c8d 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -55,6 +55,7 @@ fn prove_and_verify_vm_minimal(elf: &Elf, traces: &mut Traces) -> bool { true, &traces.page_configs, &table_counts, + None, ); // Build air_trace_pairs for all tables @@ -991,6 +992,7 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { true, &traces.page_configs, &table_counts, + None, ); let proof = multi_prove_ram( prover_airs.air_trace_pairs(&mut traces), @@ -1000,8 +1002,14 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { // Verifier uses EMPTY runtime pages → missing stack/public-output pages let wrong_configs = Traces::page_configs_from_elf_and_runtime(&elf, &[], 0); - let verifier_airs = - crate::VmAirs::new(&elf, &proof_options, true, &wrong_configs, &table_counts); + let verifier_airs = crate::VmAirs::new( + &elf, + &proof_options, + true, + &wrong_configs, + &table_counts, + None, + ); let verifier_air_refs = verifier_airs.air_refs(); let mut replay_transcript = DefaultTranscript::::new(&[]); let expected_bus_balance = crate::compute_expected_commit_bus_balance( @@ -1031,7 +1039,7 @@ fn test_verify_rejects_tampered_public_output() { let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &Default::default()) .expect("Prover should succeed for test_commit_4"); assert!( - crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) + crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, None) .expect("Valid commit proof should verify"), "Baseline proof should verify before tampering" ); @@ -1043,7 +1051,7 @@ fn test_verify_rejects_tampered_public_output() { ..vm_proof }; - let verified = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options) + let verified = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, None) .expect("Verifier should not error on tampered public output"); assert!( !verified, @@ -1730,6 +1738,7 @@ fn test_deep_stack_runtime_pages_roundtrip() { true, &traces.page_configs, &table_counts, + None, ); let proof = multi_prove_ram( prover_airs.air_trace_pairs(&mut traces), @@ -1738,8 +1747,14 @@ fn test_deep_stack_runtime_pages_roundtrip() { .expect("Prover failed"); // Verifier reconstructs from ELF + runtime_page_ranges hint let verifier_configs = Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, 0); - let verifier_airs = - crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); + let verifier_airs = crate::VmAirs::new( + &elf, + &proof_options, + true, + &verifier_configs, + &table_counts, + None, + ); let verifier_air_refs = verifier_airs.air_refs(); let mut replay_transcript = DefaultTranscript::::new(&[]); let expected_bus_balance = crate::compute_expected_commit_bus_balance( @@ -1787,6 +1802,7 @@ fn test_deep_stack_missing_pages_rejected() { true, &traces.page_configs, &table_counts, + None, ); let proof = multi_prove_ram( prover_airs.air_trace_pairs(&mut traces), @@ -1795,8 +1811,14 @@ fn test_deep_stack_missing_pages_rejected() { .expect("Prover failed"); // Verifier uses EMPTY runtime_page_ranges → missing stack/heap pages let wrong_configs = Traces::page_configs_from_elf_and_runtime(&elf, &[], 0); - let verifier_airs = - crate::VmAirs::new(&elf, &proof_options, true, &wrong_configs, &table_counts); + let verifier_airs = crate::VmAirs::new( + &elf, + &proof_options, + true, + &wrong_configs, + &table_counts, + None, + ); let verifier_air_refs = verifier_airs.air_refs(); let mut replay_transcript = DefaultTranscript::::new(&[]); let expected_bus_balance = crate::compute_expected_commit_bus_balance( @@ -1879,6 +1901,7 @@ fn test_heap_alloc_runtime_pages_roundtrip() { true, &traces.page_configs, &table_counts, + None, ); let proof = multi_prove_ram( prover_airs.air_trace_pairs(&mut traces), @@ -1887,8 +1910,14 @@ fn test_heap_alloc_runtime_pages_roundtrip() { .expect("Prover failed"); // Verifier reconstructs from ELF + runtime hint (ranges decoded to pages) let verifier_configs = Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, 0); - let verifier_airs = - crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); + let verifier_airs = crate::VmAirs::new( + &elf, + &proof_options, + true, + &verifier_configs, + &table_counts, + None, + ); let verifier_air_refs = verifier_airs.air_refs(); let mut replay_transcript = DefaultTranscript::::new(&[]); let expected_bus_balance = crate::compute_expected_commit_bus_balance( @@ -1948,7 +1977,7 @@ fn test_verify_rejects_zero_table_counts() { .expect("Prover should succeed on valid program"); assert!( - crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) + crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, None) .expect("Verification should not error on valid proof"), "Valid proof should verify" ); @@ -1969,7 +1998,7 @@ fn test_verify_rejects_zero_table_counts() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, None); assert!(result.is_err(), "Got {:?}", result); } @@ -1990,7 +2019,7 @@ fn test_verify_rejects_zero_cpu_count() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, None); assert!(result.is_err(), "Got {:?}", result); } @@ -2011,7 +2040,7 @@ fn test_verify_rejects_zero_memw_count() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, None); assert!(result.is_err(), "Got {:?}", result); } @@ -2034,7 +2063,7 @@ fn test_crafted_zero_count_proof_must_not_verify() { branch: 0, memw_register: 0, }; - let airs = VmAirs::new(&elf, &proof_options, true, &[], &zero_counts); + let airs = VmAirs::new(&elf, &proof_options, true, &[], &zero_counts, None); let verifier_air_refs = airs.air_refs(); assert_eq!(verifier_air_refs.len(), 8); @@ -2086,7 +2115,7 @@ fn test_small_max_rows_splits_tables() { vm_proof.table_counts.cpu ); - let verified = crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) + let verified = crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, None) .expect("Verifier should not error"); assert!(verified, "Proof with small max_rows should verify"); } @@ -2139,7 +2168,7 @@ fn test_verify_rejects_inflated_table_counts() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, None); assert!( result.is_err(), "Inflated table_counts should be rejected, got {:?}", From 70bb4a9f1e837bdaf1ed99fd61098a119e6a6d33 Mon Sep 17 00:00:00 2001 From: Nicole Date: Mon, 1 Jun 2026 13:17:20 -0300 Subject: [PATCH 2/4] change hardcoded doc and add test --- prover/src/lib.rs | 10 +++++----- prover/src/tables/decode.rs | 7 +++---- prover/src/tests/decode_tests.rs | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 8ac7d4be6..b7d771fad 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -333,9 +333,9 @@ impl VmAirs { /// `decode_commitment` is an optional precomputed DECODE preprocessed /// commitment. When `Some`, the supplied value is used directly and the /// FFT + Merkle build is skipped — useful for callers who have already - /// computed the commitment offline and hardcoded it (e.g. the recursion - /// guest, where the in-VM recompute is too expensive). When `None`, the - /// commitment is computed from the ELF. + /// computed the commitment offline and embedded it as a compile-time + /// constant (e.g. the recursion guest, where the in-VM recompute is too + /// expensive). When `None`, the commitment is computed from the ELF. /// /// The trust anchor for `decode_commitment` is the caller's compiled /// binary — never accept prover-supplied bytes here. Wrong values @@ -746,8 +746,8 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result { /// `decode_commitment` is an optional precomputed DECODE preprocessed /// commitment. When `Some`, the supplied value is used directly and the /// in-verifier FFT + Merkle build for the DECODE preprocessed columns is -/// skipped — useful for callers (e.g. the recursion guest) that hardcode -/// the commitment in their compiled binary to avoid the in-VM recompute +/// skipped — useful for callers (e.g. the recursion guest) that embed the +/// commitment as a compile-time constant to avoid the in-VM recompute /// cost. When `None`, the verifier computes the commitment from the ELF. /// /// Trust model: `decode_commitment`, when supplied, must come from the diff --git a/prover/src/tables/decode.rs b/prover/src/tables/decode.rs index 95ea2744a..4805ffc42 100644 --- a/prover/src/tables/decode.rs +++ b/prover/src/tables/decode.rs @@ -247,10 +247,9 @@ pub fn bus_interactions() -> Vec { /// * **Cache once per process**: wrap the call in a `OnceLock` / /// `HashMap` at the caller site. Useful for native /// verifiers that check many proofs of the same ELF in one process. -/// * **Hardcoded at compile time**: call this function once offline (e.g. -/// via the `#[ignore]`d `print_decode_commitments_for_basic_program` test -/// in `static_commitments_tests.rs`, or a one-off test in the consumer -/// crate), then paste the resulting bytes as a `const` in the caller's +/// * **Compile-time constant**: call this function once offline (e.g. from +/// a one-off test in the consumer crate that prints the result), then +/// store the resulting bytes as a `const [u8; 32]` in the caller's /// source. Useful for the recursion guest where in-VM recomputation is /// too expensive. /// diff --git a/prover/src/tests/decode_tests.rs b/prover/src/tests/decode_tests.rs index 22a0eb95f..1f885f1d1 100644 --- a/prover/src/tests/decode_tests.rs +++ b/prover/src/tests/decode_tests.rs @@ -1205,3 +1205,19 @@ fn decode_commitment_wrong_value_rejects() { "tampered decode commitment must cause Fiat-Shamir rejection", ); } + +#[test] +fn decode_commitment_zero_bytes_rejects() { + let elf_bytes = asm_elf_bytes("sub"); + let vm_proof = prove(&elf_bytes).expect("prove failed"); + let options = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 valid"); + + // [0u8; 32] is the most plausible accidental default — passing it must + // not pass verification. + let result = verify_with_options(&vm_proof, &elf_bytes, &options, Some([0u8; 32])) + .expect("verify must not return Err — Fiat-Shamir mismatch is Ok(false)"); + assert!( + !result, + "all-zero decode commitment must cause Fiat-Shamir rejection", + ); +} From aa54ecaacd56841389f4f2f2f739c5b9a6bbf4df Mon Sep 17 00:00:00 2001 From: Nicole Date: Mon, 1 Jun 2026 15:55:44 -0300 Subject: [PATCH 3/4] add tests with const --- prover/src/tests/decode_tests.rs | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/prover/src/tests/decode_tests.rs b/prover/src/tests/decode_tests.rs index 1f885f1d1..84ae8ff3a 100644 --- a/prover/src/tests/decode_tests.rs +++ b/prover/src/tests/decode_tests.rs @@ -1221,3 +1221,46 @@ fn decode_commitment_zero_bytes_rejects() { "all-zero decode commitment must cause Fiat-Shamir rejection", ); } + +/// DECODE preprocessed commitment for the `sub` asm test ELF at blowup=2, +/// computed offline once. Mirrors how the recursion guest embeds the +/// commitment as a compile-time constant for its inner program. If the +/// AIR or FFT pipeline changes, this drifts and the test fails — +/// regenerate via the `print_decode_commitment_for_sub` helper below. +const SUB_DECODE_COMMITMENT_BLOWUP_2: [u8; 32] = [ + 0x00, 0x83, 0x59, 0xa3, 0x34, 0x5f, 0x86, 0x79, 0x59, 0x71, 0xc8, 0x71, 0x54, 0x2c, 0xc4, 0xac, + 0x8b, 0x9c, 0x48, 0x9b, 0x25, 0xa3, 0x6a, 0xc7, 0x48, 0xee, 0x71, 0xe6, 0x77, 0xfb, 0x59, 0xfa, +]; + +#[test] +fn decode_commitment_compile_time_const_accepts() { + let elf_bytes = asm_elf_bytes("sub"); + let vm_proof = prove(&elf_bytes).expect("prove failed"); + let options = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 valid"); + + // Pass the OFFLINE-COMPUTED const directly — mimics the recursion guest's + // workflow where the value lives in the caller's compiled binary. + let result = verify_with_options( + &vm_proof, + &elf_bytes, + &options, + Some(SUB_DECODE_COMMITMENT_BLOWUP_2), + ) + .expect("verify must not return Err"); + assert!( + result, + "verifier must accept the offline-computed decode commitment", + ); +} + +#[test] +#[ignore = "prints decode commitment for the sub asm ELF so SUB_DECODE_COMMITMENT_BLOWUP_2 \ + can be regenerated; run with --ignored --nocapture"] +fn print_decode_commitment_for_sub() { + let elf_bytes = asm_elf_bytes("sub"); + let elf = Elf::load(&elf_bytes).expect("ELF load"); + let options = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 valid"); + let c = commitment_from_elf(&elf, &options).expect("decode commitment"); + eprintln!("SUB_DECODE_COMMITMENT_BLOWUP_2 (sub.elf, blowup=2):"); + eprintln!("{c:02x?}"); +} From 0a449c5c9a03f3caa8e875c6ac38c4b5d8f0b2d2 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Tue, 2 Jun 2026 16:22:29 -0300 Subject: [PATCH 4/4] docs: clarify decode_commitment rejection is mismatch or FS divergence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A wrong supplied decode_commitment is caught either by the verifier's explicit precomputed-commitment equality check or by Fiat-Shamir transcript divergence — not by FS alone. Reword both doc comments to reflect the "or", since the earlier wording attributed rejection solely to Fiat-Shamir. --- prover/src/lib.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index b7d771fad..aaefc60ed 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -338,9 +338,10 @@ impl VmAirs { /// expensive). When `None`, the commitment is computed from the ELF. /// /// The trust anchor for `decode_commitment` is the caller's compiled - /// binary — never accept prover-supplied bytes here. Wrong values - /// surface as Fiat-Shamir transcript divergence (proof rejected), - /// never as silently-accepted wrong proofs. + /// binary — never accept prover-supplied bytes here. A wrong value is + /// rejected, never silently accepted: it either mismatches the prover's + /// committed precomputed root (an explicit verifier check) or yields + /// diverging Fiat-Shamir challenges. pub fn new( elf: &Elf, proof_options: &ProofOptions, @@ -752,9 +753,9 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result { /// /// Trust model: `decode_commitment`, when supplied, must come from the /// caller's compiled binary (e.g. a `const [u8; 32]`), never from prover- -/// supplied bytes. Wrong values surface as Fiat-Shamir transcript -/// divergence (proof rejected); they cannot cause silently-accepted wrong -/// proofs. +/// supplied bytes. A wrong value is rejected, never silently accepted: it +/// either mismatches the prover's committed precomputed root (an explicit +/// verifier check) or yields diverging Fiat-Shamir challenges. pub fn verify_with_options( vm_proof: &VmProof, elf_bytes: &[u8],