From 94965b491979011789e78ecd5ecc085f9cb40176 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 15 Apr 2026 11:59:07 -0300 Subject: [PATCH 1/8] add prove_with_inputs --- bin/cli/src/main.rs | 2 +- .../programs/asm/test_private_input_xpage.s | 38 +++++++++++++++++++ prover/src/lib.rs | 18 ++++++++- prover/src/tests/prove_elfs_tests.rs | 26 ++++++++++--- 4 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 executor/programs/asm/test_private_input_xpage.s diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 725f0de5f..6723faf49 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -276,7 +276,7 @@ fn cmd_prove(elf_path: PathBuf, output_path: PathBuf, blowup: Option, time: "Generating proof (blowup={b}, queries={})...", opts.fri_number_of_queries ); - prover::prove_with_options(&elf_data, &opts, &Default::default()) + prover::prove_with_options(&elf_data, vec![], &opts, &Default::default()) } None => { eprintln!("Generating proof..."); diff --git a/executor/programs/asm/test_private_input_xpage.s b/executor/programs/asm/test_private_input_xpage.s new file mode 100644 index 000000000..4869e9867 --- /dev/null +++ b/executor/programs/asm/test_private_input_xpage.s @@ -0,0 +1,38 @@ + .attribute 5, "rv64i2p1" + .globl main +main: + # Loop: read N bytes of private input, write each back to memory, commit 8 bytes. + # This mimics what a real program would do: read private input, do memory ops on it. + + li t0, -4096 # 1: sp offset (fits in imm) + add sp, sp, t0 # 2: allocate 4KB stack + li t0, -4096 + add sp, sp, t0 # 3: another 4KB (total 8KB, spans 2 pages) + + # GetPrivateInputs: copy to sp + mv a0, sp # 4: dest = sp + li a7, 4 # 5: syscall = GetPrivateInputs + ecall # 6: get_private_input + + # Read 8 bytes at sp+8 (aligned), write to sp+2000 + ld t1, 8(sp) # 7 + sd t1, 2000(sp) # 8 + + # Read 8 bytes at sp+2000 (should match what we just wrote) + ld t2, 2000(sp) # 9 + + # Commit 8 bytes from sp+8 + addi a1, sp, 8 # 10 + li a0, 1 # 11 + li a2, 8 # 12 + li a7, 64 # 13 + ecall # 14 + + # Halt + li t0, 4096 # 15 + add sp, sp, t0 # 16 + li t0, 4096 + add sp, sp, t0 # 17 + li a0, 0 # 18 + li a7, 93 # 19 + ecall # 20 diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 7cb947f6f..12f395f79 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -481,6 +481,20 @@ pub(crate) fn compute_expected_commit_bus_balance( pub fn prove(elf_bytes: &[u8]) -> Result { prove_with_options( elf_bytes, + vec![], + &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), + &MaxRowsConfig::default(), + ) +} + +/// Prove an ELF binary execution with a private input. +/// +/// The `private_input` bytes are passed to the executor; the guest program +/// retrieves them via the `GetPrivateInputs` syscall. +pub fn prove_with_input(elf_bytes: &[u8], private_input: Vec) -> Result { + prove_with_options( + elf_bytes, + private_input, &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), &MaxRowsConfig::default(), ) @@ -489,6 +503,7 @@ pub fn prove(elf_bytes: &[u8]) -> Result { /// Prove an ELF binary execution with custom proof options and max rows config. pub fn prove_with_options( elf_bytes: &[u8], + private_input: Vec, proof_options: &ProofOptions, max_rows: &MaxRowsConfig, ) -> Result { @@ -500,7 +515,8 @@ pub fn prove_with_options( let phase_start = std::time::Instant::now(); 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 executor = + Executor::new(&program, private_input).map_err(|e| Error::Execution(format!("{e}")))?; let result = executor .run() .map_err(|e| Error::Execution(format!("{e}")))?; diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 9a1b8a6d0..549c0e399 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -801,7 +801,7 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { fn test_verify_rejects_tampered_public_output() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_commit_4"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &Default::default()) + let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) .expect("Prover should succeed for test_commit_4"); assert!( crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) @@ -1696,7 +1696,7 @@ fn test_verify_rejects_zero_table_counts() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &Default::default()) + let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) .expect("Prover should succeed on valid program"); assert!( @@ -1731,7 +1731,7 @@ fn test_verify_rejects_zero_cpu_count() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &Default::default()) + let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) .expect("Prover should succeed on valid program"); let tampered_proof = crate::VmProof { @@ -1752,7 +1752,7 @@ fn test_verify_rejects_zero_memw_count() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &Default::default()) + let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) .expect("Prover should succeed on valid program"); let tampered_proof = crate::VmProof { @@ -1828,7 +1828,7 @@ fn test_small_max_rows_splits_tables() { let proof_options = ProofOptions::default_test_options(); let max_rows = crate::tables::MaxRowsConfig::small(); - let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &max_rows) + let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &max_rows) .expect("Prover should succeed with small max_rows"); // With 2^5 max rows and 64+ instructions, tables should have multiple chunks. @@ -1879,7 +1879,7 @@ fn test_verify_rejects_inflated_table_counts() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &Default::default()) + let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) .expect("Prover should succeed on valid program"); // Inflate cpu count — total won't match proof.proofs.len() @@ -1899,6 +1899,20 @@ fn test_verify_rejects_inflated_table_counts() { ); } +/// Test that `prove_with_input` with empty input produces the same result as `prove`. +/// Backward-compatibility check: the new API must not change behavior for programs +/// that don't read private input. +#[test] +fn test_prove_with_input_empty() { + let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); + let result = crate::prove_with_input(&elf_bytes, vec![]) + .expect("prove_with_input should succeed on sub"); + assert!( + crate::verify(&result, &elf_bytes).expect("verify should not error"), + "prove_with_input(empty) proof should verify" + ); +} + /// Regression test: addiw with negative immediate must verify. /// arg2_sign_bit is the sign bit of rv2 (bit 31), not of arg2, per spec /// constraint CPU-CE61: MSB16[arg2_sign_bit; rv2[1]]. From 7bd6431d3e1f19c08310af7df64b71fd346c928f Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 15 Apr 2026 12:45:50 -0300 Subject: [PATCH 2/8] add private input logic --- .../programs/asm/test_private_input_xpage.s | 52 +++--- executor/src/vm/instruction/execution.rs | 10 -- executor/src/vm/memory.rs | 20 ++- executor/tests/asm.rs | 13 ++ prover/src/lib.rs | 18 ++- prover/src/tables/trace_builder.rs | 149 ++++++++++-------- prover/src/tests/decode_tests.rs | 2 +- prover/src/tests/prove_elfs_tests.rs | 74 ++++++--- syscalls/src/syscalls.rs | 43 ++--- 9 files changed, 223 insertions(+), 158 deletions(-) diff --git a/executor/programs/asm/test_private_input_xpage.s b/executor/programs/asm/test_private_input_xpage.s index 4869e9867..abe58452c 100644 --- a/executor/programs/asm/test_private_input_xpage.s +++ b/executor/programs/asm/test_private_input_xpage.s @@ -1,38 +1,32 @@ .attribute 5, "rv64i2p1" .globl main main: - # Loop: read N bytes of private input, write each back to memory, commit 8 bytes. - # This mimics what a real program would do: read private input, do memory ops on it. + # Read private input directly from 0xFF000000 (memory-mapped). + # Layout: [len:u32 LE] [data...] + # Commits 8 bytes of data. + # + # Note: lui in RV64 sign-extends to 64 bits. lui with 0xFF000 would give + # 0xFFFFFFFFFF000000. To get 0xFF000000 we need to construct it differently: + # lui x, 0x100000 gives 0x100000000 (53 upper bits), too high. + # Instead: load 0x0FF00000 and shift left by 4 bits, OR similar tricks. + # Simplest: use li macro and let the assembler handle it. - li t0, -4096 # 1: sp offset (fits in imm) - add sp, sp, t0 # 2: allocate 4KB stack - li t0, -4096 - add sp, sp, t0 # 3: another 4KB (total 8KB, spans 2 pages) + li t0, 0xFF000000 # 1: t0 = 0xFF000000 (private input base) - # GetPrivateInputs: copy to sp - mv a0, sp # 4: dest = sp - li a7, 4 # 5: syscall = GetPrivateInputs - ecall # 6: get_private_input + # Read length at 0xFF000000 + lw t3, 0(t0) # 2: t3 = length - # Read 8 bytes at sp+8 (aligned), write to sp+2000 - ld t1, 8(sp) # 7 - sd t1, 2000(sp) # 8 + # Load 8 bytes of data at 0xFF000008 (aligned, 4 bytes into data region) + ld t1, 8(t0) # 3 - # Read 8 bytes at sp+2000 (should match what we just wrote) - ld t2, 2000(sp) # 9 - - # Commit 8 bytes from sp+8 - addi a1, sp, 8 # 10 - li a0, 1 # 11 - li a2, 8 # 12 - li a7, 64 # 13 - ecall # 14 + # Commit 8 bytes from 0xFF000008 + addi a1, t0, 8 # 4: buf_addr = 0xFF000008 + li a0, 1 # 5: fd = 1 + li a2, 8 # 6: count = 8 + li a7, 64 # 7: syscall = Commit + ecall # 8: commit # Halt - li t0, 4096 # 15 - add sp, sp, t0 # 16 - li t0, 4096 - add sp, sp, t0 # 17 - li a0, 0 # 18 - li a7, 93 # 19 - ecall # 20 + li a0, 0 # 9: exit_code = 0 + li a7, 93 # 10: syscall = Halt + ecall # 11: halt diff --git a/executor/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index dcab3f927..e89a11fb7 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -10,7 +10,6 @@ const REGULAR_PC_UPDATE: u64 = 4; pub enum SyscallNumbers { Print = 1, Panic = 2, - GetPrivateInputs = 4, Commit = 64, Halt = 93, } @@ -21,7 +20,6 @@ impl TryFrom for SyscallNumbers { match value { 1 => Ok(SyscallNumbers::Print), 2 => Ok(SyscallNumbers::Panic), - 4 => Ok(SyscallNumbers::GetPrivateInputs), 64 => Ok(SyscallNumbers::Commit), 93 => Ok(SyscallNumbers::Halt), _ => Err(()), @@ -318,14 +316,6 @@ impl Instruction { src2_val = buf_addr; dst_val = count; } - SyscallNumbers::GetPrivateInputs => { - // get private inputs - let pointer = registers.read(10)?; - let private_inputs = memory.load_private_inputs()?; - for (i, byte) in private_inputs.iter().enumerate() { - memory.store_byte(pointer + i as u64, *byte); - } - } SyscallNumbers::Halt => { // halt return Ok(Log { diff --git a/executor/src/vm/memory.rs b/executor/src/vm/memory.rs index 7e4f1ac04..f8abc32b6 100644 --- a/executor/src/vm/memory.rs +++ b/executor/src/vm/memory.rs @@ -41,10 +41,12 @@ pub type U64HashMap = HashMap; // TODO: Correctly define this const MAX_PUBLIC_OUTPUT_COMMIT_SIZE: u64 = 1024; const PUBLIC_OUTPUT_START_INDEX: u64 = 0; -// Ported from main: increased from 1024 to support larger inputs (ethrex) -const MAX_PRIVATE_INPUT_SIZE: u64 = 6700000; -// Ported from main: fixed high address to avoid overlap with program memory -const PRIVATE_INPUT_START_INDEX: u64 = 0xFF000000; +/// Maximum size of the private input memory region (in bytes). +pub const MAX_PRIVATE_INPUT_SIZE: u64 = 6700000; +/// Fixed high address where private input is mapped. Guest programs can read +/// directly from this address (ZisK-style memory-mapped input). +/// Layout: 4-byte LE length prefix at `PRIVATE_INPUT_START_INDEX`, then data at +4. +pub const PRIVATE_INPUT_START_INDEX: u64 = 0xFF000000; #[derive(Default, Debug)] pub struct Memory(U64HashMap<[u8; 4]>); @@ -151,6 +153,9 @@ impl Memory { Ok(self.load_bytes(PUBLIC_OUTPUT_START_INDEX + 4, size as u64)) } + /// Pre-loads private input bytes at `PRIVATE_INPUT_START_INDEX` as a + /// 4-byte LE length prefix followed by the raw data. The guest reads these + /// bytes directly via normal RISC-V loads (ZisK-style memory-mapped input). pub fn store_private_inputs(&mut self, inputs: Vec) -> Result<(), MemoryError> { if inputs.len() as u64 > MAX_PRIVATE_INPUT_SIZE { return Err(MemoryError::PrivateInputSizeExceeded); @@ -160,13 +165,6 @@ impl Memory { Ok(()) } - pub fn load_private_inputs(&self) -> Result, MemoryError> { - let size = self.load_word(PRIVATE_INPUT_START_INDEX)?; - let mut inputs = size.to_le_bytes().to_vec(); - inputs.extend_from_slice(&self.load_bytes(PRIVATE_INPUT_START_INDEX + 4, size as u64)); - Ok(inputs) - } - pub fn load_bytes(&self, mut addr: u64, len: u64) -> Vec { let mut result = Vec::with_capacity(len as usize); let end = addr + len; diff --git a/executor/tests/asm.rs b/executor/tests/asm.rs index fc9d3657f..cbc1adec5 100644 --- a/executor/tests/asm.rs +++ b/executor/tests/asm.rs @@ -21,6 +21,19 @@ fn run_program(elf_path: &str) { ); } +/// Test that the memory-mapped private input region is readable by guest programs. +/// The ASM program reads from 0xFF000000 and commits 8 bytes of data. +#[test] +fn test_private_input_memory_mapped() { + let elf_data = std::fs::read("./program_artifacts/asm/test_private_input_xpage.elf").unwrap(); + let program = Elf::load(&elf_data).unwrap(); + let input: Vec = (0u8..16).collect(); + let executor = Executor::new(&program, input.clone()).unwrap(); + let result = executor.run().unwrap(); + // Committed bytes are at 0xFF000008 = data bytes [4..12] + assert_eq!(result.return_values.memory_values, input[4..12].to_vec()); +} + #[test] fn test_basic_program() { run_program("./program_artifacts/asm/basic_program.elf"); diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 12f395f79..3b21f9126 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -138,6 +138,10 @@ pub struct VmProof { pub table_counts: TableCounts, /// Committed public output bytes. pub public_output: Vec, + /// Private input bytes. The verifier needs these to reconstruct the + /// PAGE preprocessed commitments for the private-input memory region + /// (pages at `PRIVATE_INPUT_START_INDEX = 0xFF000000`). + pub private_input: Vec, } /// Error type for the prover crate. @@ -515,6 +519,10 @@ pub fn prove_with_options( let phase_start = std::time::Instant::now(); let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; + // Clone private_input so we can (a) hand ownership to the executor, + // (b) pass a reference to the trace builder, and (c) include it in VmProof + // for the verifier to reconstruct PAGE init data. + let saved_private_input = private_input.clone(); let executor = Executor::new(&program, private_input).map_err(|e| Error::Execution(format!("{e}")))?; let result = executor @@ -530,7 +538,8 @@ pub fn prove_with_options( // Generate all traces from ELF and execution logs. // Page tables are derived from the prover's MemoryState (all accessed pages). - let mut traces = Traces::from_elf_and_logs(&program, &result.logs, max_rows)?; + let mut traces = + Traces::from_elf_and_logs(&program, &result.logs, max_rows, &saved_private_input)?; #[cfg(feature = "instruments")] let trace_build_elapsed = phase_start.elapsed(); @@ -575,6 +584,7 @@ pub fn prove_with_options( runtime_page_ranges, table_counts, public_output: traces.public_output_bytes.clone(), + private_input: saved_private_input, }) } @@ -607,7 +617,11 @@ pub fn verify_with_options( let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; let page_configs = - Traces::page_configs_from_elf_and_runtime(&program, &vm_proof.runtime_page_ranges); + Traces::page_configs_from_elf_and_runtime( + &program, + &vm_proof.runtime_page_ranges, + &vm_proof.private_input, + ); // Cross-check: table_counts must match the number of sub-proofs. // Fixed tables (bitwise, decode, halt, commit, register) = 5, plus page tables. diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 6cbcd0735..9ae0488af 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -97,6 +97,26 @@ impl MemoryState { Self { cells } } + /// Pre-populate the private input memory region at `PRIVATE_INPUT_START_INDEX`. + /// + /// Writes the 4-byte LE length prefix at `0xFF000000` and the input bytes at + /// `0xFF000004+`, all at timestamp=0. This mirrors the executor's + /// `Memory::store_private_inputs` layout so the guest can read input bytes + /// directly via RISC-V loads (ZisK-style memory-mapped input). + fn add_private_input(&mut self, private_input: &[u8]) { + use executor::vm::memory::PRIVATE_INPUT_START_INDEX; + let start = PRIVATE_INPUT_START_INDEX; + // Length prefix (4 bytes LE) + let len_bytes = (private_input.len() as u32).to_le_bytes(); + for (i, &b) in len_bytes.iter().enumerate() { + self.cells.insert(start + i as u64, (b, 0)); + } + // Input bytes + for (i, &b) in private_input.iter().enumerate() { + self.cells.insert(start + 4 + i as u64, (b, 0)); + } + } + /// Read a byte from memory. Returns (value, timestamp) or (0, 0) if never written. fn read_byte(&self, address: u64) -> MemoryCell { self.cells.get(&address).copied().unwrap_or((0, 0)) @@ -1330,26 +1350,27 @@ fn collect_byte_check_ops_for_padding(num_padding_rows: usize) -> Vec Vec { - use std::collections::BTreeSet; - +/// Build a page_base → init_data map from the ELF segments and the private input region. +/// +/// The private input is laid out at `PRIVATE_INPUT_START_INDEX` as a 4-byte LE length +/// prefix followed by the raw bytes (matching `Memory::store_private_inputs` and +/// `MemoryState::add_private_input`). Both the prover (trace generation) and verifier +/// (page config reconstruction) call this so they agree on PAGE init values exactly. +fn build_init_page_data(elf: &Elf, private_input: &[u8]) -> HashMap> { + use executor::vm::memory::PRIVATE_INPUT_START_INDEX; let page_size = page::DEFAULT_PAGE_SIZE; - let mut bitwise_ops = Vec::new(); - - // Collect ELF page init data - let mut elf_page_data: HashMap> = HashMap::new(); + let mut init_page_data: HashMap> = HashMap::new(); + // ELF segments for segment in &elf.data { 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); - - let page_data = elf_page_data + let page_data = init_page_data .entry(page_base) .or_insert_with(|| vec![0u8; page_size]); page_data[offset] = byte_value; @@ -1357,6 +1378,40 @@ fn collect_bitwise_from_page(elf: &Elf, memory_state: &MemoryState) -> Vec = len_bytes + .iter() + .chain(private_input.iter()) + .copied() + .collect(); + for (i, &b) in all_bytes.iter().enumerate() { + let addr = PRIVATE_INPUT_START_INDEX + i as u64; + let page_base = page::page_base_for_address(addr, page_size); + let offset = page::offset_in_page(addr, page_size); + let page_data = init_page_data + .entry(page_base) + .or_insert_with(|| vec![0u8; page_size]); + page_data[offset] = b; + } + } + + init_page_data +} + +fn collect_bitwise_from_page( + elf: &Elf, + memory_state: &MemoryState, + private_input: &[u8], +) -> Vec { + use std::collections::BTreeSet; + + let page_size = page::DEFAULT_PAGE_SIZE; + let mut bitwise_ops = Vec::new(); + + let elf_page_data = build_init_page_data(elf, private_input); + // Derive ALL page bases from memory_state (includes ELF + runtime pages) let mut page_bases: BTreeSet = BTreeSet::new(); for &addr in memory_state.cells.keys() { @@ -1505,6 +1560,7 @@ fn collect_bitwise_from_commit(commit_ops: &[CommitOperation]) -> Vec ( Vec>, Vec, @@ -1513,27 +1569,7 @@ fn generate_page_tables( let page_size = page::DEFAULT_PAGE_SIZE; - // Collect ELF page init data (needed for PageConfig::with_data) - let mut elf_page_data: HashMap> = HashMap::new(); - - for segment in &elf.data { - 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); - - let page_data = elf_page_data - .entry(page_base) - .or_insert_with(|| vec![0u8; page_size]); - page_data[offset] = byte_value; - } - } - } + let elf_page_data = build_init_page_data(elf, private_input); // Derive ALL page bases from memory_state (includes ELF + runtime pages) let mut page_bases: BTreeSet = BTreeSet::new(); @@ -1663,54 +1699,33 @@ impl Traces { /// Returns PageConfigs for pages covered by ELF segments, with their /// init data populated. Used by the verifier to reconstruct the ELF /// portion of the PAGE table layout. - pub fn page_configs_from_elf(elf: &Elf) -> Vec { - use std::collections::BTreeSet; - + pub fn page_configs_from_elf(elf: &Elf, private_input: &[u8]) -> Vec { let page_size = page::DEFAULT_PAGE_SIZE; - let mut page_bases: BTreeSet = BTreeSet::new(); - let mut elf_page_data: HashMap> = HashMap::new(); - - for segment in &elf.data { - 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; - } - } - } + let init_page_data = build_init_page_data(elf, private_input); - page_bases + let mut bases: Vec = init_page_data.keys().copied().collect(); + bases.sort(); + bases .into_iter() .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) - } + let data = init_page_data[&base].clone(); + PageConfig::with_data(base, page_size, data) }) .collect() } - /// Reconstruct page configs from ELF and runtime page ranges. + /// Reconstruct page configs from ELF, private input, and runtime page ranges. /// /// Used by the verifier to reconstruct the full PAGE table layout. - /// Combines deterministic ELF pages with prover-provided runtime + /// Combines deterministic ELF + private-input pages with prover-provided runtime /// page ranges (zero-initialized: stack, heap, etc.). /// Each range is `(base, count)` — `count` contiguous 4KB pages from `base`. pub fn page_configs_from_elf_and_runtime( elf: &Elf, runtime_page_ranges: &[crate::RuntimePageRange], + private_input: &[u8], ) -> Vec { - let mut configs = Self::page_configs_from_elf(elf); + let mut configs = Self::page_configs_from_elf(elf, private_input); let page_size = page::DEFAULT_PAGE_SIZE; for r in runtime_page_ranges { let (base, count) = (r.base, r.count); @@ -1779,6 +1794,7 @@ impl Traces { elf: &Elf, logs: &[Log], max_rows: &super::MaxRowsConfig, + private_input: &[u8], ) -> Result { // ===================================================================== // PHASE 0: ELF → DECODE + instructions @@ -1802,6 +1818,7 @@ impl Traces { // Processes cpu_ops in order. MEMW/LOAD need state tracking, LT/Bitwise don't. // Initialize memory state from ELF so first accesses get correct old_value. let mut memory_state = MemoryState::from_elf(elf); + memory_state.add_private_input(private_input); let mut register_state = RegisterState::new(elf.entry_point); let (mut memw_ops, load_ops, mut lt_ops, shift_ops, mut bitwise_ops, commit_ops) = collect_ops_from_cpu(&cpu_ops, &mut memory_state, &mut register_state); @@ -1898,7 +1915,7 @@ impl Traces { // MEMW_R sends IS_HALFWORD[timestamp_0 - old_timestamp_lo - 1] bitwise_ops.extend(collect_bitwise_from_memw_register(&memw_register_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, private_input)); let public_output_bytes: Vec = commit_ops .iter() @@ -1977,7 +1994,7 @@ impl Traces { let ((pages_val, register_val), halt_val) = rayon::join( || { rayon::join( - || generate_page_tables(elf, &memory_state), + || generate_page_tables(elf, &memory_state, private_input), || { register::generate_register_trace( ®ister_final_state, @@ -1996,7 +2013,7 @@ impl Traces { } #[cfg(not(feature = "parallel"))] { - let (pages_v, page_configs_v) = generate_page_tables(elf, &memory_state); + let (pages_v, page_configs_v) = generate_page_tables(elf, &memory_state, private_input); pages = pages_v; page_configs = page_configs_v; register_trace = diff --git a/prover/src/tests/decode_tests.rs b/prover/src/tests/decode_tests.rs index 361a52d66..852c2ccd6 100644 --- a/prover/src/tests/decode_tests.rs +++ b/prover/src/tests/decode_tests.rs @@ -1032,7 +1032,7 @@ fn test_decode_soundness_same_elf_accepted() { let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs(&prover_elf, &result.logs, &Default::default()).unwrap(); + Traces::from_elf_and_logs(&prover_elf, &result.logs, &Default::default(), &[]).unwrap(); let table_counts = traces.table_counts(); let prover_airs = VmAirs::new( &prover_elf, diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 549c0e399..bb2ed7f75 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -158,7 +158,7 @@ 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, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), @@ -597,7 +597,7 @@ 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, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "test_lb_lh_8 failed" @@ -607,7 +607,7 @@ 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, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); assert!( !traces.memws.is_empty(), "test_sb_sh_8 should produce MEMW rows for byte/halfword memory accesses" @@ -624,7 +624,7 @@ fn test_prove_elfs_test_sb_sh_8() { #[test] fn test_prove_elfs_lw_sw() { let (elf, logs, _instructions) = run_asm_elf("lw_sw"); - let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); assert!( !traces.memw_aligneds.is_empty(), "lw_sw should produce MEMW_A rows for aligned word accesses" @@ -644,7 +644,7 @@ fn test_prove_elfs_lw_sw() { #[test] fn test_prove_elfs_test_memw_split_ts() { let (elf, logs, _instructions) = run_asm_elf("test_memw_split_ts"); - let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); assert!( !traces.memws.is_empty(), "test_memw_split_ts should produce MEMW rows (split old_timestamps from sb+sb+lh)" @@ -683,7 +683,7 @@ 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, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "all_loadstore_32 failed" @@ -731,7 +731,7 @@ fn test_prove_elfs_test_commit_4() { "Public output should match committed bytes" ); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); assert_eq!( traces.public_output_bytes, result.return_values.memory_values @@ -756,7 +756,7 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { let executor = executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); // Prover uses correct page configs let table_counts = traces.table_counts(); @@ -774,7 +774,7 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { .expect("Prover failed"); // Verifier uses EMPTY runtime pages → missing stack/public-output pages - let wrong_configs = Traces::page_configs_from_elf_and_runtime(&elf, &[]); + let wrong_configs = Traces::page_configs_from_elf_and_runtime(&elf, &[], &[]); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &wrong_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1126,7 +1126,7 @@ 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, &Default::default()).unwrap(); + let traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); let memw = &traces.memws[0]; // Small test: single MEMW chunk println!("DEBUG: test_sb_sh_8 Memory bus tokens (FULL)"); @@ -1456,7 +1456,7 @@ fn test_debug_memory_tokens_sb_sh() { #[test] fn test_deep_stack_passes() { let (elf, logs, _instructions) = run_asm_elf("deep_stack"); - let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), @@ -1477,7 +1477,7 @@ fn test_deep_stack_runtime_pages_roundtrip() { let executor = executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); @@ -1499,7 +1499,7 @@ 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); + let verifier_configs = Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, &[]); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1536,7 +1536,7 @@ fn test_deep_stack_missing_pages_rejected() { let executor = executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); // Prover uses correct page configs (auto-detected from MemoryState) let table_counts = traces.table_counts(); @@ -1553,7 +1553,7 @@ 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, &[]); + let wrong_configs = Traces::page_configs_from_elf_and_runtime(&elf, &[], &[]); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &wrong_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1586,7 +1586,7 @@ fn test_deep_stack_missing_pages_rejected() { #[test] fn test_heap_alloc_passes() { let (elf, logs, _instructions) = run_asm_elf("heap_alloc"); - let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &logs, &Default::default(), &[]).unwrap(); // Verify runtime_page_ranges encodes the heap pages as a contiguous range let ranges = traces.runtime_page_ranges(); @@ -1614,7 +1614,7 @@ fn test_heap_alloc_runtime_pages_roundtrip() { let executor = executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default()).unwrap(); + let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); @@ -1640,7 +1640,7 @@ 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); + let verifier_configs = Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, &[]); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1913,6 +1913,44 @@ fn test_prove_with_input_empty() { ); } +/// Tiny ASM test: reads private input directly from 0xFF000000 via memory-mapped access, +/// commits 8 bytes. Validates the full prove+verify pipeline with a non-empty private input. +#[test] +fn test_prove_private_input_xpage() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); + // 16 bytes of data; at 0xFF000008 (offset 4 into data) we'll read bytes [4..12] + let input: Vec = (0u8..16).collect(); + let proof = crate::prove_with_input(&elf_bytes, input.clone()) + .expect("prove_with_input should succeed"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify should not error"), + "proof should verify" + ); + // The ASM commits 8 bytes starting at 0xFF000008 (offset 4 into data region) + assert_eq!(proof.public_output, input[4..12].to_vec()); +} + +/// End-to-end test: prove+verify a Rust program (commit_sum) with private input. +/// commit_sum reads input[0] + input[1] and commits the u8 sum. +#[test] +#[ignore = "Long-running: commit_sum uses std, ~10M instructions"] +fn test_prove_commit_sum() { + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/commit_sum.elf")) + .expect("commit_sum.elf"); + let proof = crate::prove_with_input(&elf_bytes, vec![3u8, 5u8]) + .expect("prove_with_input should succeed"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify should not error"), + "proof should verify" + ); + assert_eq!(proof.public_output, vec![8u8]); // 3 + 5 +} + /// Regression test: addiw with negative immediate must verify. /// arg2_sign_bit is the sign bit of rv2 (bit 31), not of arg2, per spec /// constraint CPU-CE61: MSB16[arg2_sign_bit; rv2[1]]. diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index e31b66619..fc7526e97 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -1,15 +1,16 @@ #[cfg(target_arch = "riscv64")] use core::arch::asm; +/// Memory-mapped private input region start address. +/// Layout: 4-byte LE length prefix at this address, data at +4. +/// The host pre-loads the input; the guest reads directly (no ecall). #[cfg(target_arch = "riscv64")] -// TODO: This should be properly defined -const MAX_PRIVATE_INPUT_SIZE: usize = 6700000; +const PRIVATE_INPUT_START: usize = 0xFF000000; #[cfg(target_arch = "riscv64")] enum SyscallNumbers { Print = 1, Panic = 2, - GetPrivateInputs = 4, Commit = 64, Halt = 93, } @@ -76,26 +77,26 @@ pub fn commit(slice: &[u8]) { } } +/// Read private input bytes from the memory-mapped region at +/// `PRIVATE_INPUT_START = 0xFF000000`. +/// +/// The host pre-loads the input before execution; this function reads the +/// 4-byte LE length prefix and then copies the data bytes into a new `Vec`. +/// No ecall is performed — it's a plain memory read (ZisK-style). #[cfg(target_arch = "riscv64")] pub fn get_private_input() -> Result, SyscallError> { - print_string("get_private_input called\n"); - let mut dest = vec![0u8; MAX_PRIVATE_INPUT_SIZE]; - unsafe { - asm!( - "ecall", - in("a0") dest.as_mut_ptr(), - in("a7") SyscallNumbers::GetPrivateInputs as usize, - ) - } - let len = u32::from_le_bytes( - dest[0..4] - .try_into() - .map_err(|_| SyscallError::WrongPrivateInputSize)?, - ) as usize; - dest.drain(0..4); - dest.truncate(len); - - Ok(dest) + // Read length prefix (4 bytes LE at PRIVATE_INPUT_START) + let len_ptr = PRIVATE_INPUT_START as *const u32; + let len = unsafe { core::ptr::read_volatile(len_ptr) } as usize; + // Read data bytes starting at PRIVATE_INPUT_START + 4 + let data_ptr = (PRIVATE_INPUT_START + 4) as *const u8; + let slice = unsafe { core::slice::from_raw_parts(data_ptr, len) }; + Ok(slice.to_vec()) +} + +#[cfg(not(target_arch = "riscv64"))] +pub fn get_private_input() -> Result, SyscallError> { + unimplemented!("syscalls are only implemented for riscv64 targets"); } #[derive(Debug)] From 3c9c038e6adf77134320856bfb891fb9b9f1bcaf Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 15 Apr 2026 12:48:03 -0300 Subject: [PATCH 3/8] add tests --- prover/src/tests/prove_elfs_tests.rs | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index bb2ed7f75..23c4cf665 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1930,6 +1930,55 @@ fn test_prove_private_input_xpage() { assert_eq!(proof.public_output, input[4..12].to_vec()); } +/// Same ASM program but with different input values to make sure the output +/// actually depends on the private input (not a hardcoded constant masquerading +/// as a proof). +#[test] +fn test_prove_private_input_different_values() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); + let input: Vec = vec![ + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, + ]; + let proof = crate::prove_with_input(&elf_bytes, input.clone()).expect("prove"); + assert!(crate::verify(&proof, &elf_bytes).expect("verify"), "proof should verify"); + // ASM commits 8 bytes starting at 0xFF000008 = input[4..12] + assert_eq!(proof.public_output, input[4..12].to_vec()); +} + +/// Security test: verifier must reject a proof whose `private_input` field was +/// tampered with after proving. Private input is committed via the PAGE +/// preprocessed commitments at 0xFF000000, so any mismatch between the prover's +/// input and the verifier's reconstructed pages must fail verification. +#[test] +fn test_verify_rejects_tampered_private_input() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); + let input: Vec = (0u8..16).collect(); + let vm_proof = crate::prove_with_input(&elf_bytes, input.clone()).expect("prove"); + + // Baseline: untampered proof must verify. + assert!( + crate::verify(&vm_proof, &elf_bytes).expect("verify should not error"), + "Baseline proof must verify before tampering" + ); + + // Tamper: flip a bit in the private input. + let mut tampered_input = vm_proof.private_input.clone(); + tampered_input[0] ^= 0x01; + let tampered_proof = crate::VmProof { + private_input: tampered_input, + ..vm_proof + }; + + // The verifier reconstructs PAGE commitments from the tampered input, which + // don't match the prover's commitments — verification must fail. + let verified = crate::verify(&tampered_proof, &elf_bytes) + .expect("verify should not error on tampered input"); + assert!( + !verified, + "Verifier must reject proof with tampered private_input" + ); +} + /// End-to-end test: prove+verify a Rust program (commit_sum) with private input. /// commit_sum reads input[0] + input[1] and commits the u8 sum. #[test] From eb5cc73fd1eead47ed344451615a77a395bc8312 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 15 Apr 2026 16:01:15 -0300 Subject: [PATCH 4/8] fix preloaded memory --- .../rust/pure_commit/.cargo/config.toml | 6 +++ executor/programs/rust/pure_commit/Cargo.lock | 7 +++ executor/programs/rust/pure_commit/Cargo.toml | 8 ++++ .../programs/rust/pure_commit/src/main.rs | 40 ++++++++++++++++ executor/tests/asm.rs | 11 +++++ prover/src/tests/prove_elfs_tests.rs | 48 ++++++++++++++++++- syscalls/src/allocator.rs | 3 +- syscalls/src/syscalls.rs | 3 +- 8 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 executor/programs/rust/pure_commit/.cargo/config.toml create mode 100644 executor/programs/rust/pure_commit/Cargo.lock create mode 100644 executor/programs/rust/pure_commit/Cargo.toml create mode 100644 executor/programs/rust/pure_commit/src/main.rs diff --git a/executor/programs/rust/pure_commit/.cargo/config.toml b/executor/programs/rust/pure_commit/.cargo/config.toml new file mode 100644 index 000000000..be730c3ec --- /dev/null +++ b/executor/programs/rust/pure_commit/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.riscv64im-lambda-vm-elf] +rustflags = [ + "-C", "link-arg=-e", + "-C", "link-arg=main", + "-C", "passes=lower-atomic" +] diff --git a/executor/programs/rust/pure_commit/Cargo.lock b/executor/programs/rust/pure_commit/Cargo.lock new file mode 100644 index 000000000..d365437ff --- /dev/null +++ b/executor/programs/rust/pure_commit/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "pure_commit" +version = "0.1.0" diff --git a/executor/programs/rust/pure_commit/Cargo.toml b/executor/programs/rust/pure_commit/Cargo.toml new file mode 100644 index 000000000..95fdfe36f --- /dev/null +++ b/executor/programs/rust/pure_commit/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] + +[package] +name = "pure_commit" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/executor/programs/rust/pure_commit/src/main.rs b/executor/programs/rust/pure_commit/src/main.rs new file mode 100644 index 000000000..14c4c5ad8 --- /dev/null +++ b/executor/programs/rust/pure_commit/src/main.rs @@ -0,0 +1,40 @@ +// Minimal Rust program that proves on main: +// - no_std, no_main (no Rust std runtime) +// - no allocator (no TLSF init) +// - no syscalls crate (no Print/Panic ecalls) +// - Only uses Commit (64) and Halt (93) ecalls, both of which have +// receivers on the Ecall bus. +#![no_std] +#![no_main] + +use core::arch::asm; +use core::panic::PanicInfo; + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +#[unsafe(export_name = "main")] +pub extern "C" fn main() -> ! { + // Commit 4 bytes [0xAA, 0xBB, 0xCC, 0xDD] + let buf: [u8; 4] = [0xAA, 0xBB, 0xCC, 0xDD]; + unsafe { + asm!( + "ecall", + in("a0") 1usize, // fd = stdout + in("a1") buf.as_ptr(), + in("a2") 4usize, + in("a7") 64usize, // Commit + ); + } + // Halt + unsafe { + asm!( + "ecall", + in("a0") 0usize, // exit_code = 0 + in("a7") 93usize, // Halt + ); + } + loop {} +} diff --git a/executor/tests/asm.rs b/executor/tests/asm.rs index cbc1adec5..fa93cb073 100644 --- a/executor/tests/asm.rs +++ b/executor/tests/asm.rs @@ -21,6 +21,17 @@ fn run_program(elf_path: &str) { ); } +#[test] +fn test_debug_commit_sum_count() { + let elf_data = std::fs::read("./program_artifacts/rust/commit_sum.elf").unwrap(); + let program = Elf::load(&elf_data).unwrap(); + let executor = Executor::new(&program, vec![3u8, 5u8]).unwrap(); + let result = executor.run().unwrap(); + eprintln!("Instructions: {}", result.logs.len()); + eprintln!("Output: {:?}", result.return_values.memory_values); + assert_eq!(result.return_values.memory_values, vec![8u8]); +} + /// Test that the memory-mapped private input region is readable by guest programs. /// The ASM program reads from 0xFF000000 and commits 8 bytes of data. #[test] diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 23c4cf665..7b7adfec0 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1981,8 +1981,14 @@ fn test_verify_rejects_tampered_private_input() { /// End-to-end test: prove+verify a Rust program (commit_sum) with private input. /// commit_sum reads input[0] + input[1] and commits the u8 sum. +/// +/// NOTE: currently ignored because Rust std programs hit a pre-existing +/// Memory/Shift bus imbalance (likely from `init_allocator`'s TLSF heap setup). +/// This is unrelated to the private input handling — confirmed by running the +/// ASM tests above (`test_prove_private_input_xpage` etc.) which exercise the +/// full memory-mapped private input path and verify correctly. #[test] -#[ignore = "Long-running: commit_sum uses std, ~10M instructions"] +#[ignore = "Blocked on pre-existing Rust std allocator bus imbalance (not private-input related)"] fn test_prove_commit_sum() { let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() @@ -2000,6 +2006,46 @@ fn test_prove_commit_sum() { assert_eq!(proof.public_output, vec![8u8]); // 3 + 5 } +/// Minimal Rust program that proves on main: no_std, no_main, +/// no syscalls crate → only Commit + Halt ecalls (both have receivers). +/// Demonstrates that Rust CAN prove when it avoids Print/Panic. +#[test] +fn test_pure_commit_rust() { + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/pure_commit.elf")) + .unwrap(); + let proof = crate::prove(&elf_bytes).expect("prove should succeed"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify should not error"), + "pure_commit.elf should verify — it has no Print/Panic ecalls" + ); + assert_eq!(proof.public_output, vec![0xAA, 0xBB, 0xCC, 0xDD]); +} + +#[test] +#[ignore = "Requires ~128GB RAM: proves an empty Ethereum block"] +fn test_prove_ethrex_empty_block() { + let _ = env_logger::builder().is_test(true).try_init(); + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let elf_bytes = std::fs::read(workspace_root.join("executor/program_artifacts/rust/ethrex.elf")) + .expect("need ethrex.elf"); + let input = + std::fs::read(workspace_root.join("executor/tests/ethrex_empty_block.bin")).unwrap(); + let proof = crate::prove_with_input(&elf_bytes, input).expect("prove"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify"), + "ethrex empty block should verify" + ); + assert_eq!(proof.public_output.len(), 160); +} + /// Regression test: addiw with negative immediate must verify. /// arg2_sign_bit is the sign bit of rv2 (bit 31), not of arg2, per spec /// constraint CPU-CE61: MSB16[arg2_sign_bit; rv2[1]]. diff --git a/syscalls/src/allocator.rs b/syscalls/src/allocator.rs index 2469f435d..5cdf64785 100644 --- a/syscalls/src/allocator.rs +++ b/syscalls/src/allocator.rs @@ -49,6 +49,7 @@ pub unsafe extern "C" fn sys_getenv( _varname: *const u8, _varname_len: usize, ) -> usize { - print_string("sys_getenv is disabled"); + // NOTE: no print_string here — the Print ecall (#1) has no receiver on the + // Ecall bus and would cause a verification failure. usize::MAX } diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index fc7526e97..fb7426401 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -106,7 +106,8 @@ pub enum SyscallError { #[cfg(target_arch = "riscv64")] pub fn sys_halt() -> ! { - print_string("sys_halt called\n"); + // NOTE: no print_string here — the Print ecall is unmatched on the Ecall bus + // and would cause a verification failure. unsafe { asm!( "ecall", From aaf9299dcc1ea4fece5f550a7cc7062832d9145f Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 15 Apr 2026 17:41:26 -0300 Subject: [PATCH 5/8] fix_bugs --- .../programs/rust/pure_commit/src/main.rs | 11 +++--- executor/tests/asm.rs | 11 ------ executor/tests/ethrex_empty_block.bin | Bin 0 -> 1876 bytes prover/src/lib.rs | 8 +++- prover/src/tables/shift.rs | 16 ++++++-- prover/src/tables/trace_builder.rs | 36 ++++++++++-------- prover/src/tables/types.rs | 10 ++++- prover/src/tests/prove_elfs_tests.rs | 29 ++++++++++---- 8 files changed, 75 insertions(+), 46 deletions(-) create mode 100644 executor/tests/ethrex_empty_block.bin diff --git a/executor/programs/rust/pure_commit/src/main.rs b/executor/programs/rust/pure_commit/src/main.rs index 14c4c5ad8..c23b46f79 100644 --- a/executor/programs/rust/pure_commit/src/main.rs +++ b/executor/programs/rust/pure_commit/src/main.rs @@ -1,9 +1,8 @@ -// Minimal Rust program that proves on main: -// - no_std, no_main (no Rust std runtime) -// - no allocator (no TLSF init) -// - no syscalls crate (no Print/Panic ecalls) -// - Only uses Commit (64) and Halt (93) ecalls, both of which have -// receivers on the Ecall bus. +// Minimal Rust guest program: no_std, no_main, no allocator, no syscalls crate. +// Uses only raw inline `asm!("ecall")` for Commit (64) and Halt (93). +// Serves as a control case in the prover test suite (`test_pure_commit_rust`): +// verifies that Rust can compile to a provable ELF when the heap allocator is +// bypassed, independent of the Rust-std startup path. #![no_std] #![no_main] diff --git a/executor/tests/asm.rs b/executor/tests/asm.rs index fa93cb073..cbc1adec5 100644 --- a/executor/tests/asm.rs +++ b/executor/tests/asm.rs @@ -21,17 +21,6 @@ fn run_program(elf_path: &str) { ); } -#[test] -fn test_debug_commit_sum_count() { - let elf_data = std::fs::read("./program_artifacts/rust/commit_sum.elf").unwrap(); - let program = Elf::load(&elf_data).unwrap(); - let executor = Executor::new(&program, vec![3u8, 5u8]).unwrap(); - let result = executor.run().unwrap(); - eprintln!("Instructions: {}", result.logs.len()); - eprintln!("Output: {:?}", result.return_values.memory_values); - assert_eq!(result.return_values.memory_values, vec![8u8]); -} - /// Test that the memory-mapped private input region is readable by guest programs. /// The ASM program reads from 0xFF000000 and commits 8 bytes of data. #[test] diff --git a/executor/tests/ethrex_empty_block.bin b/executor/tests/ethrex_empty_block.bin new file mode 100644 index 0000000000000000000000000000000000000000..b3551200a069208628f0d30ca1164f09434b9e05 GIT binary patch literal 1876 zcmbPl*)`$$yyY^@3LM97K4o=Y@6ey4Ak@s|v1Eswy9w6~*)zT`?j4VxY=jO>o6{}&;PGa)ioK3ka?alJ z7fT3xAuld{CUn{VX4hwv4*bZA_IMyYGiU7q#w7i%kCV435|YPa<){!+2rxp~4Tn#{ zm|zhes5sBR|NoJc6J#+xwI8+X6TT2V()D z^g9Kc#s!2@@B%_fl%Rs01pST8VZ%zSp`nHGAvGFl5aj~%T z^D#5=uyS!SvN3ZB{P1b$;C--d^YRo127)oR;OHec#eki2Zm`6*E-Bdh>;HdHtY$zHYUFA2U7~Xy`0!JYmkYonpluREmpA$%e#L)SH zQT1SXejv#O#P;5szaphhSvd4U8>EpkZI2vGxW?`{6Yl7M}vpVw4G8H2@Mo B`RV`w literal 0 HcmV?d00001 diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 3b21f9126..60fff0804 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -493,8 +493,12 @@ pub fn prove(elf_bytes: &[u8]) -> Result { /// Prove an ELF binary execution with a private input. /// -/// The `private_input` bytes are passed to the executor; the guest program -/// retrieves them via the `GetPrivateInputs` syscall. +/// The `private_input` bytes are pre-loaded at `PRIVATE_INPUT_START_INDEX` +/// (`0xFF000000`) as an initial memory segment (4-byte LE length prefix + +/// data). The guest reads them via normal RISC-V loads — see +/// `syscalls::syscalls::get_private_input`. The bytes are also included in +/// the returned `VmProof` so the verifier can reconstruct the matching PAGE +/// preprocessed commitments. pub fn prove_with_input(elf_bytes: &[u8], private_input: Vec) -> Result { prove_with_options( elf_bytes, diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index bb535c537..c5a9be32e 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -181,8 +181,14 @@ impl ShiftOperation { let left = !self.direction; let right = self.direction; - // is_negative = MSB of in[3] - let is_negative = (self.in_halves[3] >> 15) & 1 == 1; + // is_negative is the MSB of in[3] BUT gated by `signed`. The SHIFT + // 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 + // 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; let extension: u16 = if is_negative { 0xFFFF } else { 0 }; // bit_shift @@ -642,7 +648,11 @@ impl ShiftConstraint { let left = &mu - &dir; // μ - direction let right = dir; - // extension = 65535 * is_negative + // extension = 65535 * is_negative (virtual; matches SHIFT-C7 HWSL send). + // The IS_NEGATIVE column is only constrained by MSB16 when signed=1 + // (SHIFT-C14's bus send is gated by SIGNED). For SRL (signed=0) the + // trace sets IS_NEGATIVE=0 explicitly so extension=0 → x[4]=0 → no + // spurious extension bleed into `shifted`. let is_neg = step.get_main_evaluation_element(0, cols::IS_NEGATIVE); let extension = is_neg * FieldElement::::from(65535u64); diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 9ae0488af..7debcb9ae 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1343,19 +1343,14 @@ fn collect_byte_check_ops_for_padding(num_padding_rows: usize) -> Vec HashMap> { use executor::vm::memory::PRIVATE_INPUT_START_INDEX; let page_size = page::DEFAULT_PAGE_SIZE; @@ -1400,6 +1395,13 @@ fn build_init_page_data(elf: &Elf, private_input: &[u8]) -> HashMap init_page_data } +/// Collects IS_BYTE lookups from PAGE data (init and fini values). +/// +/// Each PAGE byte generates 2 IS_BYTE lookups: +/// - C1: IS_BYTE[init] for initialization range check +/// - C2: IS_BYTE[fini] for finalization range check +/// +/// This must be called BEFORE bitwise multiplicities are updated. fn collect_bitwise_from_page( elf: &Elf, memory_state: &MemoryState, @@ -1694,11 +1696,13 @@ impl Traces { } } - /// Extract page configurations from ELF only (deterministic from binary). + /// Extract page configurations for the deterministic portion of memory: + /// ELF segments and the private-input region at `PRIVATE_INPUT_START_INDEX`. /// - /// Returns PageConfigs for pages covered by ELF segments, with their - /// init data populated. Used by the verifier to reconstruct the ELF - /// portion of the PAGE table layout. + /// Returns PageConfigs for every page covered by an ELF segment or by the + /// private-input bytes, with init data populated. Used by the verifier to + /// reconstruct these pages (runtime-only pages like stack/heap come from + /// `page_configs_from_elf_and_runtime`). pub fn page_configs_from_elf(elf: &Elf, private_input: &[u8]) -> Vec { let page_size = page::DEFAULT_PAGE_SIZE; let init_page_data = build_init_page_data(elf, private_input); diff --git a/prover/src/tables/types.rs b/prover/src/tables/types.rs index 8e89490ae..bd45c3861 100644 --- a/prover/src/tables/types.rs +++ b/prover/src/tables/types.rs @@ -668,7 +668,15 @@ impl DecodeEntry { } Instruction::CSR { .. } => { - // CSR instructions not yet supported in prover + // CSR instructions are executed as no-ops by the VM (see + // `executor/src/vm/instruction/execution.rs` Instruction::CSR arm + // returning `dst_val: 0, src1/2_val: 0`). Mirror that here by + // treating them as `ADDI x0, x0, 0` — same pattern as `Fence`. + // This sets `op_add=true` so CM54's "1 - pad" (sum of ALU flags) + // multiplicity is non-zero and the CPU's PC-update Memw sender + // fires to match the MEMW_R PC row that + // `collect_register_ops_from_cpu` unconditionally emits. + entry.op_add = true; } Instruction::EcallEbreak => { diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 7b7adfec0..ac33eee88 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1981,14 +1981,7 @@ fn test_verify_rejects_tampered_private_input() { /// End-to-end test: prove+verify a Rust program (commit_sum) with private input. /// commit_sum reads input[0] + input[1] and commits the u8 sum. -/// -/// NOTE: currently ignored because Rust std programs hit a pre-existing -/// Memory/Shift bus imbalance (likely from `init_allocator`'s TLSF heap setup). -/// This is unrelated to the private input handling — confirmed by running the -/// ASM tests above (`test_prove_private_input_xpage` etc.) which exercise the -/// full memory-mapped private input path and verify correctly. #[test] -#[ignore = "Blocked on pre-existing Rust std allocator bus imbalance (not private-input related)"] fn test_prove_commit_sum() { let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() @@ -2006,6 +1999,28 @@ fn test_prove_commit_sum() { assert_eq!(proof.public_output, vec![8u8]); // 3 + 5 } +/// Regression test: a minimal Rust std program that uses `init_allocator()` +/// and `String::from("Hello World") + commit`. Exercises the full stack: +/// TLSF heap init, SRL on high-bit-set values (fixed in `shift::compute_aux`) +/// and CSR instructions (fixed in `DecodeEntry::from(Instruction::CSR)`). +#[test] +fn test_prove_allocator_minimal_reproducer() { + let _ = env_logger::builder().is_test(true).try_init(); + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/allocator.elf")) + .unwrap(); + let proof = crate::prove(&elf_bytes).expect("prove should succeed"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify should not error"), + "allocator.elf should verify" + ); + assert_eq!(proof.public_output, b"Hello World"); +} + /// Minimal Rust program that proves on main: no_std, no_main, /// no syscalls crate → only Commit + Halt ecalls (both have receivers). /// Demonstrates that Rust CAN prove when it avoids Print/Panic. From 95c2b38b18d0a0ac652f4b64dedcf7307414b001 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Fri, 17 Apr 2026 11:57:37 -0300 Subject: [PATCH 6/8] rm unused import --- syscalls/src/allocator.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/syscalls/src/allocator.rs b/syscalls/src/allocator.rs index 5cdf64785..08dbb2d92 100644 --- a/syscalls/src/allocator.rs +++ b/syscalls/src/allocator.rs @@ -1,8 +1,6 @@ use embedded_alloc::TlsfHeap as Heap; use riscv as _; -use crate::syscalls::print_string; - #[global_allocator] static HEAP: Heap = Heap::empty(); From 149f022c2d6bf1d6589c0d05614cfdfb088275ce Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Fri, 17 Apr 2026 18:16:38 -0300 Subject: [PATCH 7/8] fix bug --- executor/src/vm/instruction/execution.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/executor/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index e89a11fb7..a5222557a 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -64,8 +64,12 @@ impl Instruction { } } Instruction::ArithImmW { dst, src, imm, op } => { - // W-suffix: operate on lower 32 bits, sign-extend result to 64 bits - let op1 = registers.read(src)? as i32; + // W-suffix: operate on lower 32 bits, sign-extend result to 64 bits. + // Log must store the RAW register value in src1_val (full 64 bits) + // for the prover's MEMW register chain. The truncation to i32 is only + // for the ALU computation. + let raw_src = registers.read(src)?; + let op1 = raw_src as i32; if matches!(op, ArithOp::Sub) { return Err(ExecutionError::SubImmNotSupported); } @@ -75,7 +79,7 @@ impl Instruction { Log { current_pc: pc, next_pc: pc.wrapping_add(REGULAR_PC_UPDATE), - src1_val: op1 as u64, + src1_val: raw_src, src2_val: 0, dst_val: res, } @@ -245,17 +249,21 @@ impl Instruction { src2, op, } => { - // W-suffix: operate on lower 32 bits, sign-extend result to 64 bits - let a = registers.read(src1)? as i32; - let b = registers.read(src2)? as i32; + // W-suffix: operate on lower 32 bits, sign-extend result to 64 bits. + // Log must store RAW register values (full 64 bits) for the prover's + // MEMW register chain. Truncation to i32 is only for ALU computation. + let raw_src1 = registers.read(src1)?; + let raw_src2 = registers.read(src2)?; + let a = raw_src1 as i32; + let b = raw_src2 as i32; let res32 = op.apply_word(a, b)?; let res = res32 as i64 as u64; // Sign-extend to 64 bits registers.write(dst, res)?; Log { current_pc: pc, next_pc: pc.wrapping_add(REGULAR_PC_UPDATE), - src1_val: a as u64, - src2_val: b as u64, + src1_val: raw_src1, + src2_val: raw_src2, dst_val: res, } } From 9559d16fbc4a5e2a2573a9e1ec9a7bef57a9fb30 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Fri, 17 Apr 2026 18:48:13 -0300 Subject: [PATCH 8/8] fmt --- prover/src/lib.rs | 11 +++--- prover/src/tests/prove_elfs_tests.rs | 59 ++++++++++++++++++---------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 60fff0804..a9b98b0c9 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -620,12 +620,11 @@ pub fn verify_with_options( vm_proof.table_counts.validate()?; let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; - let page_configs = - Traces::page_configs_from_elf_and_runtime( - &program, - &vm_proof.runtime_page_ranges, - &vm_proof.private_input, - ); + let page_configs = Traces::page_configs_from_elf_and_runtime( + &program, + &vm_proof.runtime_page_ranges, + &vm_proof.private_input, + ); // Cross-check: table_counts must match the number of sub-proofs. // Fixed tables (bitwise, decode, halt, commit, register) = 5, plus page tables. diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index ac33eee88..2ce3cfc5a 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -731,7 +731,8 @@ fn test_prove_elfs_test_commit_4() { "Public output should match committed bytes" ); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); assert_eq!( traces.public_output_bytes, result.return_values.memory_values @@ -756,7 +757,8 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { let executor = executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); // Prover uses correct page configs let table_counts = traces.table_counts(); @@ -801,8 +803,9 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { fn test_verify_rejects_tampered_public_output() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_commit_4"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) - .expect("Prover should succeed for test_commit_4"); + let vm_proof = + crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) + .expect("Prover should succeed for test_commit_4"); assert!( crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) .expect("Valid commit proof should verify"), @@ -1477,7 +1480,8 @@ fn test_deep_stack_runtime_pages_roundtrip() { let executor = executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); @@ -1499,7 +1503,8 @@ 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, &[]); + let verifier_configs = + Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, &[]); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1536,7 +1541,8 @@ fn test_deep_stack_missing_pages_rejected() { let executor = executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); // Prover uses correct page configs (auto-detected from MemoryState) let table_counts = traces.table_counts(); @@ -1614,7 +1620,8 @@ fn test_heap_alloc_runtime_pages_roundtrip() { let executor = executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); - let mut traces = Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); + let mut traces = + Traces::from_elf_and_logs(&elf, &result.logs, &Default::default(), &[]).unwrap(); let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); @@ -1640,7 +1647,8 @@ 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, &[]); + let verifier_configs = + Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, &[]); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1696,8 +1704,9 @@ fn test_verify_rejects_zero_table_counts() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) - .expect("Prover should succeed on valid program"); + let vm_proof = + crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) + .expect("Prover should succeed on valid program"); assert!( crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) @@ -1731,8 +1740,9 @@ fn test_verify_rejects_zero_cpu_count() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) - .expect("Prover should succeed on valid program"); + let vm_proof = + crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) + .expect("Prover should succeed on valid program"); let tampered_proof = crate::VmProof { table_counts: crate::TableCounts { @@ -1752,8 +1762,9 @@ fn test_verify_rejects_zero_memw_count() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) - .expect("Prover should succeed on valid program"); + let vm_proof = + crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) + .expect("Prover should succeed on valid program"); let tampered_proof = crate::VmProof { table_counts: crate::TableCounts { @@ -1879,8 +1890,9 @@ fn test_verify_rejects_inflated_table_counts() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let proof_options = ProofOptions::default_test_options(); - let vm_proof = crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) - .expect("Prover should succeed on valid program"); + let vm_proof = + crate::prove_with_options(&elf_bytes, vec![], &proof_options, &Default::default()) + .expect("Prover should succeed on valid program"); // Inflate cpu count — total won't match proof.proofs.len() let tampered_proof = crate::VmProof { @@ -1937,10 +1949,14 @@ fn test_prove_private_input_xpage() { fn test_prove_private_input_different_values() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); let input: Vec = vec![ - 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, + 0x00, ]; let proof = crate::prove_with_input(&elf_bytes, input.clone()).expect("prove"); - assert!(crate::verify(&proof, &elf_bytes).expect("verify"), "proof should verify"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify"), + "proof should verify" + ); // ASM commits 8 bytes starting at 0xFF000008 = input[4..12] assert_eq!(proof.public_output, input[4..12].to_vec()); } @@ -2049,8 +2065,9 @@ fn test_prove_ethrex_empty_block() { .parent() .unwrap() .to_path_buf(); - let elf_bytes = std::fs::read(workspace_root.join("executor/program_artifacts/rust/ethrex.elf")) - .expect("need ethrex.elf"); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/ethrex.elf")) + .expect("need ethrex.elf"); let input = std::fs::read(workspace_root.join("executor/tests/ethrex_empty_block.bin")).unwrap(); let proof = crate::prove_with_input(&elf_bytes, input).expect("prove");