Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion bin/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ fn cmd_prove(elf_path: PathBuf, output_path: PathBuf, blowup: Option<u8>, 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...");
Expand Down
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
6 changes: 6 additions & 0 deletions executor/programs/rust/pure_commit/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[target.riscv64im-lambda-vm-elf]
rustflags = [
"-C", "link-arg=-e",
"-C", "link-arg=main",
"-C", "passes=lower-atomic"
]
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.

8 changes: 8 additions & 0 deletions executor/programs/rust/pure_commit/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[workspace]

[package]
name = "pure_commit"
version = "0.1.0"
edition = "2024"

[dependencies]
39 changes: 39 additions & 0 deletions executor/programs/rust/pure_commit/src/main.rs
Original file line number Diff line number Diff line change
@@ -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 {}
}
34 changes: 16 additions & 18 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 @@ -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);
}
Expand All @@ -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,
}
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 9 additions & 11 deletions executor/src/vm/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ 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.
pub const PRIVATE_INPUT_START_INDEX: u64 = 0xFF000000;

#[derive(Default, Debug)]
pub struct Memory(U64HashMap<[u8; 4]>);
Expand Down Expand Up @@ -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<u8>) -> Result<(), MemoryError> {
if inputs.len() as u64 > MAX_PRIVATE_INPUT_SIZE {
return Err(MemoryError::PrivateInputSizeExceeded);
Expand All @@ -160,13 +165,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.
41 changes: 37 additions & 4 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>,
/// 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<u8>,
}

/// Error type for the prover crate.
Expand Down Expand Up @@ -481,6 +485,24 @@ pub(crate) fn compute_expected_commit_bus_balance(
pub fn prove(elf_bytes: &[u8]) -> Result<VmProof, Error> {
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<u8>) -> Result<VmProof, Error> {
prove_with_options(
elf_bytes,
private_input,
&GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"),
&MaxRowsConfig::default(),
)
Expand All @@ -489,6 +511,7 @@ pub fn prove(elf_bytes: &[u8]) -> Result<VmProof, Error> {
/// Prove an ELF binary execution with custom proof options and max rows config.
pub fn prove_with_options(
elf_bytes: &[u8],
private_input: Vec<u8>,
proof_options: &ProofOptions,
max_rows: &MaxRowsConfig,
) -> Result<VmProof, Error> {
Expand All @@ -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}")))?;
Expand All @@ -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();
Expand Down Expand Up @@ -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,
})
}

Expand Down Expand Up @@ -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.
Expand Down
16 changes: 13 additions & 3 deletions prover/src/tables/shift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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::<F>::from(65535u64);

Expand Down
Loading
Loading