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/programs/rust/pure_commit/.cargo/config.toml b/executor/programs/rust/pure_commit/.cargo/config.toml new file mode 100644 index 000000000..be730c3ec --- /dev/null +++ b/executor/programs/rust/pure_commit/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.riscv64im-lambda-vm-elf] +rustflags = [ + "-C", "link-arg=-e", + "-C", "link-arg=main", + "-C", "passes=lower-atomic" +] diff --git a/executor/programs/rust/pure_commit/Cargo.lock b/executor/programs/rust/pure_commit/Cargo.lock new file mode 100644 index 000000000..d365437ff --- /dev/null +++ b/executor/programs/rust/pure_commit/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "pure_commit" +version = "0.1.0" diff --git a/executor/programs/rust/pure_commit/Cargo.toml b/executor/programs/rust/pure_commit/Cargo.toml new file mode 100644 index 000000000..95fdfe36f --- /dev/null +++ b/executor/programs/rust/pure_commit/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] + +[package] +name = "pure_commit" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/executor/programs/rust/pure_commit/src/main.rs b/executor/programs/rust/pure_commit/src/main.rs new file mode 100644 index 000000000..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..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(()), @@ -66,8 +64,12 @@ impl Instruction { } } Instruction::ArithImmW { dst, src, imm, op } => { - // W-suffix: operate on lower 32 bits, sign-extend result to 64 bits - let op1 = registers.read(src)? as i32; + // W-suffix: operate on lower 32 bits, sign-extend result to 64 bits. + // Log must store the RAW register value in src1_val (full 64 bits) + // for the prover's MEMW register chain. The truncation to i32 is only + // for the ALU computation. + let raw_src = registers.read(src)?; + let op1 = raw_src as i32; if matches!(op, ArithOp::Sub) { return Err(ExecutionError::SubImmNotSupported); } @@ -77,7 +79,7 @@ impl Instruction { Log { current_pc: pc, next_pc: pc.wrapping_add(REGULAR_PC_UPDATE), - src1_val: op1 as u64, + src1_val: raw_src, src2_val: 0, dst_val: res, } @@ -247,17 +249,21 @@ impl Instruction { src2, op, } => { - // W-suffix: operate on lower 32 bits, sign-extend result to 64 bits - let a = registers.read(src1)? as i32; - let b = registers.read(src2)? as i32; + // W-suffix: operate on lower 32 bits, sign-extend result to 64 bits. + // Log must store RAW register values (full 64 bits) for the prover's + // MEMW register chain. Truncation to i32 is only for ALU computation. + let raw_src1 = registers.read(src1)?; + let raw_src2 = registers.read(src2)?; + let a = raw_src1 as i32; + let b = raw_src2 as i32; let res32 = op.apply_word(a, b)?; let res = res32 as i64 as u64; // Sign-extend to 64 bits registers.write(dst, res)?; Log { current_pc: pc, next_pc: pc.wrapping_add(REGULAR_PC_UPDATE), - src1_val: a as u64, - src2_val: b as u64, + src1_val: raw_src1, + src2_val: raw_src2, dst_val: res, } } @@ -318,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 000000000..b3551200a Binary files /dev/null and b/executor/tests/ethrex_empty_block.bin differ 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/shift.rs b/prover/src/tables/shift.rs index bb535c537..c5a9be32e 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -181,8 +181,14 @@ impl ShiftOperation { let left = !self.direction; let right = self.direction; - // is_negative = MSB of in[3] - let is_negative = (self.in_halves[3] >> 15) & 1 == 1; + // is_negative is the MSB of in[3] BUT gated by `signed`. The SHIFT + // AIR constrains IS_NEGATIVE via the MSB16 bus (SHIFT-C14) only when + // `signed = 1` — for `signed = 0` IS_NEGATIVE is free, so we set it + // to zero. This makes `extension = 65535 * is_negative = 0` for SRL, + // so the extension contribution in `compute_shifted_half` naturally + // vanishes (zero fill) — matching RISC-V SRL semantics regardless of + // the top-bit value of the input. + let is_negative = self.signed && (self.in_halves[3] >> 15) & 1 == 1; let extension: u16 = if is_negative { 0xFFFF } else { 0 }; // bit_shift @@ -642,7 +648,11 @@ impl ShiftConstraint { let left = &mu - &dir; // μ - direction let right = dir; - // extension = 65535 * is_negative + // extension = 65535 * is_negative (virtual; matches SHIFT-C7 HWSL send). + // The IS_NEGATIVE column is only constrained by MSB16 when signed=1 + // (SHIFT-C14's bus send is gated by SIGNED). For SRL (signed=0) the + // trace sets IS_NEGATIVE=0 explicitly so extension=0 → x[4]=0 → no + // spurious extension bleed into `shifted`. let is_neg = step.get_main_evaluation_element(0, cols::IS_NEGATIVE); let extension = is_neg * FieldElement::::from(65535u64); diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 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/tables/types.rs b/prover/src/tables/types.rs index 8e89490ae..bd45c3861 100644 --- a/prover/src/tables/types.rs +++ b/prover/src/tables/types.rs @@ -668,7 +668,15 @@ impl DecodeEntry { } Instruction::CSR { .. } => { - // CSR instructions not yet supported in prover + // CSR instructions are executed as no-ops by the VM (see + // `executor/src/vm/instruction/execution.rs` Instruction::CSR arm + // returning `dst_val: 0, src1/2_val: 0`). Mirror that here by + // treating them as `ADDI x0, x0, 0` — same pattern as `Fence`. + // This sets `op_add=true` so CM54's "1 - pad" (sum of ALU flags) + // multiplicity is non-zero and the CPU's PC-update Memw sender + // fires to match the MEMW_R PC row that + // `collect_register_ops_from_cpu` unconditionally emits. + entry.op_add = true; } Instruction::EcallEbreak => { diff --git a/prover/src/tests/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 9a1b8a6d0..2ce3cfc5a 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,173 @@ 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. +/// 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(); + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/allocator.elf")) + .unwrap(); + let proof = crate::prove(&elf_bytes).expect("prove should succeed"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify should not error"), + "allocator.elf should verify" + ); + assert_eq!(proof.public_output, b"Hello World"); +} + +/// Minimal Rust program that proves on main: no_std, no_main, +/// no syscalls crate → only Commit + Halt ecalls (both have receivers). +/// Demonstrates that Rust CAN prove when it avoids Print/Panic. +#[test] +fn test_pure_commit_rust() { + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/pure_commit.elf")) + .unwrap(); + let proof = crate::prove(&elf_bytes).expect("prove should succeed"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify should not error"), + "pure_commit.elf should verify — it has no Print/Panic ecalls" + ); + assert_eq!(proof.public_output, vec![0xAA, 0xBB, 0xCC, 0xDD]); +} + +#[test] +#[ignore = "Requires ~128GB RAM: proves an empty Ethereum block"] +fn test_prove_ethrex_empty_block() { + let _ = env_logger::builder().is_test(true).try_init(); + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/ethrex.elf")) + .expect("need ethrex.elf"); + let input = + std::fs::read(workspace_root.join("executor/tests/ethrex_empty_block.bin")).unwrap(); + let proof = crate::prove_with_input(&elf_bytes, input).expect("prove"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify"), + "ethrex empty block should verify" + ); + assert_eq!(proof.public_output.len(), 160); +} + /// Regression test: addiw with negative immediate must verify. /// arg2_sign_bit is the sign bit of rv2 (bit 31), not of arg2, per spec /// constraint CPU-CE61: MSB16[arg2_sign_bit; rv2[1]]. diff --git a/syscalls/src/allocator.rs b/syscalls/src/allocator.rs index 2469f435d..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..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)] @@ -105,7 +106,8 @@ pub enum SyscallError { #[cfg(target_arch = "riscv64")] pub fn sys_halt() -> ! { - print_string("sys_halt called\n"); + // NOTE: no print_string here — the Print ecall is unmatched on the Ecall bus + // and would cause a verification failure. unsafe { asm!( "ecall",