Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
82ca002
fix executor bugs
ColoCarletti Apr 20, 2026
4cd0262
add private inputs feature
ColoCarletti Apr 20, 2026
fe5ce4b
Merge branch 'main' into feat/private-input-v2
ColoCarletti Apr 22, 2026
7adb2d9
merge
ColoCarletti Apr 22, 2026
13a33ef
change page size
ColoCarletti Apr 22, 2026
852caef
fmt
ColoCarletti Apr 22, 2026
018ca30
fix_tests
ColoCarletti Apr 22, 2026
a74b854
Merge branch 'main' into feat/private-input-v2
ColoCarletti Apr 22, 2026
4b85658
Merge branch 'main' into feat/private-input-v2
ColoCarletti Apr 22, 2026
1c83f3c
Merge branch 'main' into feat/private-input-v2
ColoCarletti Apr 22, 2026
7027205
fix
ColoCarletti Apr 22, 2026
f022518
Merge branch 'feat/private-input-v2' of github.com:yetanotherco/lambd…
ColoCarletti Apr 22, 2026
1e9a4b4
Merge branch 'main' into feat/private-input-v2
ColoCarletti Apr 22, 2026
0ab0911
fix_merge
ColoCarletti Apr 22, 2026
96de63b
fmt
ColoCarletti Apr 22, 2026
d576cec
Merge branch 'main' into feat/private-input-v2
ColoCarletti Apr 22, 2026
dcf7929
fix_comments
ColoCarletti Apr 22, 2026
bd2b0fb
Merge branch 'feat/private-input-v2' of github.com:yetanotherco/lambd…
ColoCarletti Apr 22, 2026
d3326af
change verifier privete_inputs handle
ColoCarletti Apr 22, 2026
d610c84
fmt
ColoCarletti Apr 22, 2026
de9e0ec
Merge branch 'main' into feat/private-input-v2
diegokingston Apr 23, 2026
3df476e
add max private input size
ColoCarletti Apr 24, 2026
17b5b61
Merge branch 'feat/private-input-v2' of github.com:yetanotherco/lambd…
ColoCarletti Apr 24, 2026
5501b94
fmt
ColoCarletti Apr 24, 2026
37f6641
fmt
ColoCarletti Apr 24, 2026
a27148c
fmt
ColoCarletti Apr 24, 2026
b3cdaa5
test: add security tests for private input tamper scenarios
MauroToscano Apr 24, 2026
2241eac
clippy + fmt
MauroToscano Apr 24, 2026
e065a7d
clippy + fmt
MauroToscano Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions executor/programs/asm/test_private_input_xpage.s
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion executor/programs/rust/commit/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use lambda_vm_syscalls as syscalls;

pub fn main() {
let input: Vec<u8> = syscalls::syscalls::get_private_input().unwrap();
let input: Vec<u8> = syscalls::syscalls::get_private_input();
syscalls::syscalls::print_string(format!("Private input received: {:?}\n", input).as_str());
syscalls::syscalls::commit(&input);
}
2 changes: 1 addition & 1 deletion executor/programs/rust/commit_sum/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use lambda_vm_syscalls as syscalls;

pub fn main() {
let input: Vec<u8> = syscalls::syscalls::get_private_input().unwrap();
let input: Vec<u8> = syscalls::syscalls::get_private_input();
let a = input[0];
let b = input[1];
syscalls::syscalls::commit((a + b).to_le_bytes().as_ref());
Expand Down
2 changes: 1 addition & 1 deletion executor/programs/rust/ethrex/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ProgramInput, Error>(&input).unwrap();
let output = execution_program(input).unwrap();
let output_bytes = output.encode();
Expand Down
2 changes: 1 addition & 1 deletion executor/programs/rust/memory/src/main.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions executor/programs/rust/pure_commit/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 0 additions & 10 deletions executor/src/vm/instruction/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const REGULAR_PC_UPDATE: u64 = 4;
pub enum SyscallNumbers {
Print = 1,
Panic = 2,
GetPrivateInputs = 4,
Commit = 64,
Halt = 93,
}
Expand All @@ -21,7 +20,6 @@ impl TryFrom<u64> 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(()),
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 13 additions & 11 deletions executor/src/vm/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ pub type U64HashMap<V> = HashMap<u64, V, U64BuildHasher>;
// 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]>);
Expand Down Expand Up @@ -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<u8>) -> Result<(), MemoryError> {
if inputs.is_empty() {
return Ok(());
}
if inputs.len() as u64 > MAX_PRIVATE_INPUT_SIZE {
return Err(MemoryError::PrivateInputSizeExceeded);
}
Expand All @@ -160,13 +169,6 @@ impl Memory {
Ok(())
}

pub fn load_private_inputs(&self) -> Result<Vec<u8>, 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<u8> {
let mut result = Vec::with_capacity(len as usize);
let end = addr + len;
Expand Down
13 changes: 13 additions & 0 deletions executor/tests/asm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> = (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");
Expand Down
Binary file added executor/tests/ethrex_empty_block.bin
Binary file not shown.
58 changes: 50 additions & 8 deletions prover/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ pub struct VmProof {
pub table_counts: TableCounts,
/// Committed public output bytes.
pub public_output: Vec<u8>,
/// 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,
Comment thread
ColoCarletti marked this conversation as resolved.
}

/// Error type for the prover crate.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
})
}

Expand Down Expand Up @@ -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.
Expand Down
27 changes: 24 additions & 3 deletions prover/src/tables/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
ColoCarletti marked this conversation as resolved.

/// Stack top address (where SP starts). Re-exported from executor.
pub use executor::vm::registers::STACK_TOP;
Expand Down Expand Up @@ -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<Vec<u8>>,
/// 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 {
Expand All @@ -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<u8>) -> Self {
assert!(data.len() <= page_size, "Data exceeds page size");
let mut init_values = data;
Expand All @@ -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<u8>) -> 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,
}
}
}
Expand Down
Loading
Loading