diff --git a/Cargo.lock b/Cargo.lock index 221876c56..cfb376c01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2159,6 +2159,7 @@ dependencies = [ "executor", "math", "rayon", + "serde", "stark", ] diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 550ef3721..70cac9aad 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -11,8 +11,7 @@ use executor::{ flamegraph::FlamegraphGenerator, vm::execution::Executor, }; -use prover::tables::types::{GoldilocksExtension, GoldilocksField}; -use stark::proof::stark::MultiProof; +use prover::VmProof; #[derive(Parser)] #[command(author, version, about = "Lambda VM - RISC-V zkVM", long_about = None)] @@ -216,14 +215,13 @@ fn cmd_verify(proof_path: PathBuf, elf_path: PathBuf) -> ExitCode { } }; - let proof: MultiProof = - match bincode::deserialize(&proof_bytes) { - Ok(p) => p, - Err(e) => { - eprintln!("Failed to deserialize proof: {}", e); - return ExitCode::FAILURE; - } - }; + let proof: VmProof = match bincode::deserialize(&proof_bytes) { + Ok(p) => p, + Err(e) => { + eprintln!("Failed to deserialize proof: {}", e); + return ExitCode::FAILURE; + } + }; eprintln!("Verifying proof..."); let result = match prover::verify(&proof, &elf_data) { diff --git a/executor/programs/asm/deep_stack.s b/executor/programs/asm/deep_stack.s new file mode 100644 index 000000000..5c363c346 --- /dev/null +++ b/executor/programs/asm/deep_stack.s @@ -0,0 +1,17 @@ + .attribute 5, "rv64i2p1" + .globl main +main: + # Deep stack usage: allocates 8192 bytes of stack space. + # SP starts at 0xFFFF_FFFF_FFFF_FFF0. + # After allocation: SP = 0x...DFF0 (falls in page 0x...D000). + # With default stack_size=4096, only pages E000 and F000 are + # initialized, so stores into page D000 leave the memory bus + # unbalanced. Increasing stack_size to 8192 adds page D000. + lui t1, 2 # t1 = 8192 + sub sp, sp, t1 # sp -= 8192 → 0x...DFF0 + addi t0, zero, 0x42 + sb t0, 0(sp) # Store byte in page D000 + lb a0, 0(sp) # Load it back + add sp, sp, t1 # Restore stack + li a7, 5 + ecall diff --git a/executor/src/vm/registers.rs b/executor/src/vm/registers.rs index 18191c97c..61945b732 100644 --- a/executor/src/vm/registers.rs +++ b/executor/src/vm/registers.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -const STACK_MEMORY_SIZE: u64 = 0xFFFFFFFFFFFFFFF0; // 64-bit max (Multiple of 16 for RV64 ABI) +pub const STACK_TOP: u64 = 0xFFFFFFFFFFFFFFF0; // 64-bit max (Multiple of 16 for RV64 ABI) #[derive(Debug)] /// Holds the current value of all 32 registers @@ -11,7 +11,7 @@ impl Default for Registers { fn default() -> Self { let mut registers = Registers(Default::default()); // Initialize stack pointer according to available memory size - registers.0[1] = STACK_MEMORY_SIZE; + registers.0[1] = STACK_TOP; registers } } diff --git a/prover/Cargo.toml b/prover/Cargo.toml index 560b35183..4dc161001 100644 --- a/prover/Cargo.toml +++ b/prover/Cargo.toml @@ -14,6 +14,7 @@ stark = { path = "../crypto/stark" } crypto = { path = "../crypto/crypto" } math = { path = "../crypto/math" } executor = { path = "../executor" } +serde = { version = "1.0", features = ["derive"] } dhat = { version = "0.3", optional = true } rayon = { version = "1.8.0", optional = true } diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 4011cd8ae..3aa35dffc 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -6,8 +6,8 @@ //! # Example //! ```ignore //! let elf_bytes = std::fs::read("program.elf").unwrap(); -//! let proof = lambda_vm_prover::prove(&elf_bytes).unwrap(); -//! assert!(lambda_vm_prover::verify(&proof, &elf_bytes).unwrap()); +//! let vm_proof = lambda_vm_prover::prove(&elf_bytes).unwrap(); +//! assert!(lambda_vm_prover::verify(&vm_proof, &elf_bytes).unwrap()); //! ``` #[cfg(feature = "dhat-heap")] @@ -33,6 +33,8 @@ use stark::verifier::{IsStarkVerifier, Verifier}; use crate::tables::bitwise; use crate::tables::decode; +use crate::tables::page; +use crate::tables::register; use crate::tables::trace_builder::Traces; use crate::test_utils::{ E, F, VmAir, create_bitwise_air, create_branch_air, create_cpu_air, create_decode_air, @@ -40,9 +42,21 @@ use crate::test_utils::{ create_mul_air, create_page_air, create_register_air, }; +use crate::tables::page::DEFAULT_STACK_SIZE; use stark::proof::options::ProofOptions; use stark::proof::stark::MultiProof; +/// A complete VM proof bundle containing the STARK proof and metadata +/// needed by the verifier to reconstruct the AIR configuration. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct VmProof { + /// The multi-table STARK proof. + pub proof: MultiProof, + /// Stack size used during proving (bytes). The verifier uses this to + /// reconstruct the PAGE table layout. + pub stack_size: u64, +} + /// Error type for the prover crate. #[derive(Debug)] pub enum Error { @@ -172,10 +186,18 @@ impl VmAirs { let dvrm = create_dvrm_air(proof_options); let branch = create_branch_air(proof_options); let halt = create_halt_air(proof_options); - let register = create_register_air(proof_options); + let register = create_register_air(proof_options).with_preprocessed( + register::preprocessed_commitment(proof_options), + register::NUM_PREPROCESSED_COLS, + ); let pages: Vec<_> = page_configs .iter() - .map(|config| create_page_air(proof_options, config.page_base)) + .map(|config| { + create_page_air(proof_options, config.page_base).with_preprocessed( + page::precomputed_commitment_cached(config, proof_options), + page::NUM_PREPROCESSED_COLS, + ) + }) .collect(); #[cfg(feature = "debug-checks")] @@ -198,16 +220,16 @@ impl VmAirs { } } -/// Prove an ELF binary execution. Returns a serializable proof. -pub fn prove(elf_bytes: &[u8]) -> Result, Error> { +/// Prove an ELF binary execution. Returns a serializable proof bundle. +pub fn prove(elf_bytes: &[u8]) -> Result { prove_with_options(elf_bytes, &ProofOptions::default_proving_options()) } -/// Prove an ELF binary execution with custom proof options. +/// Prove an ELF binary execution with custom proof options and stack size. pub fn prove_with_options( elf_bytes: &[u8], proof_options: &ProofOptions, -) -> Result, Error> { +) -> Result { let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; let executor = Executor::new(&program, vec![]).map_err(|e| Error::Execution(format!("{e}")))?; let result = executor @@ -216,49 +238,58 @@ pub fn prove_with_options( // Generate all traces from ELF and execution logs // This uses the combined ELF processing to generate DECODE and PAGE tables - let mut traces = Traces::from_elf_and_logs(&program, &result.logs)?; + let mut traces = Traces::from_elf_and_logs(&program, &result.logs, DEFAULT_STACK_SIZE)?; let airs = VmAirs::new(&program, proof_options, false, &traces.page_configs); - Prover::multi_prove( + let proof = Prover::multi_prove( airs.air_trace_pairs(&mut traces), &mut DefaultTranscript::::new(&[]), ) - .map_err(|e| Error::Prover(format!("{e:?}"))) -} + .map_err(|e| Error::Prover(format!("{e:?}")))?; -/// Verify a proof produced by [`prove`]. -pub fn verify(proof: &MultiProof, elf_bytes: &[u8]) -> Result { - verify_with_options(proof, elf_bytes, &ProofOptions::default_proving_options()) + Ok(VmProof { + proof, + stack_size: DEFAULT_STACK_SIZE, + }) } -/// Verify a proof with custom proof options. +/// Verify a proof produced by [`prove`] using default proof options. /// -/// Derives page layout from ELF to reconstruct PAGE AIRs for verification. -/// This works for programs that only use ELF segments and stack (no dynamic heap). +/// Uses [`ProofOptions::default_proving_options`] for verification — the +/// `proof_options` stored in [`VmProof`] are metadata only and NOT trusted. +/// `stack_size` is extracted from the proof; it is safe to trust because +/// preprocessed commitments bind the verifier to the correct page layout. +pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result { + verify_with_options( + vm_proof, + elf_bytes, + &ProofOptions::default_proving_options(), + ) +} + +/// Verify a proof with caller-specified proof options. /// -/// # Note -/// For full production use with dynamic heap allocation, page_configs should be -/// embedded in proof public inputs. This implementation assumes deterministic -/// page layout derivable from ELF. +/// 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. pub fn verify_with_options( - proof: &MultiProof, + vm_proof: &VmProof, elf_bytes: &[u8], proof_options: &ProofOptions, ) -> Result { let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; - // Derive page layout from ELF (works for programs without dynamic heap) - let page_configs = Traces::page_configs_from_elf(&program); + let page_configs = Traces::page_configs_from_elf(&program, vm_proof.stack_size); let airs = VmAirs::new(&program, proof_options, false, &page_configs); Ok(Verifier::multi_verify( &airs.air_refs(), - proof, + &vm_proof.proof, &mut DefaultTranscript::::new(&[]), )) } /// Prove and verify in one call (convenience). pub fn prove_and_verify(elf_bytes: &[u8]) -> Result { - let proof = prove(elf_bytes)?; - verify(&proof, elf_bytes) + let vm_proof = prove(elf_bytes)?; + verify(&vm_proof, elf_bytes) } diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index 3f08fefc0..b4d67458d 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -32,9 +32,15 @@ //! | PAGE-C4 | Memory | `[0, address, timestamp, fini]` | 1 (sender) | use std::collections::HashMap; +use std::sync::OnceLock; +use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; +use math::polynomial::Polynomial; +use stark::config::{BatchedMerkleTree, Commitment}; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::trace::TraceTable; +use stark::proof::options::ProofOptions; +use stark::prover::evaluate_polynomial_on_lde_domain; +use stark::trace::{TraceTable, columns2rows}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; @@ -45,8 +51,11 @@ use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; /// Default page size in bytes (4KB). pub const DEFAULT_PAGE_SIZE: usize = 4096; -/// Stack top address (where SP starts). Must match executor. -pub const STACK_TOP: u64 = 0xFFFF_FFFF_FFFF_FFF0; +/// Stack top address (where SP starts). Re-exported from executor. +pub use executor::vm::registers::STACK_TOP; + +/// Default stack size in bytes. +pub const DEFAULT_STACK_SIZE: u64 = 4096; // ========================================================================= // Column indices for PAGE table @@ -195,6 +204,91 @@ pub fn generate_page_trace( TraceTable::new_main(data, cols::NUM_COLUMNS, 1) } +// ========================================================================= +// Preprocessed commitment +// ========================================================================= + +/// Cached commitment for zero-initialized 4KB pages. +/// All zero-init pages of the same size have identical OFFSET and INIT columns. +/// +/// INVARIANT: All callers within a process must use identical `ProofOptions`. +/// The cache is keyed only by page content, not by options. +static ZERO_PAGE_4K_COMMITMENT: OnceLock = OnceLock::new(); + +/// Computes the Merkle root commitment over the LDE of PAGE precomputed columns. +/// +/// The commitment covers OFFSET (0..page_size-1) and INIT (from config). +/// Each page may have different INIT data, producing a different commitment. +pub fn compute_precomputed_commitment(config: &PageConfig, options: &ProofOptions) -> Commitment { + let page_size = config.page_size; + assert!(page_size.is_power_of_two(), "Page size must be power of 2"); + + let num_rows = page_size; + + // Precomputed columns: OFFSET and INIT. + // + // OFFSET (col 0): deterministic row index 0..page_size-1, the same for every + // page of a given size regardless of the program being proven. + // + // INIT (col 1): the initial byte value at each offset. For zero-init pages + // (stack, heap, BSS) this is all zeros. For ELF data pages it holds the + // bytes loaded from the binary. Either way the column is fully determined + // before execution, so the verifier can check it against a preprocessed + // commitment instead of including it in the main trace. + let mut offset_col = vec![FE::zero(); num_rows]; + let mut init_col = vec![FE::zero(); num_rows]; + + for i in 0..page_size { + offset_col[i] = FE::from(i as u64); + init_col[i] = if let Some(ref init_vals) = config.init_values { + FE::from(init_vals[i] as u64) + } else { + FE::zero() + }; + } + + let columns = [offset_col, init_col]; + + let polys: Vec> = columns + .iter() + .map(|col| { + Polynomial::interpolate_fft::(col) + .expect("FFT interpolation failed for page column") + }) + .collect(); + + let blowup_factor = options.blowup_factor as usize; + let coset_offset = FE::from(options.coset_offset); + let mut lde_columns: Vec> = polys + .iter() + .map(|poly| { + evaluate_polynomial_on_lde_domain(poly, blowup_factor, num_rows, &coset_offset) + .expect("LDE evaluation failed for page polynomial") + }) + .collect(); + + for col in lde_columns.iter_mut() { + in_place_bit_reverse_permute(col); + } + + let lde_rows = columns2rows(lde_columns); + let tree = BatchedMerkleTree::::build(&lde_rows) + .expect("Failed to build Merkle tree for page LDE"); + tree.root +} + +/// Returns the preprocessed commitment for a PAGE table, with caching for zero-init pages. +/// +/// Zero-init pages of DEFAULT_PAGE_SIZE share a cached commitment. +/// ELF data pages compute their commitment fresh. +pub fn precomputed_commitment_cached(config: &PageConfig, options: &ProofOptions) -> Commitment { + if config.init_values.is_none() && config.page_size == DEFAULT_PAGE_SIZE { + *ZERO_PAGE_4K_COMMITMENT.get_or_init(|| compute_precomputed_commitment(config, options)) + } else { + compute_precomputed_commitment(config, options) + } +} + // ========================================================================= // Bus interactions // ========================================================================= diff --git a/prover/src/tables/register.rs b/prover/src/tables/register.rs index 41a8abf1c..11f32fe70 100644 --- a/prover/src/tables/register.rs +++ b/prover/src/tables/register.rs @@ -19,9 +19,15 @@ //! | timestamp | DWordWL | Final timestamp (0 if never accessed) | use std::collections::HashMap; +use std::sync::OnceLock; +use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; +use math::polynomial::Polynomial; +use stark::config::{BatchedMerkleTree, Commitment}; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; -use stark::trace::TraceTable; +use stark::proof::options::ProofOptions; +use stark::prover::evaluate_polynomial_on_lde_domain; +use stark::trace::{TraceTable, columns2rows}; use super::page::STACK_TOP; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; @@ -41,6 +47,10 @@ pub const WORDS_PER_REGISTER: usize = 2; /// Each register uses 2 Word addresses in the Memory bus. pub const NUM_REGISTER_ADDRESSES: usize = NUM_REGISTERS * WORDS_PER_REGISTER; +/// Number of preprocessed columns (OFFSET, INIT). +/// OFFSET is 0..63 and INIT is 0 except SP (x2) words which hold STACK_TOP. +pub const NUM_PREPROCESSED_COLS: usize = 2; + // ========================================================================= // Column indices for REGISTER table // ========================================================================= @@ -142,6 +152,83 @@ pub fn generate_register_trace( TraceTable::new_main(data, cols::NUM_COLUMNS, 1) } +// ========================================================================= +// Preprocessed commitment +// ========================================================================= + +/// Cached commitment for the REGISTER preprocessed columns. +/// +/// INVARIANT: All callers within a process must use identical `ProofOptions`. +/// The cache is keyed only by table content, not by options. +static REGISTER_COMMITMENT: OnceLock = OnceLock::new(); + +/// Computes the Merkle root commitment over the LDE of REGISTER precomputed columns. +/// +/// The REGISTER table is program-independent: OFFSET=0..63, INIT=0 except +/// SP (x2) words at offset 4 and 5 which hold STACK_TOP. +pub fn compute_precomputed_commitment(options: &ProofOptions) -> Commitment { + let num_rows = NUM_REGISTER_ADDRESSES.next_power_of_two(); + + // Precomputed columns: OFFSET and INIT. + // + // OFFSET (col 0): deterministic row index 0..63 (32 registers × 2 words each). + // Identical for every program. + // + // INIT (col 1): initial word value for each register address. All zeros except + // for the stack pointer (x2), whose two 32-bit words hold STACK_TOP: + // - offset 4 (SP low word): STACK_TOP & 0xFFFF_FFFF + // - offset 5 (SP high word): STACK_TOP >> 32 + // This is program-independent (STACK_TOP is a fixed constant), so the entire + // REGISTER preprocessed commitment can be computed once and cached globally. + let mut offset_col = vec![FE::zero(); num_rows]; + let mut init_col = vec![FE::zero(); num_rows]; + + for i in 0..NUM_REGISTER_ADDRESSES { + offset_col[i] = FE::from(i as u64); + init_col[i] = if i == 4 { + FE::from(STACK_TOP & 0xFFFF_FFFF) + } else if i == 5 { + FE::from(STACK_TOP >> 32) + } else { + FE::zero() + }; + } + + let columns = [offset_col, init_col]; + + let polys: Vec> = columns + .iter() + .map(|col| { + Polynomial::interpolate_fft::(col) + .expect("FFT interpolation failed for register column") + }) + .collect(); + + let blowup_factor = options.blowup_factor as usize; + let coset_offset = FE::from(options.coset_offset); + let mut lde_columns: Vec> = polys + .iter() + .map(|poly| { + evaluate_polynomial_on_lde_domain(poly, blowup_factor, num_rows, &coset_offset) + .expect("LDE evaluation failed for register polynomial") + }) + .collect(); + + for col in lde_columns.iter_mut() { + in_place_bit_reverse_permute(col); + } + + let lde_rows = columns2rows(lde_columns); + let tree = BatchedMerkleTree::::build(&lde_rows) + .expect("Failed to build Merkle tree for register LDE"); + tree.root +} + +/// Returns the preprocessed commitment for the REGISTER table, with caching. +pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { + *REGISTER_COMMITMENT.get_or_init(|| compute_precomputed_commitment(options)) +} + // ========================================================================= // Bus interactions // ========================================================================= diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 093a88d6d..57d934c9d 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -21,7 +21,7 @@ //! ```ignore //! use lambda_vm_prover::tables::trace_builder::Traces; //! -//! let traces = Traces::from_elf_and_logs(&elf, &logs)?; +//! let traces = Traces::from_elf_and_logs(&elf, &logs, 4096)?; //! // Use traces.cpu, traces.bitwise, traces.lt, traces.memw, traces.load, traces.memory_init //! ``` @@ -929,7 +929,11 @@ fn collect_bitwise_from_branch(branch_ops: &[BranchOperation]) -> Vec Vec { +fn collect_bitwise_from_page( + elf: &Elf, + memory_state: &MemoryState, + stack_size: u64, +) -> Vec { use std::collections::BTreeSet; let page_size = page::DEFAULT_PAGE_SIZE; @@ -961,7 +965,6 @@ fn collect_bitwise_from_page(elf: &Elf, memory_state: &MemoryState) -> Vec Vec ( Vec>, Vec, @@ -1058,8 +1062,6 @@ fn generate_page_tables( // Add stack pages covering from STACK_TOP down to stack_bottom // Stack grows downward from STACK_TOP, so we need pages for both ends - // TODO: Make this configurable via MemoryInitConfig - let stack_size = 4096u64; // 1 page for now let stack_bottom = page::STACK_TOP - stack_size; // Add page containing STACK_TOP (where SP starts and first accesses happen) let stack_top_page = page::page_base_for_address(page::STACK_TOP, page_size); @@ -1155,26 +1157,33 @@ impl Traces { /// For full production use, page_configs should be embedded in proof public inputs /// to handle dynamic heap allocation. This function works for programs that only /// use ELF segments and stack. - pub fn page_configs_from_elf(elf: &Elf) -> Vec { + pub fn page_configs_from_elf(elf: &Elf, stack_size: u64) -> Vec { use std::collections::BTreeSet; let page_size = page::DEFAULT_PAGE_SIZE; let mut page_bases: BTreeSet = BTreeSet::new(); + let mut elf_page_data: HashMap> = HashMap::new(); - // Collect pages from ELF data segments + // Collect pages from ELF data segments with init data for segment in &elf.data { - for (i, _) in segment.values.iter().enumerate() { + for (i, &word) in segment.values.iter().enumerate() { let word_addr = segment.base_addr + (i as u64 * 4); for byte_offset in 0..4u64 { let byte_addr = word_addr + byte_offset; + let byte_value = ((word >> (byte_offset * 8)) & 0xFF) as u8; let page_base = page::page_base_for_address(byte_addr, page_size); + let offset = page::offset_in_page(byte_addr, page_size); page_bases.insert(page_base); + + let page_data = elf_page_data + .entry(page_base) + .or_insert_with(|| vec![0u8; page_size]); + page_data[offset] = byte_value; } } } // Add stack pages (same logic as generate_page_tables) - let stack_size = 4096u64; let stack_bottom = page::STACK_TOP - stack_size; let stack_top_page = page::page_base_for_address(page::STACK_TOP, page_size); page_bases.insert(stack_top_page); @@ -1185,7 +1194,13 @@ impl Traces { page_bases .into_iter() - .map(|base| PageConfig::zero_init(base, page_size)) + .map(|base| { + if let Some(init_data) = elf_page_data.get(&base) { + PageConfig::with_data(base, page_size, init_data.clone()) + } else { + PageConfig::zero_init(base, page_size) + } + }) .collect() } @@ -1198,7 +1213,7 @@ impl Traces { /// 3. MEMW → LT operations (timestamp ordering) /// 4. LT, MEMW, Branch → Bitwise lookups /// 5. Generate all traces including PAGE tables - pub fn from_elf_and_logs(elf: &Elf, logs: &[Log]) -> Result { + pub fn from_elf_and_logs(elf: &Elf, logs: &[Log], stack_size: u64) -> Result { // ===================================================================== // PHASE 0: ELF → DECODE + instructions // ===================================================================== @@ -1301,7 +1316,7 @@ impl Traces { bitwise_ops.extend(collect_bitwise_from_dvrm(&dvrm_ops)); bitwise_ops.extend(collect_bitwise_from_branch(&branch_ops)); // PAGE tables do IS_BYTE lookups for init and fini values (C1, C2) - bitwise_ops.extend(collect_bitwise_from_page(elf, &memory_state)); + bitwise_ops.extend(collect_bitwise_from_page(elf, &memory_state, stack_size)); // ===================================================================== // PHASE 5: Generate final traces @@ -1337,7 +1352,7 @@ impl Traces { decode::update_multiplicities(&mut decode, &pc_to_row, &decode_lookups); // Generate PAGE tables from ELF and final memory state - let (pages, page_configs) = generate_page_tables(elf, &memory_state); + let (pages, page_configs) = generate_page_tables(elf, &memory_state, stack_size); // Generate REGISTER table from final register state let register_final_state = register_state.to_final_state_map(); diff --git a/prover/src/tests/decode_tests.rs b/prover/src/tests/decode_tests.rs index 46a252ce7..58b67daed 100644 --- a/prover/src/tests/decode_tests.rs +++ b/prover/src/tests/decode_tests.rs @@ -1029,7 +1029,12 @@ fn test_decode_soundness_same_elf_accepted() { .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&prover_elf, &result.logs).unwrap(); + let mut traces = Traces::from_elf_and_logs( + &prover_elf, + &result.logs, + crate::tables::page::DEFAULT_STACK_SIZE, + ) + .unwrap(); let prover_airs = VmAirs::new(&prover_elf, &proof_options, false, &traces.page_configs); let proof = Prover::multi_prove( diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index dbb05b4f2..7afcb26df 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -134,7 +134,8 @@ fn test_prove_elfs_sub_fast() { let _ = env_logger::builder().is_test(true).try_init(); let (elf, logs, _instructions) = run_asm_elf("sub"); // Use from_elf_and_logs to get PAGE and REGISTER tables for Memory bus - let mut traces = Traces::from_elf_and_logs(&elf, &logs).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &logs, crate::tables::page::DEFAULT_STACK_SIZE).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), @@ -449,7 +450,8 @@ fn test_prove_elfs_test_xor_8() { #[test] fn test_prove_elfs_test_lb_lh_8() { let (elf, logs, _instructions) = run_asm_elf("test_lb_lh_8"); - let mut traces = Traces::from_elf_and_logs(&elf, &logs).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &logs, crate::tables::page::DEFAULT_STACK_SIZE).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "test_lb_lh_8 failed" @@ -459,7 +461,8 @@ fn test_prove_elfs_test_lb_lh_8() { #[test] fn test_prove_elfs_test_sb_sh_8() { let (elf, logs, _instructions) = run_asm_elf("test_sb_sh_8"); - let mut traces = Traces::from_elf_and_logs(&elf, &logs).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &logs, crate::tables::page::DEFAULT_STACK_SIZE).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "test_sb_sh_8 failed" @@ -489,7 +492,8 @@ fn test_prove_elfs_all_branches_16() { #[test] fn test_prove_elfs_all_loadstore_32() { let (elf, logs, _instructions) = run_asm_elf("all_loadstore_32"); - let mut traces = Traces::from_elf_and_logs(&elf, &logs).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &logs, crate::tables::page::DEFAULT_STACK_SIZE).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "all_loadstore_32 failed" @@ -910,7 +914,8 @@ fn test_debug_memory_tokens_sb_sh() { use std::collections::HashMap; let (elf, logs, _instructions) = run_asm_elf("test_sb_sh_8"); - let traces = Traces::from_elf_and_logs(&elf, &logs).unwrap(); + let traces = + Traces::from_elf_and_logs(&elf, &logs, crate::tables::page::DEFAULT_STACK_SIZE).unwrap(); println!("DEBUG: test_sb_sh_8 Memory bus tokens (FULL)"); println!(" MEMW rows: {}", traces.memw.num_rows()); @@ -1286,7 +1291,8 @@ fn test_page_trace_values_debug() { use crate::tables::page::cols as page_cols; let (elf, logs, _instructions) = run_asm_elf("test_sb_sh_8"); - let traces = Traces::from_elf_and_logs(&elf, &logs).unwrap(); + let traces = + Traces::from_elf_and_logs(&elf, &logs, crate::tables::page::DEFAULT_STACK_SIZE).unwrap(); println!("=== Checking PAGE trace values for stack addresses ==="); @@ -1353,7 +1359,8 @@ fn test_page_trace_values_debug() { #[ignore] // Intentionally removes 3 of 4 PAGE tables, so Memory bus won't balance fn test_single_page_table_balance() { let (elf, logs, _instructions) = run_asm_elf("test_sb_sh_8"); - let mut traces = Traces::from_elf_and_logs(&elf, &logs).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &logs, crate::tables::page::DEFAULT_STACK_SIZE).unwrap(); println!("Original PAGE tables: {}", traces.pages.len()); println!("PAGE configs:"); @@ -1388,3 +1395,35 @@ fn test_single_page_table_balance() { ); assert!(result, "Single PAGE table test failed"); } + +// ============================================================================= +// Deep stack tests (page coverage) +// ============================================================================= + +/// deep_stack allocates 8192 bytes, writing at SP = 0x...DFF0 (page D000). +/// Default stack_size=4096 only creates pages E000+F000, so page D000 is +/// missing and the memory bus cannot balance → verification must fail. +#[test] +fn test_deep_stack_default_stack_size_fails() { + let (elf, logs, _instructions) = run_asm_elf("deep_stack"); + let mut traces = + Traces::from_elf_and_logs(&elf, &logs, crate::tables::page::DEFAULT_STACK_SIZE).unwrap(); + + assert!( + !prove_and_verify_vm_minimal(&elf, &mut traces), + "deep_stack should FAIL with default stack_size (page D000 not initialized)" + ); +} + +/// Same program but with stack_size=8192, which adds page D000. +/// All accessed addresses are now covered → verification must succeed. +#[test] +fn test_deep_stack_large_stack_size_passes() { + let (elf, logs, _instructions) = run_asm_elf("deep_stack"); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, 8192).unwrap(); + + assert!( + prove_and_verify_vm_minimal(&elf, &mut traces), + "deep_stack should PASS with stack_size=8192 (page D000 initialized)" + ); +}