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/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/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/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..b1f047ee1 100644 --- a/executor/src/vm/memory.rs +++ b/executor/src/vm/memory.rs @@ -41,10 +41,13 @@ 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. +/// Must match `PRIVATE_INPUT_START` in `syscalls/src/syscalls.rs`. +pub const PRIVATE_INPUT_START_INDEX: u64 = 0xFF000000; #[derive(Default, Debug)] pub struct Memory(U64HashMap<[u8; 4]>); @@ -151,7 +154,13 @@ 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.is_empty() { + return Ok(()); + } if inputs.len() as u64 > MAX_PRIVATE_INPUT_SIZE { return Err(MemoryError::PrivateInputSizeExceeded); } @@ -160,13 +169,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 763257356..bd42b9948 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, + /// 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. @@ -366,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) @@ -507,7 +520,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())?; + let traces = Traces::from_elf_and_logs( + &program, + &result.logs, + &MaxRowsConfig::default(), + private_inputs, + )?; Ok(( traces.total_field_elements(), traces.total_auxiliary_field_elements(), @@ -558,7 +576,7 @@ pub fn prove_with_options_and_inputs( // 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, private_inputs)?; #[cfg(feature = "instruments")] let trace_build_elapsed = phase_start.elapsed(); @@ -608,11 +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(), + num_private_input_pages, }) } @@ -643,9 +668,26 @@ 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 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; + 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, &vm_proof.runtime_page_ranges); + let page_configs = Traces::page_configs_from_elf_and_runtime( + &program, + &vm_proof.runtime_page_ranges, + vm_proof.num_private_input_pages, + ); // 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/page.rs b/prover/src/tables/page.rs index 872b8d553..a4597e1b8 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -47,8 +47,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; @@ -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 b1af698de..74afd5b4a 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -97,6 +97,18 @@ impl MemoryState { Self { cells } } + /// 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; + for (i, &b) in private_input_bytes(private_input).iter().enumerate() { + self.cells.insert(start + 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)) @@ -1343,32 +1355,62 @@ fn collect_byte_check_ops_for_padding(num_padding_rows: usize) -> Vec Vec { - use std::collections::BTreeSet; +/// +/// Encode private input as `[len_u32_LE][data]` — the canonical wire format. +/// Must match `executor::vm::memory::Memory::store_private_inputs`. +fn private_input_bytes(private_input: &[u8]) -> 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; - 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(); 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; } } } + if !private_input.is_empty() { + 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); + let page_data = init_page_data + .entry(page_base) + .or_insert_with(|| vec![0u8; page_size]); + page_data[offset] = b; + } + } + init_page_data +} + +fn collect_bitwise_from_page( + elf: &Elf, + memory_state: &MemoryState, + private_input: &[u8], +) -> Vec { + use std::collections::BTreeSet; + + let page_size = page::DEFAULT_PAGE_SIZE; + let mut bitwise_ops = Vec::new(); + + let elf_page_data = build_init_page_data(elf, private_input); // Derive ALL page bases from memory_state (includes ELF + runtime pages) let mut page_bases: BTreeSet = BTreeSet::new(); @@ -1513,6 +1555,7 @@ fn collect_bitwise_from_commit(commit_ops: &[CommitOperation]) -> Vec ( Vec>, Vec, @@ -1521,27 +1564,8 @@ 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; - } - } - } + // 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(); @@ -1560,8 +1584,22 @@ 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) = elf_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) @@ -1785,6 +1823,7 @@ fn build_traces( decode_pc_to_row: HashMap, register_state: RegisterState, max_rows: &super::MaxRowsConfig, + private_input: &[u8], ) -> Result { let CollectedOps { cpu_ops, @@ -1820,7 +1859,7 @@ fn build_traces( bitwise_ops.extend(collect_bitwise_from_memw_register(&memw_register_ops)); // PAGE tables do a batched IS_BYTE[init, fini] lookup per row (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 @@ -1895,7 +1934,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), @@ -1913,7 +1952,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; } @@ -2159,31 +2198,14 @@ impl Traces { use std::collections::BTreeSet; let page_size = page::DEFAULT_PAGE_SIZE; - let mut page_bases: BTreeSet = BTreeSet::new(); - let mut elf_page_data: HashMap> = HashMap::new(); + let init_page_data = build_init_page_data(elf, &[]); - for segment in &elf.data { - for (i, &word) in segment.values.iter().enumerate() { - let word_addr = segment.base_addr + (i as u64 * 4); - for byte_offset in 0..4u64 { - let byte_addr = word_addr + byte_offset; - let byte_value = ((word >> (byte_offset * 8)) & 0xFF) as u8; - let page_base = page::page_base_for_address(byte_addr, page_size); - let offset = page::offset_in_page(byte_addr, page_size); - page_bases.insert(page_base); - - let page_data = elf_page_data - .entry(page_base) - .or_insert_with(|| vec![0u8; page_size]); - page_data[offset] = byte_value; - } - } - } + let page_bases: BTreeSet = init_page_data.keys().copied().collect(); page_bases .into_iter() .map(|base| { - if let Some(init_data) = elf_page_data.get(&base) { + 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) @@ -2192,18 +2214,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], + num_private_input_pages: usize, ) -> Vec { 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 { @@ -2213,6 +2239,21 @@ 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 } @@ -2271,6 +2312,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) @@ -2284,6 +2326,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); @@ -2309,6 +2352,7 @@ impl Traces { decode_pc_to_row, register_state, max_rows, + private_input, ) } @@ -2357,6 +2401,7 @@ impl Traces { 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 3c9abbb98..7e0fbc181 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, &[], 0); let verifier_airs = crate::VmAirs::new(&elf, &proof_options, true, &wrong_configs, &table_counts); let verifier_air_refs = verifier_airs.air_refs(); @@ -1126,7 +1128,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)"); @@ -1457,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), @@ -1478,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(); @@ -1500,7 +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); + 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(); @@ -1537,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(); @@ -1554,7 +1558,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(); @@ -1581,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(); + 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 ); @@ -1615,16 +1620,17 @@ 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(); - // 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 ); @@ -1641,7 +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); + 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(); @@ -1951,6 +1957,219 @@ fn test_pure_commit_rust() { assert_eq!(proof.public_output, vec![0xAA, 0xBB, 0xCC, 0xDD]); } +/// Backward-compatibility: `prove_with_inputs` with empty input must match `prove`. +#[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"); + assert!( + crate::verify(&result, &elf_bytes).expect("verify should not error"), + "prove_with_inputs(empty) proof should verify" + ); +} + +/// ASM test: reads private input from 0xFF000000, commits 8 bytes. +#[test] +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"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify should not error"), + "proof should verify" + ); + assert_eq!(proof.public_output, input[4..12].to_vec()); +} + +/// Same ASM, different input values — output depends on input. +#[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_inputs(&elf_bytes, &input).expect("prove"); + assert!( + crate::verify(&proof, &elf_bytes).expect("verify"), + "proof should verify" + ); + assert_eq!(proof.public_output, input[4..12].to_vec()); +} + +/// End-to-end: Rust std program with private input. +#[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 not found — run `make compile-programs-rust`"); + 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"), + "commit_sum should verify" + ); + assert_eq!(proof.public_output, vec![8u8]); +} + +#[test] +#[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")) + .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_inputs(&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); +} + +// ============================================================================= +// 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(), + "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(), + "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 = (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), + // 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]]. diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index f85ccd3ae..ae0315ff5 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -1,15 +1,17 @@ #[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). +/// Must match `executor::vm::memory::PRIVATE_INPUT_START_INDEX`. #[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,31 +78,27 @@ 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) +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; + let data_ptr = (PRIVATE_INPUT_START + 4) as *const u8; + let slice = unsafe { core::slice::from_raw_parts(data_ptr, len) }; + slice.to_vec() } -#[derive(Debug)] -pub enum SyscallError { - WrongPrivateInputSize, +#[cfg(not(target_arch = "riscv64"))] +pub fn get_private_input() -> Vec { + unimplemented!("syscalls are only implemented for riscv64 targets"); } #[cfg(target_arch = "riscv64")]