Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ fn cmd_verify(proof_path: PathBuf, elf_path: PathBuf, blowup: Option<u8>, 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),
};
Expand Down
41 changes: 37 additions & 4 deletions prover/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -328,12 +329,26 @@ 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 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. 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,
minimal_bitwise: bool,
page_configs: &[crate::tables::page::PageConfig],
table_counts: &TableCounts,
decode_commitment: Option<Commitment>,
) -> Self {
let cpus: Vec<_> = (0..table_counts.cpu)
.map(|i| create_cpu_air(proof_options).with_name(&format!("CPU[{}]", i)))
Expand Down Expand Up @@ -361,11 +376,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();
Expand Down Expand Up @@ -646,6 +662,7 @@ pub fn prove_with_options_and_inputs(
false,
&traces.page_configs,
&table_counts,
None,
);

#[cfg(feature = "instruments")]
Expand Down Expand Up @@ -717,6 +734,7 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result<bool, Error> {
vm_proof,
elf_bytes,
&GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"),
None,
)
}

Expand All @@ -725,10 +743,24 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result<bool, Error> {
/// 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 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
/// caller's compiled binary (e.g. a `const [u8; 32]`), never from prover-
/// 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],
proof_options: &ProofOptions,
decode_commitment: Option<Commitment>,
) -> Result<bool, Error> {
// Validate table_counts before constructing AIRs.
// A malicious prover could set counts to 0, removing entire constraint sets.
Expand Down Expand Up @@ -774,6 +806,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.
Expand Down
23 changes: 20 additions & 3 deletions prover/src/tables/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,20 @@ pub fn bus_interactions() -> Vec<BusInteraction> {
/// 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<elf_hash, Commitment>` at the caller site. Useful for native
/// verifiers that check many proofs of the same ELF in one process.
/// * **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.
///
/// ## Arguments
/// * `instructions` - The program's instruction map (PC → Instruction)
Expand Down Expand Up @@ -320,7 +332,12 @@ pub fn instructions_from_elf(elf: &Elf) -> Result<U64HashMap<Instruction>, 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,
Expand Down
113 changes: 111 additions & 2 deletions prover/src/tests/decode_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1046,6 +1050,7 @@ fn test_decode_soundness_same_elf_accepted() {
false,
&traces.page_configs,
&table_counts,
None,
);

let proof = multi_prove_ram(
Expand All @@ -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::<E>::new(&[]);
Expand Down Expand Up @@ -1155,3 +1161,106 @@ 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",
);
}

#[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",
);
}

/// 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?}");
}
8 changes: 4 additions & 4 deletions prover/src/tests/disk_spill_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
Expand All @@ -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)"
);
}
Loading
Loading