From 82ca0023a50288143788e8097b4f35d796d80f26 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Mon, 20 Apr 2026 11:11:33 -0300 Subject: [PATCH 01/19] fix executor bugs --- executor/programs/asm/test_wsuffix_64bit.s | 36 +++++++++++++ .../rust/pure_commit/.cargo/config.toml | 6 +++ executor/programs/rust/pure_commit/Cargo.toml | 8 +++ .../programs/rust/pure_commit/src/main.rs | 39 ++++++++++++++ executor/src/vm/instruction/execution.rs | 24 ++++++--- prover/src/tables/shift.rs | 10 +++- prover/src/tables/types.rs | 8 ++- prover/src/tests/prove_elfs_tests.rs | 51 +++++++++++++++++++ syscalls/src/allocator.rs | 5 +- syscalls/src/syscalls.rs | 3 +- 10 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 executor/programs/asm/test_wsuffix_64bit.s create mode 100644 executor/programs/rust/pure_commit/.cargo/config.toml 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/asm/test_wsuffix_64bit.s b/executor/programs/asm/test_wsuffix_64bit.s new file mode 100644 index 000000000..1e57a1ff2 --- /dev/null +++ b/executor/programs/asm/test_wsuffix_64bit.s @@ -0,0 +1,36 @@ + .attribute 5, "rv64i2p1" + .globl main +main: + # Exercises W-suffix instructions (ADDIW, SRLIW) on a register holding + # a 64-bit value with non-zero upper 32 bits. The executor's Log must + # store the full 64-bit register value in src1_val/src2_val so the + # prover's MEMW_R Memory bus chain stays consistent. + + # Load a 64-bit value with non-zero hi32 into a0 (x10). + # 0xDEADBEEF_12345678 + li t0, 0xDEADBEEF # t0 = 0xDEADBEEF (sign-extended) + slli t0, t0, 32 # t0 = 0xDEADBEEF_00000000 + li t1, 0x12345678 + or a0, t0, t1 # a0 = 0xDEADBEEF_12345678 + + # Execute ADDIW on a0 — reads a0 (64-bit) but operates on lower 32. + # If src1_val is truncated to 32 bits, the upper 0xDEADBEEF is lost and + # the prover's MEMW_R chain for x10 word 1 won't balance. + addiw t2, a0, 1 # t2 = sign_extend32(0x12345678 + 1) = 0x12345679 + + # Execute SRLIW — another W-suffix that reads a0. + srliw t3, a0, 4 # t3 = sign_extend32(0x12345678 >> 4) = 0x01234567 + + # Commit 8 bytes of a0 (the original 64-bit value should be intact). + # a0 was never written by ADDIW/SRLIW (they write t2/t3, not a0). + addi a1, sp, -8 # buf on stack + sd a0, 0(a1) # store a0 to buf + li a0, 1 # fd = 1 + li a2, 8 # count = 8 + li a7, 64 # Commit + ecall + + # Halt + li a0, 0 + li a7, 93 + ecall 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.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..c23b46f79 --- /dev/null +++ b/executor/programs/rust/pure_commit/src/main.rs @@ -0,0 +1,39 @@ +// 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] + +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/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index dcab3f927..3cd299143 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -66,8 +66,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); } @@ -77,7 +81,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, } @@ -247,17 +251,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, } } diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index bb535c537..ffa247feb 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 diff --git a/prover/src/tables/types.rs b/prover/src/tables/types.rs index 8e89490ae..fcedf869a 100644 --- a/prover/src/tables/types.rs +++ b/prover/src/tables/types.rs @@ -668,7 +668,13 @@ impl DecodeEntry { } Instruction::CSR { .. } => { - // CSR instructions not yet supported in prover + // CSR instructions are executed as no-ops by the VM (see + // executor 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 multiplicity is non-zero and the + // CPU's PC-update Memw sender fires. + 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 9a1b8a6d0..ee8890667 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1899,6 +1899,57 @@ fn test_verify_rejects_inflated_table_counts() { ); } +/// Proves a program that uses W-suffix instructions (ADDIW, SRLIW) on a +/// register holding a 64-bit value with non-zero upper 32 bits. +/// Verifies that the full 64-bit value is preserved in the MEMW_R chain. +#[test] +fn test_prove_wsuffix_64bit() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_wsuffix_64bit"); + let result = crate::prove_and_verify(&elf_bytes).expect("prove_and_verify failed"); + assert!(result, "W-suffix 64-bit register test should verify"); +} + +/// Proves a minimal Rust std program that uses `init_allocator()` and +/// `String::from("Hello World") + commit`. Exercises the full Rust-std stack: +/// TLSF heap init (SRL on high-bit values), CSR instructions injected by +/// the Rust toolchain, and the allocator's memory access patterns. +#[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: no_std, no_main, no allocator, no +/// syscalls crate. Only Commit + Halt ecalls (both have receivers). +#[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" + ); + assert_eq!(proof.public_output, vec![0xAA, 0xBB, 0xCC, 0xDD]); +} + /// 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..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(); @@ -49,6 +47,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 e31b66619..f85ccd3ae 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -105,7 +105,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 4cd0262eea7d4cd0c01e0bde283b4f83be34deef Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Mon, 20 Apr 2026 11:22:35 -0300 Subject: [PATCH 02/19] add private inputs feature --- bin/cli/src/main.rs | 2 +- .../programs/asm/test_private_input_xpage.s | 32 +++ executor/src/vm/instruction/execution.rs | 10 - executor/src/vm/memory.rs | 20 +- executor/tests/asm.rs | 13 ++ executor/tests/ethrex_empty_block.bin | Bin 0 -> 1876 bytes prover/src/lib.rs | 41 +++- prover/src/tables/trace_builder.rs | 174 ++++++++------- prover/src/tests/decode_tests.rs | 2 +- prover/src/tests/prove_elfs_tests.rs | 211 +++++++++++++++--- syscalls/src/syscalls.rs | 43 ++-- 11 files changed, 385 insertions(+), 163 deletions(-) create mode 100644 executor/programs/asm/test_private_input_xpage.s create mode 100644 executor/tests/ethrex_empty_block.bin 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..abe58452c --- /dev/null +++ b/executor/programs/asm/test_private_input_xpage.s @@ -0,0 +1,32 @@ + .attribute 5, "rv64i2p1" + .globl main +main: + # 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, 0xFF000000 # 1: t0 = 0xFF000000 (private input base) + + # Read length at 0xFF000000 + lw t3, 0(t0) # 2: t3 = length + + # Load 8 bytes of data at 0xFF000008 (aligned, 4 bytes into data region) + ld t1, 8(t0) # 3 + + # 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 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 3cd299143..a5222557a 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(()), @@ -326,14 +324,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/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 7cb947f6f..a9b98b0c9 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. @@ -481,6 +485,24 @@ 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 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, + private_input, &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), &MaxRowsConfig::default(), ) @@ -489,6 +511,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 +523,12 @@ 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}")))?; + // 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 .run() .map_err(|e| Error::Execution(format!("{e}")))?; @@ -514,7 +542,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(); @@ -559,6 +588,7 @@ pub fn prove_with_options( runtime_page_ranges, table_counts, public_output: traces.public_output_bytes.clone(), + private_input: saved_private_input, }) } @@ -590,8 +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); + 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/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 94d3d5021..92f0f70b2 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -97,6 +97,24 @@ 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 (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; + 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)); + } + 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)) @@ -1323,33 +1341,29 @@ fn collect_byte_check_ops_for_padding(num_padding_rows: usize) -> Vec Vec { - use std::collections::BTreeSet; - +/// 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 +1371,47 @@ 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 +} + +/// 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, + 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() { @@ -1377,7 +1432,7 @@ fn collect_bitwise_from_page(elf: &Elf, memory_state: &MemoryState) -> Vec 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(); @@ -1775,6 +1811,7 @@ fn build_traces( entry_point: u64, decode_trace: TraceTable, decode_pc_to_row: HashMap, + private_input: &[u8], register_state: RegisterState, max_rows: &super::MaxRowsConfig, ) -> Result { @@ -1812,7 +1849,7 @@ fn build_traces( bitwise_ops.extend(collect_bitwise_from_memw_register(&memw_register_ops)); // PAGE tables do IS_BYTE lookups for init and fini values (C1, C2) if let Some(elf) = elf { - 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 @@ -1887,7 +1924,7 @@ fn build_traces( || { rayon::join( || match elf { - Some(elf) => generate_page_tables(elf, memory_state), + Some(elf) => generate_page_tables(elf, memory_state, private_input), None => (Vec::new(), Vec::new()), }, || register::generate_register_trace(®ister_final_state, entry_point), @@ -1905,7 +1942,7 @@ fn build_traces( { match elf { Some(elf) => { - let (p, c) = generate_page_tables(elf, memory_state); + let (p, c) = generate_page_tables(elf, memory_state, private_input); pages = p; page_configs = c; } @@ -1957,59 +1994,34 @@ impl Traces { } } - /// Extract page configurations from ELF only (deterministic from binary). - /// - /// 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; - + /// Extract page configurations for the deterministic portion of memory: + /// ELF segments and the private-input region at `PRIVATE_INPUT_START_INDEX`. + 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(); + let init_page_data = build_init_page_data(elf, private_input); - 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; - } - } - } - - 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); @@ -2078,6 +2090,7 @@ impl Traces { elf: &Elf, logs: &[Log], max_rows: &super::MaxRowsConfig, + private_input: &[u8], ) -> Result { // Phase 0: ELF → DECODE + instructions // IMPORTANT: Use generate_decode_trace (same as compute_precomputed_commitment) @@ -2091,6 +2104,7 @@ impl Traces { // Phase 2: Collect + route all ops 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 (memw_ops, load_ops, lt_ops, shift_ops, bitwise_ops, commit_ops) = collect_ops_from_cpu(&cpu_ops, &mut memory_state, &mut register_state); @@ -2114,6 +2128,7 @@ impl Traces { elf.entry_point, decode_trace, decode_pc_to_row, + private_input, register_state, max_rows, ) @@ -2162,6 +2177,7 @@ impl Traces { entry_point, decode_trace, decode_pc_to_row, + &[], register_state, max_rows, ) 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 ee8890667..31b150598 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,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(); @@ -774,7 +776,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(); @@ -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, &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"), @@ -1126,7 +1129,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 +1459,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 +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(); @@ -1553,7 +1559,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 +1592,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 +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, &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, &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, &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 { @@ -1828,7 +1839,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,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, &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 { @@ -1899,6 +1911,91 @@ 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" + ); +} + +/// 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()); +} + +/// 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. /// Proves a program that uses W-suffix instructions (ADDIW, SRLIW) on a /// register holding a 64-bit value with non-zero upper 32 bits. /// Verifies that the full 64-bit value is preserved in the MEMW_R chain. @@ -1909,10 +2006,30 @@ fn test_prove_wsuffix_64bit() { assert!(result, "W-suffix 64-bit register test should verify"); } -/// Proves a minimal Rust std program that uses `init_allocator()` and -/// `String::from("Hello World") + commit`. Exercises the full Rust-std stack: -/// TLSF heap init (SRL on high-bit values), CSR instructions injected by -/// the Rust toolchain, and the allocator's memory access patterns. +/// 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] +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: 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(); @@ -1931,8 +2048,9 @@ fn test_prove_allocator_minimal_reproducer() { assert_eq!(proof.public_output, b"Hello World"); } -/// Minimal Rust program that proves: no_std, no_main, no allocator, no -/// syscalls crate. Only Commit + Halt ecalls (both have receivers). +/// 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")) @@ -1945,11 +2063,32 @@ fn test_pure_commit_rust() { 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" + "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/syscalls.rs b/syscalls/src/syscalls.rs index f85ccd3ae..fb7426401 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 7adb2d93908905fa4997f55b33f61b58a23f37f6 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 11:53:22 -0300 Subject: [PATCH 03/19] merge --- bin/cli/src/main.rs | 2 +- prover/src/lib.rs | 76 ++++++++++++++------------ prover/src/tables/trace_builder.rs | 82 +++++++++++----------------- prover/src/tests/prove_elfs_tests.rs | 66 +++++++++++----------- 4 files changed, 107 insertions(+), 119 deletions(-) diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 162a201ef..26e2cc90c 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -457,7 +457,7 @@ fn cmd_verify(proof_path: PathBuf, elf_path: PathBuf, blowup: Option, time: return ExitCode::FAILURE; } }; - prover::verify_with_options(&proof, &elf_data, &opts) + prover::verify_with_options(&proof, &elf_data, &opts, &[]) } None => prover::verify(&proof, &elf_data), }; diff --git a/prover/src/lib.rs b/prover/src/lib.rs index a9b98b0c9..8cf6becd0 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -138,10 +138,6 @@ 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. @@ -483,26 +479,14 @@ pub(crate) fn compute_expected_commit_bus_balance( /// Prove an ELF binary execution. Returns a serializable proof bundle. 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_with_inputs(elf_bytes, &[]) } -/// Prove an ELF binary execution with a private input. -/// -/// 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( +/// Prove an ELF binary execution with private inputs. Returns a serializable proof bundle. +pub fn prove_with_inputs(elf_bytes: &[u8], private_inputs: &[u8]) -> Result { + prove_with_options_and_inputs( elf_bytes, - private_input, + private_inputs, &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), &MaxRowsConfig::default(), ) @@ -511,7 +495,17 @@ pub fn prove_with_input(elf_bytes: &[u8], private_input: Vec) -> Result, + proof_options: &ProofOptions, + max_rows: &MaxRowsConfig, +) -> Result { + prove_with_options_and_inputs(elf_bytes, &[], proof_options, max_rows) +} + +/// Prove an ELF binary execution with custom proof options, max rows config, +/// and explicit private inputs. +pub fn prove_with_options_and_inputs( + elf_bytes: &[u8], + private_inputs: &[u8], proof_options: &ProofOptions, max_rows: &MaxRowsConfig, ) -> Result { @@ -523,12 +517,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}")))?; - // 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 executor = Executor::new(&program, private_inputs.to_vec()) + .map_err(|e| Error::Execution(format!("{e}")))?; let result = executor .run() .map_err(|e| Error::Execution(format!("{e}")))?; @@ -542,8 +532,7 @@ 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, &saved_private_input)?; + let mut traces = Traces::from_elf_and_logs(&program, &result.logs, max_rows, private_inputs)?; #[cfg(feature = "instruments")] let trace_build_elapsed = phase_start.elapsed(); @@ -588,7 +577,6 @@ pub fn prove_with_options( runtime_page_ranges, table_counts, public_output: traces.public_output_bytes.clone(), - private_input: saved_private_input, }) } @@ -602,6 +590,7 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result { vm_proof, elf_bytes, &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), + &[], ) } @@ -614,17 +603,15 @@ pub fn verify_with_options( vm_proof: &VmProof, elf_bytes: &[u8], proof_options: &ProofOptions, + private_inputs: &[u8], ) -> Result { // Validate table_counts before constructing AIRs. // A malicious prover could set counts to 0, removing entire constraint sets. 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, private_inputs); // Cross-check: table_counts must match the number of sub-proofs. // Fixed tables (bitwise, decode, halt, commit, register) = 5, plus page tables. @@ -668,6 +655,23 @@ pub fn verify_with_options( )) } +/// Verify a proof that was produced with private inputs. +/// +/// The verifier needs the private inputs to reconstruct the correct page +/// configs (private input bytes have non-zero init values in PAGE tables). +pub fn verify_with_inputs( + vm_proof: &VmProof, + elf_bytes: &[u8], + private_inputs: &[u8], +) -> Result { + verify_with_options( + vm_proof, + elf_bytes, + &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), + private_inputs, + ) +} + /// Prove and verify in one call (convenience). pub fn prove_and_verify(elf_bytes: &[u8]) -> Result { let vm_proof = prove(elf_bytes)?; diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 92f0f70b2..e8e9903c6 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -98,11 +98,6 @@ impl MemoryState { } /// 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 (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; @@ -1341,20 +1336,17 @@ 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; 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); @@ -1370,15 +1362,9 @@ fn build_init_page_data(elf: &Elf, private_input: &[u8]) -> HashMap } } } - - // Private input: length prefix at 0xFF000000, data at 0xFF000004+ if !private_input.is_empty() { let len_bytes = (private_input.len() as u32).to_le_bytes(); - let all_bytes: Vec = len_bytes - .iter() - .chain(private_input.iter()) - .copied() - .collect(); + let all_bytes: 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); @@ -1389,22 +1375,10 @@ fn build_init_page_data(elf: &Elf, private_input: &[u8]) -> HashMap page_data[offset] = b; } } - 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, - private_input: &[u8], -) -> Vec { +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; @@ -1432,7 +1406,7 @@ fn collect_bitwise_from_page( for offset in 0..page_size { let addr = page_base + offset as u64; - // Get init value (from ELF/private-input or 0) + // Get init value (from ELF or 0) let init = init_data.map_or(0u8, |data| data[offset]); // Get fini value (from final_state or init if never accessed) @@ -1569,7 +1543,8 @@ fn generate_page_tables( let page_size = page::DEFAULT_PAGE_SIZE; - let elf_page_data = build_init_page_data(elf, private_input); + // Collect init data from ELF segments + private input region + let init_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(); @@ -1589,7 +1564,7 @@ fn generate_page_tables( let mut page_configs = Vec::new(); for &page_base in &page_bases { - let config = if let Some(init_data) = elf_page_data.get(&page_base) { + let config = if let Some(init_data) = init_page_data.get(&page_base) { PageConfig::with_data(page_base, page_size, init_data.clone()) } else { PageConfig::zero_init(page_base, page_size) @@ -1811,9 +1786,9 @@ fn build_traces( entry_point: u64, decode_trace: TraceTable, decode_pc_to_row: HashMap, - private_input: &[u8], register_state: RegisterState, max_rows: &super::MaxRowsConfig, + private_input: &[u8], ) -> Result { let CollectedOps { cpu_ops, @@ -1994,28 +1969,37 @@ impl Traces { } } - /// Extract page configurations for the deterministic portion of memory: - /// ELF segments and the private-input region at `PRIVATE_INPUT_START_INDEX`. + /// Extract page configurations from ELF only (deterministic from binary). + /// + /// 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, private_input: &[u8]) -> Vec { + use std::collections::BTreeSet; + let page_size = page::DEFAULT_PAGE_SIZE; let init_page_data = build_init_page_data(elf, private_input); - let mut bases: Vec = init_page_data.keys().copied().collect(); - bases.sort(); - bases + let page_bases: BTreeSet = init_page_data.keys().copied().collect(); + + page_bases .into_iter() .map(|base| { - let data = init_page_data[&base].clone(); - PageConfig::with_data(base, page_size, data) + if let Some(init_data) = init_page_data.get(&base) { + PageConfig::with_data(base, page_size, init_data.clone()) + } else { + PageConfig::zero_init(base, page_size) + } }) .collect() } - /// Reconstruct page configs from ELF, private input, and runtime page ranges. + /// Reconstruct page configs from ELF and runtime page ranges. /// /// Used by the verifier to reconstruct the full PAGE table layout. - /// Combines deterministic ELF + private-input pages with prover-provided runtime + /// Combines deterministic ELF 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], @@ -2128,9 +2112,9 @@ impl Traces { elf.entry_point, decode_trace, decode_pc_to_row, - private_input, register_state, max_rows, + private_input, ) } @@ -2177,9 +2161,9 @@ impl Traces { entry_point, decode_trace, decode_pc_to_row, - &[], register_state, max_rows, + &[], ) } diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 971e89bf1..a20f99ae1 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(); @@ -804,7 +804,7 @@ fn test_verify_rejects_tampered_public_output() { let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &Default::default()) .expect("Prover should succeed for test_commit_4"); assert!( - crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) + crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, &[]) .expect("Valid commit proof should verify"), "Baseline proof should verify before tampering" ); @@ -816,7 +816,7 @@ fn test_verify_rejects_tampered_public_output() { ..vm_proof }; - let verified = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options) + let verified = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]) .expect("Verifier should not error on tampered public output"); assert!( !verified, @@ -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(); @@ -1700,7 +1700,7 @@ fn test_verify_rejects_zero_table_counts() { .expect("Prover should succeed on valid program"); assert!( - crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) + crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, &[]) .expect("Verification should not error on valid proof"), "Valid proof should verify" ); @@ -1721,7 +1721,7 @@ fn test_verify_rejects_zero_table_counts() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]); assert!(result.is_err(), "Got {:?}", result); } @@ -1742,7 +1742,7 @@ fn test_verify_rejects_zero_cpu_count() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]); assert!(result.is_err(), "Got {:?}", result); } @@ -1763,7 +1763,7 @@ fn test_verify_rejects_zero_memw_count() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]); assert!(result.is_err(), "Got {:?}", result); } @@ -1838,7 +1838,7 @@ fn test_small_max_rows_splits_tables() { vm_proof.table_counts.cpu ); - let verified = crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) + let verified = crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, &[]) .expect("Verifier should not error"); assert!(verified, "Proof with small max_rows should verify"); } @@ -1891,7 +1891,7 @@ fn test_verify_rejects_inflated_table_counts() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]); assert!( result.is_err(), "Inflated table_counts should be rejected, got {:?}", @@ -1970,7 +1970,7 @@ fn test_prove_private_input_xpage() { let proof = crate::prove_with_inputs(&elf_bytes, &input) .expect("prove_with_inputs should succeed"); assert!( - crate::verify(&proof, &elf_bytes).expect("verify should not error"), + crate::verify_with_inputs(&proof, &elf_bytes, &input).expect("verify should not error"), "proof should verify" ); assert_eq!(proof.public_output, input[4..12].to_vec()); @@ -1986,7 +1986,7 @@ fn test_prove_private_input_different_values() { ]; let proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove"); assert!( - crate::verify(&proof, &elf_bytes).expect("verify"), + crate::verify_with_inputs(&proof, &elf_bytes, &input).expect("verify"), "proof should verify" ); assert_eq!(proof.public_output, input[4..12].to_vec()); @@ -2002,17 +2002,17 @@ fn test_prove_commit_sum() { let elf_bytes = std::fs::read(workspace_root.join("executor/program_artifacts/rust/commit_sum.elf")) .expect("commit_sum.elf not found — run `make compile-programs-rust`"); - let proof = - crate::prove_with_inputs(&elf_bytes, &[3u8, 5u8]).expect("prove should succeed"); + let input = &[3u8, 5u8]; + let proof = crate::prove_with_inputs(&elf_bytes, input).expect("prove should succeed"); assert!( - crate::verify(&proof, &elf_bytes).expect("verify should not error"), + crate::verify_with_inputs(&proof, &elf_bytes, input).expect("verify should not error"), "commit_sum should verify" ); assert_eq!(proof.public_output, vec![8u8]); } #[test] -#[ignore = "Requires ~128GB RAM: proves an empty Ethereum block"] +#[ignore = "takes too long"] 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")) @@ -2026,7 +2026,7 @@ fn test_prove_ethrex_empty_block() { std::fs::read(workspace_root.join("executor/tests/ethrex_empty_block.bin")).unwrap(); let proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove"); assert!( - crate::verify(&proof, &elf_bytes).expect("verify"), + crate::verify_with_inputs(&proof, &elf_bytes, &input).expect("verify"), "ethrex empty block should verify" ); assert_eq!(proof.public_output.len(), 160); From 13a33efd95d46e20811f5b61f3fe4c6ae08bc7cc Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 11:58:55 -0300 Subject: [PATCH 04/19] change page size --- prover/src/tables/page.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index 8d3c9c6b9..4d29e91de 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -48,8 +48,8 @@ use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; // Constants // ========================================================================= -/// Default page size in bytes (4KB). -pub const DEFAULT_PAGE_SIZE: usize = 4096; +/// Default page size in bytes (256KB). +pub const DEFAULT_PAGE_SIZE: usize = 1 << 18; /// Stack top address (where SP starts). Re-exported from executor. pub use executor::vm::registers::STACK_TOP; From 852caef5f77be6ad0a1f3d3e98dcd401aa320f82 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 12:04:47 -0300 Subject: [PATCH 05/19] fmt --- prover/src/lib.rs | 7 ++++-- prover/src/tables/trace_builder.rs | 12 ++++++++-- prover/src/tests/prove_elfs_tests.rs | 33 +++++++++++++++++----------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 8cf6becd0..60876309c 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -610,8 +610,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, private_inputs); + let page_configs = Traces::page_configs_from_elf_and_runtime( + &program, + &vm_proof.runtime_page_ranges, + private_inputs, + ); // 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 e8e9903c6..e54bc10e1 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1364,7 +1364,11 @@ fn build_init_page_data(elf: &Elf, private_input: &[u8]) -> HashMap } if !private_input.is_empty() { let len_bytes = (private_input.len() as u32).to_le_bytes(); - let all_bytes: Vec = len_bytes.iter().chain(private_input.iter()).copied().collect(); + let all_bytes: 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); @@ -1378,7 +1382,11 @@ fn build_init_page_data(elf: &Elf, private_input: &[u8]) -> HashMap init_page_data } -fn collect_bitwise_from_page(elf: &Elf, memory_state: &MemoryState, private_input: &[u8]) -> Vec { +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; diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index a20f99ae1..71dfbe384 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(); @@ -1477,7 +1479,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 +1502,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 +1540,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 +1619,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 +1646,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(); @@ -1954,8 +1961,8 @@ fn test_pure_commit_rust() { #[test] fn test_prove_with_input_empty() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); - let result = crate::prove_with_inputs(&elf_bytes, &[]) - .expect("prove_with_inputs should succeed on sub"); + let result = + crate::prove_with_inputs(&elf_bytes, &[]).expect("prove_with_inputs should succeed on sub"); assert!( crate::verify(&result, &elf_bytes).expect("verify should not error"), "prove_with_inputs(empty) proof should verify" @@ -1967,8 +1974,8 @@ fn test_prove_with_input_empty() { fn test_prove_private_input_xpage() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); - let proof = crate::prove_with_inputs(&elf_bytes, &input) - .expect("prove_with_inputs should succeed"); + let proof = + crate::prove_with_inputs(&elf_bytes, &input).expect("prove_with_inputs should succeed"); assert!( crate::verify_with_inputs(&proof, &elf_bytes, &input).expect("verify should not error"), "proof should verify" @@ -1981,8 +1988,8 @@ 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_inputs(&elf_bytes, &input).expect("prove"); assert!( From 018ca303e7e6d69f8ff64430680242964189f82e Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 12:45:08 -0300 Subject: [PATCH 06/19] fix_tests --- executor/src/vm/memory.rs | 3 +++ prover/src/tables/trace_builder.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/executor/src/vm/memory.rs b/executor/src/vm/memory.rs index f8abc32b6..cda69ff99 100644 --- a/executor/src/vm/memory.rs +++ b/executor/src/vm/memory.rs @@ -157,6 +157,9 @@ impl Memory { /// 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.is_empty() { + return Ok(()); + } if inputs.len() as u64 > MAX_PRIVATE_INPUT_SIZE { return Err(MemoryError::PrivateInputSizeExceeded); } diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index e54bc10e1..210663338 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -99,6 +99,9 @@ impl MemoryState { /// Pre-populate the private input memory region at `PRIVATE_INPUT_START_INDEX`. fn add_private_input(&mut self, private_input: &[u8]) { + if private_input.is_empty() { + return; + } use executor::vm::memory::PRIVATE_INPUT_START_INDEX; let start = PRIVATE_INPUT_START_INDEX; let len_bytes = (private_input.len() as u32).to_le_bytes(); From 7027205336be4cf5c096ab8ff5a07256897a5e70 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 13:07:18 -0300 Subject: [PATCH 07/19] fix --- prover/src/tests/prove_elfs_tests.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 71dfbe384..f73c770f9 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1585,20 +1585,21 @@ fn test_deep_stack_missing_pages_rejected() { // Heap allocation tests (runtime page detection) // ============================================================================= -/// heap_alloc writes to 4 pages (0x80000..0x83000) far from ELF segments and +/// heap_alloc writes to addresses 0x80000..0x83000 far from ELF segments and /// stack, plus a stack write. Tests that MemoryState-based page detection /// discovers all heap and stack pages, and run-length encodes them. +/// With 256KB pages, all 4 writes (0x80000..0x83000) fit in a single page. #[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(); - // Verify runtime_page_ranges encodes the heap pages as a contiguous range + // Verify runtime_page_ranges includes the heap page let ranges = traces.runtime_page_ranges(); - // 4 contiguous heap pages (0x80000..0x83000) should be one range + // With 256KB pages, all 4 writes land on one page containing 0x80000 assert!( - ranges.iter().any(|r| r.base == 0x80000 && r.count == 4), - "Expected contiguous heap range (0x80000, 4), got {:?}", + ranges.iter().any(|r| r.base == 0x80000 && r.count == 1), + "Expected heap range (0x80000, 1), got {:?}", ranges ); @@ -1625,11 +1626,11 @@ fn test_heap_alloc_runtime_pages_roundtrip() { let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); - // Should have a range covering heap pages 0x80000..0x83000 + // With 256KB pages, all heap writes fit in 1 page + 1 stack page let total_pages: u64 = runtime_page_ranges.iter().map(|r| r.count).sum(); assert!( - total_pages >= 5, - "Expected at least 5 runtime pages (4 heap + 1 stack), got {}", + total_pages >= 2, + "Expected at least 2 runtime pages (1 heap + 1 stack), got {}", total_pages ); From 0ab091143805d0042310602268edf1bcc063f828 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 15:05:29 -0300 Subject: [PATCH 08/19] fix_merge --- prover/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 06dd56ff9..1087da790 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -507,7 +507,7 @@ pub fn count_elements(elf_bytes: &[u8], private_inputs: &[u8]) -> Result<(u64, u let result = executor .run() .map_err(|e| Error::Execution(format!("{e}")))?; - let traces = Traces::from_elf_and_logs(&program, &result.logs, &MaxRowsConfig::default())?; + let traces = Traces::from_elf_and_logs(&program, &result.logs, &MaxRowsConfig::default(), private_inputs)?; Ok(( traces.total_field_elements(), traces.total_auxiliary_field_elements(), From 96de63bfabf754044d2a5a6d5b383fab4d8c342a Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 15:18:18 -0300 Subject: [PATCH 09/19] fmt --- prover/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 1087da790..aa4f10d47 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -507,7 +507,12 @@ pub fn count_elements(elf_bytes: &[u8], private_inputs: &[u8]) -> Result<(u64, u let result = executor .run() .map_err(|e| Error::Execution(format!("{e}")))?; - let traces = Traces::from_elf_and_logs(&program, &result.logs, &MaxRowsConfig::default(), private_inputs)?; + let traces = Traces::from_elf_and_logs( + &program, + &result.logs, + &MaxRowsConfig::default(), + private_inputs, + )?; Ok(( traces.total_field_elements(), traces.total_auxiliary_field_elements(), From dcf79295d878629dca3b5809970cfa0af8a67ee9 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 16:02:53 -0300 Subject: [PATCH 10/19] fix_comments --- executor/programs/rust/commit/src/main.rs | 2 +- executor/programs/rust/commit_sum/src/main.rs | 2 +- executor/programs/rust/ethrex/src/main.rs | 2 +- executor/programs/rust/memory/src/main.rs | 2 +- executor/src/vm/memory.rs | 1 + prover/src/lib.rs | 14 ++++++++++ prover/src/tables/trace_builder.rs | 26 ++++++++++--------- syscalls/src/syscalls.rs | 17 +++++------- 8 files changed, 40 insertions(+), 26 deletions(-) diff --git a/executor/programs/rust/commit/src/main.rs b/executor/programs/rust/commit/src/main.rs index 47a0b1ee3..bbf79f54a 100644 --- a/executor/programs/rust/commit/src/main.rs +++ b/executor/programs/rust/commit/src/main.rs @@ -1,7 +1,7 @@ use lambda_vm_syscalls as syscalls; pub fn main() { - let input: Vec = syscalls::syscalls::get_private_input().unwrap(); + let input: Vec = syscalls::syscalls::get_private_input(); syscalls::syscalls::print_string(format!("Private input received: {:?}\n", input).as_str()); syscalls::syscalls::commit(&input); } diff --git a/executor/programs/rust/commit_sum/src/main.rs b/executor/programs/rust/commit_sum/src/main.rs index 57d455b9b..6eb84bc83 100644 --- a/executor/programs/rust/commit_sum/src/main.rs +++ b/executor/programs/rust/commit_sum/src/main.rs @@ -1,7 +1,7 @@ use lambda_vm_syscalls as syscalls; pub fn main() { - let input: Vec = syscalls::syscalls::get_private_input().unwrap(); + let input: Vec = syscalls::syscalls::get_private_input(); let a = input[0]; let b = input[1]; syscalls::syscalls::commit((a + b).to_le_bytes().as_ref()); diff --git a/executor/programs/rust/ethrex/src/main.rs b/executor/programs/rust/ethrex/src/main.rs index 31671b735..4f608ef9e 100644 --- a/executor/programs/rust/ethrex/src/main.rs +++ b/executor/programs/rust/ethrex/src/main.rs @@ -2,7 +2,7 @@ use guest_program::{execution::execution_program, input::ProgramInput}; use rkyv::rancor::Error; use lambda_vm_syscalls as syscalls; pub fn main() { - let input = syscalls::syscalls::get_private_input().unwrap(); + let input = syscalls::syscalls::get_private_input(); let input = rkyv::from_bytes::(&input).unwrap(); let output = execution_program(input).unwrap(); let output_bytes = output.encode(); diff --git a/executor/programs/rust/memory/src/main.rs b/executor/programs/rust/memory/src/main.rs index 5fbfe93cb..416051e6d 100644 --- a/executor/programs/rust/memory/src/main.rs +++ b/executor/programs/rust/memory/src/main.rs @@ -1,7 +1,7 @@ use lambda_vm_syscalls as syscalls; pub fn main() { - let input = syscalls::syscalls::get_private_input().unwrap(); + let input = syscalls::syscalls::get_private_input(); let size = u32::from_be_bytes(input.try_into().unwrap()); let mut vector = vec![]; for i in 0..size { diff --git a/executor/src/vm/memory.rs b/executor/src/vm/memory.rs index cda69ff99..b1f047ee1 100644 --- a/executor/src/vm/memory.rs +++ b/executor/src/vm/memory.rs @@ -46,6 +46,7 @@ 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. +/// Must match `PRIVATE_INPUT_START` in `syscalls/src/syscalls.rs`. pub const PRIVATE_INPUT_START_INDEX: u64 = 0xFF000000; #[derive(Default, Debug)] diff --git a/prover/src/lib.rs b/prover/src/lib.rs index aa4f10d47..cfb00258b 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -138,6 +138,9 @@ pub struct VmProof { pub table_counts: TableCounts, /// Committed public output bytes. pub public_output: Vec, + /// Whether the proof was generated with non-empty private inputs. + /// Callers must use `verify_with_inputs` instead of `verify` for such proofs. + pub uses_private_input: bool, } /// Error type for the prover crate. @@ -155,6 +158,9 @@ pub enum Error { Prover(String), /// Proof contains invalid table_counts (e.g. zero for a required table) InvalidTableCounts(String), + /// Proof requires private inputs but `verify()` was called without them. + /// Use `verify_with_inputs()` instead. + MissingPrivateInput, } impl fmt::Display for Error { @@ -168,6 +174,10 @@ impl fmt::Display for Error { Error::Execution(msg) => write!(f, "execution error: {msg}"), Error::Prover(msg) => write!(f, "proving error: {msg}"), Error::InvalidTableCounts(msg) => write!(f, "invalid table_counts: {msg}"), + Error::MissingPrivateInput => write!( + f, + "proof requires private inputs — use verify_with_inputs() instead of verify()" + ), } } } @@ -618,6 +628,7 @@ pub fn prove_with_options_and_inputs( runtime_page_ranges, table_counts, public_output: traces.public_output_bytes.clone(), + uses_private_input: !private_inputs.is_empty(), }) } @@ -627,6 +638,9 @@ pub fn prove_with_options_and_inputs( /// `runtime_page_ranges` from the proof are hints — preprocessed commitments /// bind the verifier to the correct page layout. pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result { + if vm_proof.uses_private_input { + return Err(Error::MissingPrivateInput); + } verify_with_options( vm_proof, elf_bytes, diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 4fd8cc004..9727b8775 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -104,13 +104,9 @@ impl MemoryState { } use executor::vm::memory::PRIVATE_INPUT_START_INDEX; let start = PRIVATE_INPUT_START_INDEX; - let len_bytes = (private_input.len() as u32).to_le_bytes(); - for (i, &b) in len_bytes.iter().enumerate() { + for (i, &b) in private_input_bytes(private_input).iter().enumerate() { self.cells.insert(start + i as u64, (b, 0)); } - 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. @@ -1346,6 +1342,18 @@ fn collect_byte_check_ops_for_padding(num_padding_rows: usize) -> Vec Vec { + let len_bytes = (private_input.len() as u32).to_le_bytes(); + len_bytes + .iter() + .chain(private_input.iter()) + .copied() + .collect() +} + 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; @@ -1366,13 +1374,7 @@ fn build_init_page_data(elf: &Elf, private_input: &[u8]) -> HashMap } } if !private_input.is_empty() { - let len_bytes = (private_input.len() as u32).to_le_bytes(); - let all_bytes: Vec = len_bytes - .iter() - .chain(private_input.iter()) - .copied() - .collect(); - for (i, &b) in all_bytes.iter().enumerate() { + for (i, &b) in private_input_bytes(private_input).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); diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index fb7426401..ae0315ff5 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -4,6 +4,7 @@ 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). +/// Must match `executor::vm::memory::PRIVATE_INPUT_START_INDEX`. #[cfg(target_arch = "riscv64")] const PRIVATE_INPUT_START: usize = 0xFF000000; @@ -84,26 +85,22 @@ pub fn commit(slice: &[u8]) { /// 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> { - // Read length prefix (4 bytes LE at PRIVATE_INPUT_START) +pub fn get_private_input() -> Vec { + // SAFETY: The host pre-loads private input at PRIVATE_INPUT_START before + // execution. The 4-byte LE length prefix is always valid (written by the + // executor). The data pointer and length are within the memory-mapped region. 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()) + slice.to_vec() } #[cfg(not(target_arch = "riscv64"))] -pub fn get_private_input() -> Result, SyscallError> { +pub fn get_private_input() -> Vec { unimplemented!("syscalls are only implemented for riscv64 targets"); } -#[derive(Debug)] -pub enum SyscallError { - WrongPrivateInputSize, -} - #[cfg(target_arch = "riscv64")] pub fn sys_halt() -> ! { // NOTE: no print_string here — the Print ecall is unmatched on the Ecall bus From d3326af262e75480546df0e866e6f51d0c857606 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 17:44:21 -0300 Subject: [PATCH 11/19] change verifier privete_inputs handle --- bin/cli/src/main.rs | 2 +- executor/programs/rust/pure_commit/Cargo.lock | 7 +++ prover/src/lib.rs | 63 ++++++++----------- prover/src/tables/page.rs | 23 ++++++- prover/src/tables/trace_builder.rs | 53 +++++++++++++--- prover/src/tests/prove_elfs_tests.rs | 32 +++++----- 6 files changed, 115 insertions(+), 65 deletions(-) create mode 100644 executor/programs/rust/pure_commit/Cargo.lock diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 14c127c1d..aa633ef9a 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -499,7 +499,7 @@ fn cmd_verify(proof_path: PathBuf, elf_path: PathBuf, blowup: Option, time: return ExitCode::FAILURE; } }; - prover::verify_with_options(&proof, &elf_data, &opts, &[]) + prover::verify_with_options(&proof, &elf_data, &opts) } None => prover::verify(&proof, &elf_data), }; 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/prover/src/lib.rs b/prover/src/lib.rs index cfb00258b..943963ee3 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -138,9 +138,10 @@ pub struct VmProof { pub table_counts: TableCounts, /// Committed public output bytes. pub public_output: Vec, - /// Whether the proof was generated with non-empty private inputs. - /// Callers must use `verify_with_inputs` instead of `verify` for such proofs. - pub uses_private_input: bool, + /// Number of PAGE tables that hold private input data. + /// These pages are NOT preprocessed — the verifier reconstructs them + /// as non-preprocessed tables starting at `PRIVATE_INPUT_START_INDEX`. + pub num_private_input_pages: usize, } /// Error type for the prover crate. @@ -158,9 +159,6 @@ pub enum Error { Prover(String), /// Proof contains invalid table_counts (e.g. zero for a required table) InvalidTableCounts(String), - /// Proof requires private inputs but `verify()` was called without them. - /// Use `verify_with_inputs()` instead. - MissingPrivateInput, } impl fmt::Display for Error { @@ -174,10 +172,6 @@ impl fmt::Display for Error { Error::Execution(msg) => write!(f, "execution error: {msg}"), Error::Prover(msg) => write!(f, "proving error: {msg}"), Error::InvalidTableCounts(msg) => write!(f, "invalid table_counts: {msg}"), - Error::MissingPrivateInput => write!( - f, - "proof requires private inputs — use verify_with_inputs() instead of verify()" - ), } } } @@ -376,10 +370,19 @@ impl VmAirs { let pages: Vec<_> = page_configs .iter() .map(|config| { - create_page_air(proof_options, config.page_base).with_preprocessed( - page::precomputed_commitment_cached(config, proof_options), - page::NUM_PREPROCESSED_COLS, - ) + if config.is_private_input { + // Private-input pages: all columns are main trace (not preprocessed). + // The verifier doesn't see the init values; correctness is enforced + // by the memory bus constraints. + create_page_air(proof_options, config.page_base) + } else { + // ELF and zero-init pages: OFFSET + INIT are preprocessed. + // The verifier independently recomputes the commitment from public data. + create_page_air(proof_options, config.page_base).with_preprocessed( + page::precomputed_commitment_cached(config, proof_options), + page::NUM_PREPROCESSED_COLS, + ) + } }) .collect(); let memw_registers: Vec<_> = (0..table_counts.memw_register) @@ -623,12 +626,18 @@ pub fn prove_with_options_and_inputs( ); } + let num_private_input_pages = traces + .page_configs + .iter() + .filter(|c| c.is_private_input) + .count(); + Ok(VmProof { proof, runtime_page_ranges, table_counts, public_output: traces.public_output_bytes.clone(), - uses_private_input: !private_inputs.is_empty(), + num_private_input_pages, }) } @@ -638,14 +647,10 @@ pub fn prove_with_options_and_inputs( /// `runtime_page_ranges` from the proof are hints — preprocessed commitments /// bind the verifier to the correct page layout. pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result { - if vm_proof.uses_private_input { - return Err(Error::MissingPrivateInput); - } verify_with_options( vm_proof, elf_bytes, &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), - &[], ) } @@ -658,7 +663,6 @@ pub fn verify_with_options( vm_proof: &VmProof, elf_bytes: &[u8], proof_options: &ProofOptions, - private_inputs: &[u8], ) -> Result { // Validate table_counts before constructing AIRs. // A malicious prover could set counts to 0, removing entire constraint sets. @@ -668,7 +672,7 @@ pub fn verify_with_options( let page_configs = Traces::page_configs_from_elf_and_runtime( &program, &vm_proof.runtime_page_ranges, - private_inputs, + vm_proof.num_private_input_pages, ); // Cross-check: table_counts must match the number of sub-proofs. @@ -713,23 +717,6 @@ pub fn verify_with_options( )) } -/// Verify a proof that was produced with private inputs. -/// -/// The verifier needs the private inputs to reconstruct the correct page -/// configs (private input bytes have non-zero init values in PAGE tables). -pub fn verify_with_inputs( - vm_proof: &VmProof, - elf_bytes: &[u8], - private_inputs: &[u8], -) -> Result { - verify_with_options( - vm_proof, - elf_bytes, - &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), - private_inputs, - ) -} - /// Prove and verify in one call (convenience). pub fn prove_and_verify(elf_bytes: &[u8]) -> Result { let vm_proof = prove(elf_bytes)?; diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index 9f695c23f..a4597e1b8 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -111,6 +111,11 @@ pub struct PageConfig { /// Initial values for each byte in the page. /// If None, all bytes are zero-initialized. pub init_values: Option>, + /// Whether this page holds private input data. + /// Private-input pages are NOT preprocessed — the verifier does not see + /// the init values. Instead, all columns (including OFFSET and INIT) + /// are committed as main trace and constrained via the memory bus. + pub is_private_input: bool, } impl PageConfig { @@ -120,10 +125,11 @@ impl PageConfig { page_base, page_size, init_values: None, + is_private_input: false, } } - /// Create a page with initial values from data. + /// Create a page with initial values from ELF data. pub fn with_data(page_base: u64, page_size: usize, data: Vec) -> Self { assert!(data.len() <= page_size, "Data exceeds page size"); let mut init_values = data; @@ -132,6 +138,21 @@ impl PageConfig { page_base, page_size, init_values: Some(init_values), + is_private_input: false, + } + } + + /// Create a page with initial values from private input data. + /// These pages are NOT preprocessed — the verifier never sees the init values. + pub fn with_private_input(page_base: u64, page_size: usize, data: Vec) -> Self { + assert!(data.len() <= page_size, "Data exceeds page size"); + let mut init_values = data; + init_values.resize(page_size, 0); + Self { + page_base, + page_size, + init_values: Some(init_values), + is_private_input: true, } } } diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 1f0ee756e..4e4bac7d0 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1584,8 +1584,24 @@ fn generate_page_tables( let mut pages = Vec::new(); let mut page_configs = Vec::new(); + // Determine which page bases hold private input data. + let private_input_page_bases: std::collections::BTreeSet = if !private_input.is_empty() { + use executor::vm::memory::PRIVATE_INPUT_START_INDEX; + let total_bytes = 4 + private_input.len(); // length prefix + data + (0..total_bytes) + .map(|i| { + page::page_base_for_address(PRIVATE_INPUT_START_INDEX + i as u64, page_size) + }) + .collect() + } else { + std::collections::BTreeSet::new() + }; + for &page_base in &page_bases { - let config = if let Some(init_data) = init_page_data.get(&page_base) { + let config = if private_input_page_bases.contains(&page_base) { + let init_data = init_page_data.get(&page_base).cloned().unwrap_or_default(); + PageConfig::with_private_input(page_base, page_size, init_data) + } else if let Some(init_data) = init_page_data.get(&page_base) { PageConfig::with_data(page_base, page_size, init_data.clone()) } else { PageConfig::zero_init(page_base, page_size) @@ -2180,11 +2196,11 @@ 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, private_input: &[u8]) -> Vec { + pub fn page_configs_from_elf(elf: &Elf) -> Vec { use std::collections::BTreeSet; let page_size = page::DEFAULT_PAGE_SIZE; - let init_page_data = build_init_page_data(elf, private_input); + let init_page_data = build_init_page_data(elf, &[]); let page_bases: BTreeSet = init_page_data.keys().copied().collect(); @@ -2200,19 +2216,22 @@ impl Traces { .collect() } - /// Reconstruct page configs from ELF and runtime page ranges. + /// Reconstruct page configs from ELF, runtime page ranges, and private-input page count. /// /// Used by the verifier to reconstruct the full PAGE table layout. - /// Combines deterministic ELF pages with prover-provided runtime - /// page ranges (zero-initialized: stack, heap, etc.). - /// Each range is `(base, count)` — `count` contiguous 4KB pages from `base`. + /// Combines: + /// - Deterministic ELF pages (preprocessed, init from binary) + /// - Runtime pages from prover hints (preprocessed, zero-init) + /// - Private-input pages (NOT preprocessed, verifier doesn't see init values) pub fn page_configs_from_elf_and_runtime( elf: &Elf, runtime_page_ranges: &[crate::RuntimePageRange], - private_input: &[u8], + num_private_input_pages: usize, ) -> Vec { - let mut configs = Self::page_configs_from_elf(elf, private_input); + let mut configs = Self::page_configs_from_elf(elf); let page_size = page::DEFAULT_PAGE_SIZE; + + // Add zero-init runtime pages (stack, heap) for r in runtime_page_ranges { let (base, count) = (r.base, r.count); for i in 0..count { @@ -2222,6 +2241,22 @@ impl Traces { )); } } + + // Add private-input pages (non-preprocessed, verifier doesn't know init values) + if num_private_input_pages > 0 { + use executor::vm::memory::PRIVATE_INPUT_START_INDEX; + let first_page_base = + page::page_base_for_address(PRIVATE_INPUT_START_INDEX, page_size); + for i in 0..num_private_input_pages { + configs.push(PageConfig { + page_base: first_page_base + i as u64 * page_size as u64, + page_size, + init_values: None, // Verifier doesn't know these + is_private_input: true, + }); + } + } + configs.sort_by_key(|c| c.page_base); configs } diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 290618950..125f7ee36 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -776,7 +776,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, &[], 0); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &wrong_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -806,7 +806,7 @@ fn test_verify_rejects_tampered_public_output() { let vm_proof = crate::prove_with_options(&elf_bytes, &proof_options, &Default::default()) .expect("Prover should succeed for test_commit_4"); assert!( - crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, &[]) + crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) .expect("Valid commit proof should verify"), "Baseline proof should verify before tampering" ); @@ -818,7 +818,7 @@ fn test_verify_rejects_tampered_public_output() { ..vm_proof }; - let verified = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]) + let verified = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options) .expect("Verifier should not error on tampered public output"); assert!( !verified, @@ -1504,7 +1504,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, &[]); + Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, 0); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1559,7 +1559,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, &[], 0); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &wrong_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1649,7 +1649,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, &[]); + Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, 0); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1709,7 +1709,7 @@ fn test_verify_rejects_zero_table_counts() { .expect("Prover should succeed on valid program"); assert!( - crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, &[]) + crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) .expect("Verification should not error on valid proof"), "Valid proof should verify" ); @@ -1730,7 +1730,7 @@ fn test_verify_rejects_zero_table_counts() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); assert!(result.is_err(), "Got {:?}", result); } @@ -1751,7 +1751,7 @@ fn test_verify_rejects_zero_cpu_count() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); assert!(result.is_err(), "Got {:?}", result); } @@ -1772,7 +1772,7 @@ fn test_verify_rejects_zero_memw_count() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); assert!(result.is_err(), "Got {:?}", result); } @@ -1847,7 +1847,7 @@ fn test_small_max_rows_splits_tables() { vm_proof.table_counts.cpu ); - let verified = crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options, &[]) + let verified = crate::verify_with_options(&vm_proof, &elf_bytes, &proof_options) .expect("Verifier should not error"); assert!(verified, "Proof with small max_rows should verify"); } @@ -1900,7 +1900,7 @@ fn test_verify_rejects_inflated_table_counts() { ..vm_proof }; - let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options, &[]); + let result = crate::verify_with_options(&tampered_proof, &elf_bytes, &proof_options); assert!( result.is_err(), "Inflated table_counts should be rejected, got {:?}", @@ -1979,7 +1979,7 @@ fn test_prove_private_input_xpage() { let proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove_with_inputs should succeed"); assert!( - crate::verify_with_inputs(&proof, &elf_bytes, &input).expect("verify should not error"), + crate::verify(&proof, &elf_bytes).expect("verify should not error"), "proof should verify" ); assert_eq!(proof.public_output, input[4..12].to_vec()); @@ -1995,7 +1995,7 @@ fn test_prove_private_input_different_values() { ]; let proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove"); assert!( - crate::verify_with_inputs(&proof, &elf_bytes, &input).expect("verify"), + crate::verify(&proof, &elf_bytes).expect("verify"), "proof should verify" ); assert_eq!(proof.public_output, input[4..12].to_vec()); @@ -2014,7 +2014,7 @@ fn test_prove_commit_sum() { let input = &[3u8, 5u8]; let proof = crate::prove_with_inputs(&elf_bytes, input).expect("prove should succeed"); assert!( - crate::verify_with_inputs(&proof, &elf_bytes, input).expect("verify should not error"), + crate::verify(&proof, &elf_bytes).expect("verify should not error"), "commit_sum should verify" ); assert_eq!(proof.public_output, vec![8u8]); @@ -2035,7 +2035,7 @@ fn test_prove_ethrex_empty_block() { std::fs::read(workspace_root.join("executor/tests/ethrex_empty_block.bin")).unwrap(); let proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove"); assert!( - crate::verify_with_inputs(&proof, &elf_bytes, &input).expect("verify"), + crate::verify(&proof, &elf_bytes).expect("verify"), "ethrex empty block should verify" ); assert_eq!(proof.public_output.len(), 160); From d610c84549ec1e844f43dceb91ccfa64b81cc574 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Wed, 22 Apr 2026 17:59:45 -0300 Subject: [PATCH 12/19] fmt --- prover/src/tables/trace_builder.rs | 7 ++----- prover/src/tests/prove_elfs_tests.rs | 6 ++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 4e4bac7d0..74afd5b4a 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1589,9 +1589,7 @@ fn generate_page_tables( use executor::vm::memory::PRIVATE_INPUT_START_INDEX; let total_bytes = 4 + private_input.len(); // length prefix + data (0..total_bytes) - .map(|i| { - page::page_base_for_address(PRIVATE_INPUT_START_INDEX + i as u64, page_size) - }) + .map(|i| page::page_base_for_address(PRIVATE_INPUT_START_INDEX + i as u64, page_size)) .collect() } else { std::collections::BTreeSet::new() @@ -2245,8 +2243,7 @@ impl Traces { // Add private-input pages (non-preprocessed, verifier doesn't know init values) if num_private_input_pages > 0 { use executor::vm::memory::PRIVATE_INPUT_START_INDEX; - let first_page_base = - page::page_base_for_address(PRIVATE_INPUT_START_INDEX, page_size); + let first_page_base = page::page_base_for_address(PRIVATE_INPUT_START_INDEX, page_size); for i in 0..num_private_input_pages { configs.push(PageConfig { page_base: first_page_base + i as u64 * page_size as u64, diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 125f7ee36..41a8f7d85 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1503,8 +1503,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, 0); + let verifier_configs = Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, 0); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1648,8 +1647,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, 0); + let verifier_configs = Traces::page_configs_from_elf_and_runtime(&elf, &runtime_page_ranges, 0); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &verifier_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); From 3df476e8bd0e723fa71bca9240843c7f7bb5088c Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Fri, 24 Apr 2026 10:56:36 -0300 Subject: [PATCH 13/19] add max private input size --- prover/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 943963ee3..151f52645 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -668,6 +668,21 @@ pub fn verify_with_options( // A malicious prover could set counts to 0, removing entire constraint sets. vm_proof.table_counts.validate()?; + // Bound num_private_input_pages before allocating PageConfigs. + // MAX_PRIVATE_INPUT_SIZE fits in ~26 pages of DEFAULT_PAGE_SIZE. + { + use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; + use crate::tables::page::DEFAULT_PAGE_SIZE; + let max_pages = + (MAX_PRIVATE_INPUT_SIZE as usize + 4 + DEFAULT_PAGE_SIZE - 1) / DEFAULT_PAGE_SIZE + 1; + if vm_proof.num_private_input_pages > max_pages { + return Err(Error::InvalidTableCounts(format!( + "num_private_input_pages ({}) exceeds max ({max_pages})", + vm_proof.num_private_input_pages, + ))); + } + } + let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; let page_configs = Traces::page_configs_from_elf_and_runtime( &program, From 5501b94de8ede457d99145eff9385e31966812b1 Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Fri, 24 Apr 2026 11:03:44 -0300 Subject: [PATCH 14/19] fmt --- prover/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 151f52645..cd6d23b67 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -671,8 +671,8 @@ pub fn verify_with_options( // Bound num_private_input_pages before allocating PageConfigs. // MAX_PRIVATE_INPUT_SIZE fits in ~26 pages of DEFAULT_PAGE_SIZE. { - use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; use crate::tables::page::DEFAULT_PAGE_SIZE; + use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; let max_pages = (MAX_PRIVATE_INPUT_SIZE as usize + 4 + DEFAULT_PAGE_SIZE - 1) / DEFAULT_PAGE_SIZE + 1; if vm_proof.num_private_input_pages > max_pages { From 37f664133a5e1b45c8b78a53d83f7fe85980a2db Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Fri, 24 Apr 2026 11:09:46 -0300 Subject: [PATCH 15/19] fmt --- prover/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index cd6d23b67..fb09fa01a 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -674,7 +674,7 @@ pub fn verify_with_options( use crate::tables::page::DEFAULT_PAGE_SIZE; use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; let max_pages = - (MAX_PRIVATE_INPUT_SIZE as usize + 4 + DEFAULT_PAGE_SIZE - 1) / DEFAULT_PAGE_SIZE + 1; + (MAX_PRIVATE_INPUT_SIZE as usize + 4).div_ceil(DEFAULT_PAGE_SIZE) + 1; if vm_proof.num_private_input_pages > max_pages { return Err(Error::InvalidTableCounts(format!( "num_private_input_pages ({}) exceeds max ({max_pages})", From a27148ccb1d3c4540f535d4782821dccdf7f1d0e Mon Sep 17 00:00:00 2001 From: Joaquin Carletti Date: Fri, 24 Apr 2026 11:19:07 -0300 Subject: [PATCH 16/19] fmt --- prover/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index fb09fa01a..bd42b9948 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -673,8 +673,7 @@ pub fn verify_with_options( { use crate::tables::page::DEFAULT_PAGE_SIZE; use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; - let max_pages = - (MAX_PRIVATE_INPUT_SIZE as usize + 4).div_ceil(DEFAULT_PAGE_SIZE) + 1; + let max_pages = (MAX_PRIVATE_INPUT_SIZE as usize + 4).div_ceil(DEFAULT_PAGE_SIZE) + 1; if vm_proof.num_private_input_pages > max_pages { return Err(Error::InvalidTableCounts(format!( "num_private_input_pages ({}) exceeds max ({max_pages})", From b3cdaa513c50b346124b0b76c69ecc0557f09983 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 24 Apr 2026 15:38:41 -0300 Subject: [PATCH 17/19] test: add security tests for private input tamper scenarios Verify the verifier rejects proofs with tampered num_private_input_pages (zeroed, inflated, exceeds max) and tampered public_output when private input is present. Also confirm the proof struct does not carry raw private input bytes. --- prover/src/tests/prove_elfs_tests.rs | 134 +++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 41a8f7d85..f1998aedc 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -2039,6 +2039,140 @@ fn test_prove_ethrex_empty_block() { assert_eq!(proof.public_output.len(), 160); } +// ============================================================================= +// Security: private-input tamper tests +// ============================================================================= + +/// Verifier must reject when num_private_input_pages is zeroed out. +/// The proof contains a non-preprocessed PAGE sub-proof for the private input, +/// but the verifier expects 0 such pages → proof count mismatch. +#[test] +fn test_verify_rejects_tampered_num_private_input_pages_zero() { + 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_inputs(&elf_bytes, &input).expect("prove should succeed"); + + // Baseline: untampered proof must verify. + assert!( + crate::verify(&vm_proof, &elf_bytes).expect("verify should not error"), + "Baseline proof must verify before tampering" + ); + assert!( + vm_proof.num_private_input_pages > 0, + "proof should have private pages" + ); + + // Tamper: zero out private input pages. + let tampered = crate::VmProof { + num_private_input_pages: 0, + ..vm_proof + }; + + let result = crate::verify(&tampered, &elf_bytes); + assert!( + result.is_err() || result.unwrap() == false, + "Verifier must reject proof with num_private_input_pages zeroed out" + ); +} + +/// Verifier must reject when num_private_input_pages is inflated beyond actual. +/// The proof has 1 private page but we claim 2 → proof count mismatch. +#[test] +fn test_verify_rejects_inflated_num_private_input_pages() { + 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_inputs(&elf_bytes, &input).expect("prove should succeed"); + + assert_eq!( + vm_proof.num_private_input_pages, 1, + "16 bytes fits in 1 page" + ); + + let tampered = crate::VmProof { + num_private_input_pages: 2, + ..vm_proof + }; + + let result = crate::verify(&tampered, &elf_bytes); + assert!( + result.is_err() || result.unwrap() == false, + "Verifier must reject proof with inflated num_private_input_pages" + ); +} + +/// Verifier must reject num_private_input_pages that exceeds the max bound. +/// The early bounds check should catch this before constructing AIRs. +#[test] +fn test_verify_rejects_num_private_input_pages_exceeds_max() { + 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_inputs(&elf_bytes, &input).expect("prove should succeed"); + + let tampered = crate::VmProof { + num_private_input_pages: 1000, + ..vm_proof + }; + + assert!( + crate::verify(&tampered, &elf_bytes).is_err(), + "Verifier must error on num_private_input_pages exceeding max" + ); +} + +/// Verifier must reject tampered public_output when private input is used. +/// Ensures the COMMIT bus balance check still works with non-preprocessed pages. +#[test] +fn test_verify_rejects_private_input_with_tampered_public_output() { + 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_inputs(&elf_bytes, &input).expect("prove should succeed"); + + assert!( + crate::verify(&vm_proof, &elf_bytes).expect("verify"), + "Baseline must verify" + ); + + let mut tampered_output = vm_proof.public_output.clone(); + tampered_output[0] ^= 0x01; + let tampered = crate::VmProof { + public_output: tampered_output, + ..vm_proof + }; + + let verified = + crate::verify(&tampered, &elf_bytes).expect("verify should not error on tampered output"); + assert!( + !verified, + "Verifier must reject proof with tampered public_output (private input present)" + ); +} + +/// VmProof must not contain a field that stores the raw private input bytes. +/// This is a structural check: the proof struct should only carry +/// `num_private_input_pages`, not the actual input data. +#[test] +fn test_proof_does_not_contain_private_input_field() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); + let input: Vec = vec![ + 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, + 0xF0, + ]; + let vm_proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove should succeed"); + + // The VmProof struct should only contain num_private_input_pages (a count), + // not the actual bytes. Verify the proof's public fields don't contain them. + assert_eq!(vm_proof.num_private_input_pages, 1); + // public_output is the committed output, NOT the private input. + // It should contain bytes [4..12] of the input (what the ASM program commits). + assert_eq!(vm_proof.public_output, input[4..12].to_vec()); + // No `private_input` field exists — this is enforced by the type system, + // but explicitly document that the proof carries only the page count. + assert!( + vm_proof.num_private_input_pages <= 1, + "Only the page count is stored, not the bytes" + ); +} + /// 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 2241eacc01db9afc696fab9c27ca452388be7042 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 24 Apr 2026 15:54:59 -0300 Subject: [PATCH 18/19] clippy + fmt --- prover/src/tests/prove_elfs_tests.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index f1998aedc..c2bf76667 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -2153,10 +2153,7 @@ fn test_verify_rejects_private_input_with_tampered_public_output() { #[test] fn test_proof_does_not_contain_private_input_field() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); - let input: Vec = vec![ - 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, - 0xF0, - ]; + let input: Vec = (0xA0u8..0xB0).collect(); let vm_proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove should succeed"); // The VmProof struct should only contain num_private_input_pages (a count), From e065a7d5a08afd8b5a2c0b869b0e2d33416196c0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 24 Apr 2026 16:01:17 -0300 Subject: [PATCH 19/19] clippy + fmt --- prover/src/tests/prove_elfs_tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index c2bf76667..7e0fbc181 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -2070,7 +2070,7 @@ fn test_verify_rejects_tampered_num_private_input_pages_zero() { let result = crate::verify(&tampered, &elf_bytes); assert!( - result.is_err() || result.unwrap() == false, + result.is_err() || !result.unwrap(), "Verifier must reject proof with num_private_input_pages zeroed out" ); } @@ -2095,7 +2095,7 @@ fn test_verify_rejects_inflated_num_private_input_pages() { let result = crate::verify(&tampered, &elf_bytes); assert!( - result.is_err() || result.unwrap() == false, + result.is_err() || !result.unwrap(), "Verifier must reject proof with inflated num_private_input_pages" ); }