Skip to content

Commit bcca759

Browse files
authored
Fix(prover): fix issues blocking Rust std program verification (#511)
* fix executor bugs * fix coments
1 parent c66f029 commit bcca759

10 files changed

Lines changed: 175 additions & 15 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.attribute 5, "rv64i2p1"
2+
.globl main
3+
main:
4+
# Exercises W-suffix instructions (ADDIW, SRLIW) on a register holding
5+
# a 64-bit value with non-zero upper 32 bits. The executor's Log must
6+
# store the full 64-bit register value in src1_val/src2_val so the
7+
# prover's MEMW_R Memory bus chain stays consistent.
8+
9+
# Load a 64-bit value with non-zero hi32 into a0 (x10).
10+
# 0xDEADBEEF_12345678
11+
li t0, 0xDEADBEEF # t0 = 0xDEADBEEF (sign-extended)
12+
slli t0, t0, 32 # t0 = 0xDEADBEEF_00000000
13+
li t1, 0x12345678
14+
or a0, t0, t1 # a0 = 0xDEADBEEF_12345678
15+
16+
# Execute ADDIW on a0 — reads a0 (64-bit) but operates on lower 32.
17+
# If src1_val is truncated to 32 bits, the upper 0xDEADBEEF is lost and
18+
# the prover's MEMW_R chain for x10 word 1 won't balance.
19+
addiw t2, a0, 1 # t2 = sign_extend32(0x12345678 + 1) = 0x12345679
20+
21+
# Execute SRLIW — another W-suffix that reads a0.
22+
srliw t3, a0, 4 # t3 = sign_extend32(0x12345678 >> 4) = 0x01234567
23+
24+
# Commit 8 bytes of a0 (the original 64-bit value should be intact).
25+
# a0 was never written by ADDIW/SRLIW (they write t2/t3, not a0).
26+
addi a1, sp, -8 # buf on stack
27+
sd a0, 0(a1) # store a0 to buf
28+
li a0, 1 # fd = 1
29+
li a2, 8 # count = 8
30+
li a7, 64 # Commit
31+
ecall
32+
33+
# Halt
34+
li a0, 0
35+
li a7, 93
36+
ecall
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[target.riscv64im-lambda-vm-elf]
2+
rustflags = [
3+
"-C", "link-arg=-e",
4+
"-C", "link-arg=main",
5+
"-C", "passes=lower-atomic"
6+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[workspace]
2+
3+
[package]
4+
name = "pure_commit"
5+
version = "0.1.0"
6+
edition = "2024"
7+
8+
[dependencies]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Minimal Rust guest program: no_std, no_main, no allocator, no syscalls crate.
2+
// Uses only raw inline `asm!("ecall")` for Commit (64) and Halt (93).
3+
// Serves as a control case in the prover test suite (`test_pure_commit_rust`):
4+
// verifies that Rust can compile to a provable ELF when the heap allocator is
5+
// bypassed, independent of the Rust-std startup path.
6+
#![no_std]
7+
#![no_main]
8+
9+
use core::arch::asm;
10+
use core::panic::PanicInfo;
11+
12+
#[panic_handler]
13+
fn panic(_info: &PanicInfo) -> ! {
14+
loop {}
15+
}
16+
17+
#[unsafe(export_name = "main")]
18+
pub extern "C" fn main() -> ! {
19+
// Commit 4 bytes [0xAA, 0xBB, 0xCC, 0xDD]
20+
let buf: [u8; 4] = [0xAA, 0xBB, 0xCC, 0xDD];
21+
unsafe {
22+
asm!(
23+
"ecall",
24+
inlateout("a0") 1usize => _, // fd = stdout; ecall overwrites a0
25+
in("a1") buf.as_ptr(),
26+
in("a2") 4usize,
27+
in("a7") 64usize, // Commit
28+
);
29+
}
30+
// Halt
31+
unsafe {
32+
asm!(
33+
"ecall",
34+
inlateout("a0") 0usize => _, // exit_code = 0; ecall overwrites a0
35+
in("a7") 93usize, // Halt
36+
);
37+
}
38+
loop {}
39+
}

executor/src/vm/instruction/execution.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ impl Instruction {
6666
}
6767
}
6868
Instruction::ArithImmW { dst, src, imm, op } => {
69-
// W-suffix: operate on lower 32 bits, sign-extend result to 64 bits
70-
let op1 = registers.read(src)? as i32;
69+
// W-suffix: operate on lower 32 bits, sign-extend result to 64 bits.
70+
// Log must store the RAW register value in src1_val (full 64 bits)
71+
// for the prover's MEMW register chain. The truncation to i32 is only
72+
// for the ALU computation.
73+
let raw_src = registers.read(src)?;
74+
let op1 = raw_src as i32;
7175
if matches!(op, ArithOp::Sub) {
7276
return Err(ExecutionError::SubImmNotSupported);
7377
}
@@ -77,7 +81,7 @@ impl Instruction {
7781
Log {
7882
current_pc: pc,
7983
next_pc: pc.wrapping_add(REGULAR_PC_UPDATE),
80-
src1_val: op1 as u64,
84+
src1_val: raw_src,
8185
src2_val: 0,
8286
dst_val: res,
8387
}
@@ -247,17 +251,21 @@ impl Instruction {
247251
src2,
248252
op,
249253
} => {
250-
// W-suffix: operate on lower 32 bits, sign-extend result to 64 bits
251-
let a = registers.read(src1)? as i32;
252-
let b = registers.read(src2)? as i32;
254+
// W-suffix: operate on lower 32 bits, sign-extend result to 64 bits.
255+
// Log must store RAW register values (full 64 bits) for the prover's
256+
// MEMW register chain. Truncation to i32 is only for ALU computation.
257+
let raw_src1 = registers.read(src1)?;
258+
let raw_src2 = registers.read(src2)?;
259+
let a = raw_src1 as i32;
260+
let b = raw_src2 as i32;
253261
let res32 = op.apply_word(a, b)?;
254262
let res = res32 as i64 as u64; // Sign-extend to 64 bits
255263
registers.write(dst, res)?;
256264
Log {
257265
current_pc: pc,
258266
next_pc: pc.wrapping_add(REGULAR_PC_UPDATE),
259-
src1_val: a as u64,
260-
src2_val: b as u64,
267+
src1_val: raw_src1,
268+
src2_val: raw_src2,
261269
dst_val: res,
262270
}
263271
}

prover/src/tables/shift.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,14 @@ impl ShiftOperation {
181181
let left = !self.direction;
182182
let right = self.direction;
183183

184-
// is_negative = MSB of in[3]
185-
let is_negative = (self.in_halves[3] >> 15) & 1 == 1;
184+
// is_negative is the MSB of in[3] BUT gated by `signed`. The SHIFT
185+
// AIR constrains IS_NEGATIVE via the MSB16 bus (SHIFT-C14) only when
186+
// `signed = 1` — for `signed = 0` IS_NEGATIVE is free, so we set it
187+
// to zero. This makes `extension = 65535 * is_negative = 0` for SRL,
188+
// so the extension contribution in `compute_shifted_half` naturally
189+
// vanishes (zero fill) — matching RISC-V SRL semantics regardless of
190+
// the top-bit value of the input.
191+
let is_negative = self.signed && (self.in_halves[3] >> 15) & 1 == 1;
186192
let extension: u16 = if is_negative { 0xFFFF } else { 0 };
187193

188194
// bit_shift

prover/src/tables/types.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,13 @@ impl DecodeEntry {
668668
}
669669

670670
Instruction::CSR { .. } => {
671-
// CSR instructions not yet supported in prover
671+
// CSR instructions are executed as no-ops by the VM (see
672+
// executor Instruction::CSR arm returning dst_val: 0,
673+
// src1/2_val: 0). Mirror that here by treating them as
674+
// `ADDI x0, x0, 0` — same pattern as `Fence`. This sets
675+
// `op_add=true` so CM54's multiplicity is non-zero and the
676+
// CPU's PC-update Memw sender fires.
677+
entry.op_add = true;
672678
}
673679

674680
Instruction::EcallEbreak => {

prover/src/tests/prove_elfs_tests.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1899,6 +1899,57 @@ fn test_verify_rejects_inflated_table_counts() {
18991899
);
19001900
}
19011901

1902+
/// Proves a program that uses W-suffix instructions (ADDIW, SRLIW) on a
1903+
/// register holding a 64-bit value with non-zero upper 32 bits.
1904+
/// Verifies that the full 64-bit value is preserved in the MEMW_R chain.
1905+
#[test]
1906+
fn test_prove_wsuffix_64bit() {
1907+
let elf_bytes = crate::test_utils::asm_elf_bytes("test_wsuffix_64bit");
1908+
let result = crate::prove_and_verify(&elf_bytes).expect("prove_and_verify failed");
1909+
assert!(result, "W-suffix 64-bit register test should verify");
1910+
}
1911+
1912+
/// Proves a minimal Rust std program that uses `init_allocator()` and
1913+
/// `String::from("Hello World") + commit`. Exercises the full Rust-std stack:
1914+
/// TLSF heap init (SRL on high-bit values), CSR instructions injected by
1915+
/// the Rust toolchain, and the allocator's memory access patterns.
1916+
#[test]
1917+
fn test_prove_allocator_minimal_reproducer() {
1918+
let _ = env_logger::builder().is_test(true).try_init();
1919+
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1920+
.parent()
1921+
.unwrap()
1922+
.to_path_buf();
1923+
let elf_bytes =
1924+
std::fs::read(workspace_root.join("executor/program_artifacts/rust/allocator.elf"))
1925+
.expect("allocator.elf not found — run `make compile-programs-rust`");
1926+
let proof = crate::prove(&elf_bytes).expect("prove should succeed");
1927+
assert!(
1928+
crate::verify(&proof, &elf_bytes).expect("verify should not error"),
1929+
"allocator.elf should verify"
1930+
);
1931+
assert_eq!(proof.public_output, b"Hello World");
1932+
}
1933+
1934+
/// Minimal Rust program that proves: no_std, no_main, no allocator, no
1935+
/// syscalls crate. Only Commit + Halt ecalls (both have receivers).
1936+
#[test]
1937+
fn test_pure_commit_rust() {
1938+
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1939+
.parent()
1940+
.unwrap()
1941+
.to_path_buf();
1942+
let elf_bytes =
1943+
std::fs::read(workspace_root.join("executor/program_artifacts/rust/pure_commit.elf"))
1944+
.expect("pure_commit.elf not found — run `make compile-programs-rust`");
1945+
let proof = crate::prove(&elf_bytes).expect("prove should succeed");
1946+
assert!(
1947+
crate::verify(&proof, &elf_bytes).expect("verify should not error"),
1948+
"pure_commit.elf should verify"
1949+
);
1950+
assert_eq!(proof.public_output, vec![0xAA, 0xBB, 0xCC, 0xDD]);
1951+
}
1952+
19021953
/// Regression test: addiw with negative immediate must verify.
19031954
/// arg2_sign_bit is the sign bit of rv2 (bit 31), not of arg2, per spec
19041955
/// constraint CPU-CE61: MSB16[arg2_sign_bit; rv2[1]].

syscalls/src/allocator.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use embedded_alloc::TlsfHeap as Heap;
22
use riscv as _;
33

4-
use crate::syscalls::print_string;
5-
64
#[global_allocator]
75
static HEAP: Heap = Heap::empty();
86

@@ -49,6 +47,7 @@ pub unsafe extern "C" fn sys_getenv(
4947
_varname: *const u8,
5048
_varname_len: usize,
5149
) -> usize {
52-
print_string("sys_getenv is disabled");
50+
// NOTE: no print_string here — the Print ecall (#1) has no receiver on the
51+
// Ecall bus and would cause a verification failure.
5352
usize::MAX
5453
}

syscalls/src/syscalls.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ pub enum SyscallError {
105105

106106
#[cfg(target_arch = "riscv64")]
107107
pub fn sys_halt() -> ! {
108-
print_string("sys_halt called\n");
108+
// NOTE: no print_string here — the Print ecall is unmatched on the Ecall bus
109+
// and would cause a verification failure.
109110
unsafe {
110111
asm!(
111112
"ecall",

0 commit comments

Comments
 (0)