diff --git a/toolchain/build/src/lib.rs b/toolchain/build/src/lib.rs index e78531172d..f4c21070c2 100644 --- a/toolchain/build/src/lib.rs +++ b/toolchain/build/src/lib.rs @@ -257,11 +257,11 @@ pub fn build_guest_package

( "--target-dir", target_dir.as_ref().to_str().unwrap(), ]); - tty_println(&format!("cargo command: {:?}", cmd)); if !is_debug() { cmd.args(["--release"]); } + tty_println(&format!("cargo command: {:?}", cmd)); let mut child = cmd .stderr(Stdio::piped()) diff --git a/toolchain/instructions/src/phantom.rs b/toolchain/instructions/src/phantom.rs index eb4ecf0127..dda16e42e2 100644 --- a/toolchain/instructions/src/phantom.rs +++ b/toolchain/instructions/src/phantom.rs @@ -13,6 +13,8 @@ pub enum PhantomInstruction { PrintF, /// Prepare the next input vector for hinting. HintInput, + /// Prepare the next input vector for hinting, but prepend it with a 4-byte decomposition of its length instead of one field element. + HintInputRv32, /// Prepare the little-endian bit decomposition of a variable for hinting. HintBits, /// Start tracing diff --git a/toolchain/riscv/axvm/src/env/mod.rs b/toolchain/riscv/axvm/src/env/mod.rs deleted file mode 100644 index 96a2206a22..0000000000 --- a/toolchain/riscv/axvm/src/env/mod.rs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2024 RISC Zero, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Functions for interacting with the host environment. -//! -//! The zkVM provides a set of functions to perform operations that manage -//! execution, I/O, and proof composition. The set of functions related to each -//! of these operations are described below. -//! -//! ## System State -//! -//! The guest has some control over the execution of the zkVM by pausing or -//! exiting the program explicitly. This can be achieved using the [pause] and -//! [exit] functions. -//! -//! ## Proof Verification -//! -//! The zkVM supports verification of RISC Zero [receipts] in a guest program, -//! enabling [proof composition]. This can be achieved using the [verify()] and -//! [verify_integrity] functions. -//! -//! ## Input and Output -//! -//! The zkVM provides a set of functions for handling input, public output, and -//! private output. This is useful when interacting with the host and committing -//! to some data publicly. -//! -//! The zkVM provides functions that automatically perform (de)serialization on -//! types and, for performance reasons, there is also a `_slice` variant that -//! works with raw slices of plain old data. Performing operations on slices is -//! more efficient, saving cycles during execution and consequently producing -//! smaller proofs that are faster to produce. However, the `_slice` variants -//! can be less ergonomic, so consider trade-offs when choosing between the two. -//! For more information about guest optimization, see RISC Zero's [instruction -//! on guest optimization][guest-optimization] -//! -//! Convenience functions to read and write to default file descriptors are -//! provided. See [read()], [write()], [self::commit] (and their `_slice` -//! variants) for more information. -//! -//! In order to access default file descriptors directly, see [stdin], [stdout], -//! [stderr] and [journal]. These file descriptors are either [FdReader] or -//! [FdWriter] instances, which can be used to read from or write to the host. -//! To read from or write into them, use the [Read] and [Write] traits. -//! -//! WARNING: Specifying a file descriptor with the same value of a default file -//! descriptor is not recommended and may lead to unexpected behavior. A list of -//! default file descriptors can be found in the [fileno] module. -//! -//! ## Utility -//! -//! The zkVM provides utility functions to log messages to the debug console and -//! to measure the number of processor cycles that have occurred since the guest -//! began. These can be achieved using the [log] and [cycle_count] functions. -//! -//! [receipts]: crate::Receipt -//! [proof composition]:https://www.risczero.com/blog/proof-composition -//! [guest-optimization]: -//! https://dev.risczero.com/api/zkvm/optimization#when-reading-data-as-raw-bytes-use-envread_slice - -extern crate alloc; - -use axvm_platform; - -/// Terminate execution of the zkVM. -/// -/// Use an exit code of 0 to indicate success, and non-zero to indicate an error. -#[inline(always)] -pub fn exit() -> ! { - axvm_platform::rust_rt::terminate::(); - unreachable!(); -} diff --git a/toolchain/riscv/axvm/src/intrinsics/io.rs b/toolchain/riscv/axvm/src/intrinsics/io.rs new file mode 100644 index 0000000000..bc22275a8d --- /dev/null +++ b/toolchain/riscv/axvm/src/intrinsics/io.rs @@ -0,0 +1,17 @@ +// use crate::custom_insn_i; + +use axvm_platform::{custom_insn_i, intrinsics::CUSTOM_0}; + +/// Store the next 4 bytes from the hint stream to [[rd] + imm]_2. +#[macro_export] +macro_rules! hint_store_u32 { + ($x:ident, $imm:expr) => { + axvm_platform::custom_insn_i!(axvm_platform::intrinsics::CUSTOM_0, 0b001, $x, "x0", $imm) + }; +} + +/// Reset the hint stream with the next hint. +#[inline(always)] +pub fn hint_input() { + custom_insn_i!(CUSTOM_0, 0b011, "x0", "x0", 0); +} diff --git a/toolchain/riscv/axvm/src/intrinsics/mod.rs b/toolchain/riscv/axvm/src/intrinsics/mod.rs index d6c4f0426a..0dabfe4216 100644 --- a/toolchain/riscv/axvm/src/intrinsics/mod.rs +++ b/toolchain/riscv/axvm/src/intrinsics/mod.rs @@ -1,5 +1,8 @@ //! Functions that call custom instructions that use axVM intrinsic instructions. mod hash; +/// Library functions for user input/output. +pub mod io; pub use hash::*; +pub use io::*; diff --git a/toolchain/riscv/axvm/src/io.rs b/toolchain/riscv/axvm/src/io.rs new file mode 100644 index 0000000000..24d33485f6 --- /dev/null +++ b/toolchain/riscv/axvm/src/io.rs @@ -0,0 +1,47 @@ +//! User IO functions + +use alloc::vec::Vec; +use core::alloc::Layout; + +use crate::{hint_store_u32, intrinsics::hint_input}; + +/// Read `size: u32` and then `size` bytes from the hint stream into a vector. +pub fn read_vec() -> Vec { + hint_input(); + read_vec_by_len(read_u32() as usize) +} + +/// Read the next 4 bytes from the hint stream into a register. +/// Because [hint_store_u32] stores a word to memory, this function first reads to memory and then +/// loads from memory to register. +#[inline(always)] +#[allow(asm_sub_register)] +pub fn read_u32() -> u32 { + let ptr = unsafe { alloc::alloc::alloc(Layout::from_size_align(4, 4).unwrap()) }; + let addr = ptr as u32; + hint_store_u32!(addr, 0); + let result: u32; + unsafe { + core::arch::asm!("lw {rd}, ({rs1})", rd = out(reg) result, rs1 = in(reg) addr); + } + result +} + +/// Read the next `len` bytes from the hint stream into a vector. +fn read_vec_by_len(len: usize) -> Vec { + let num_words = (len + 3) / 4; + let capacity = num_words * 4; + // Allocate a buffer of the required length that is 4 byte aligned + // Note: this expect message doesn't matter until our panic handler actually cares about it + let layout = Layout::from_size_align(capacity, 4).expect("vec is too large"); + // SAFETY: We populate a `Vec` by hintstore-ing `num_words` 4 byte words. We set the length to `len` and don't care about the extra `capacity - len` bytes stored. + let ptr_start = unsafe { alloc::alloc::alloc(layout) }; + let mut ptr = ptr_start; + + // Note: if len % 4 != 0, this will discard some last bytes + for _ in 0..num_words { + hint_store_u32!(ptr, 0); + ptr = unsafe { ptr.add(4) }; + } + unsafe { Vec::from_raw_parts(ptr_start, len, capacity) } +} diff --git a/toolchain/riscv/axvm/src/lib.rs b/toolchain/riscv/axvm/src/lib.rs index e2913b8a5d..b6f92504d9 100644 --- a/toolchain/riscv/axvm/src/lib.rs +++ b/toolchain/riscv/axvm/src/lib.rs @@ -1,89 +1,16 @@ -// Copyright 2024 RISC Zero, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The RISC Zero zkVM's guest-side RISC-V API. -//! -//! Code that is validated by the [RISC Zero zkVM](crate) is run inside the guest. In almost all -//! practical cases, the guest will want to read private input data from the host and write public -//! data to the journal. This can be done with [env::read] and [env::commit], respectively; -//! additional I/O functionality is also available in [mod@env]. -//! -//! ## Installation -//! -//! To build and run RISC Zero zkVM code, you will need to install the RISC Zero -//! toolchain, which can be done using the rzup utility: -//! -//! ```sh -//! curl -L https://risczero.com/install | bash -//! rzup install -//! ``` -//! -//! ## Example -//! -//! The following guest code[^starter-ex] proves a number is -//! composite by multiplying two unsigned integers, and panicking if either is -//! `1` or if the multiplication overflows: -//! -//! ```ignore -//! #![no_main] -//! #![no_std] -//! -//! use risc0_zkvm::guest::env; -//! -//! risc0_zkvm::guest::entry!(main); -//! -//! fn main() { -//! // Load the first number from the host -//! let a: u64 = env::read(); -//! // Load the second number from the host -//! let b: u64 = env::read(); -//! // Verify that neither of them are 1 (i.e. nontrivial factors) -//! if a == 1 || b == 1 { -//! panic!("Trivial factors") -//! } -//! // Compute the product while being careful with integer overflow -//! let product = a.checked_mul(b).expect("Integer overflow"); -//! env::commit(&product); -//! } -//! ``` -//! -//! Notice how [env::read] is used to load the two factors, and [env::commit] is used to make their -//! composite product publicly available. All input an output of your guest is private except for -//! what is written to the journal with [env::commit]. -//! -//! By default, the guest only has the Rust `core` libraries and not `std`. A partial -//! implementation of the Rust standard libraries can be enabled with the `std` feature on this [crate]. -//! When this feature is not enabled, the lines including `#![no_std]` and `#![no_main]` are -//! required, as well as the use of the [crate::guest::entry] macro. When `std` is enabled, these -//! three lines can be omitted and many features of `std` can be used. -//! -//! If you encounter problems building zkVM guest code, you can see if we have a -//! known workaround for your issue by looking in our -//! [rust guest workarounds](https://github.com/risc0/risc0/issues?q=is%3Aissue+is%3Aopen+label%3A%22rust+guest+workarounds%22) -//! tag on GitHub. -//! -//! [^starter-ex]: The example is based on the [Factors example](https://github.com/risc0/risc0/tree/main/examples/factors). +//! # axVM #![cfg_attr(not(feature = "std"), no_std)] #![deny(rustdoc::broken_intra_doc_links)] #![deny(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![feature(asm_const)] extern crate alloc; -pub mod env; pub mod intrinsics; +pub mod io; +pub mod process; pub mod serde; #[cfg(target_os = "zkvm")] @@ -165,7 +92,8 @@ unsafe extern "C" fn __start() -> ! { main() } - env::exit::<0>(); + process::exit(); + unreachable!() } #[cfg(target_os = "zkvm")] diff --git a/toolchain/riscv/axvm/src/process.rs b/toolchain/riscv/axvm/src/process.rs new file mode 100644 index 0000000000..bb98b32ca4 --- /dev/null +++ b/toolchain/riscv/axvm/src/process.rs @@ -0,0 +1,11 @@ +//! System exit and panic functions. + +/// Exit the program with exit code 0. +pub fn exit() { + axvm_platform::rust_rt::terminate::<0>(); +} + +/// Exit the program with exit code 1. +pub fn panic() { + axvm_platform::rust_rt::terminate::<1>(); +} diff --git a/toolchain/riscv/examples/hint/program/.cargo/config.toml b/toolchain/riscv/examples/hint/program/.cargo/config.toml new file mode 100644 index 0000000000..bb18f45366 --- /dev/null +++ b/toolchain/riscv/examples/hint/program/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "riscv32im-risc0-zkvm-elf" + +[unstable] +build-std = ["core", "alloc", "proc_macro", "panic_abort"] diff --git a/toolchain/riscv/examples/hint/program/Cargo.toml b/toolchain/riscv/examples/hint/program/Cargo.toml new file mode 100644 index 0000000000..a04e080de9 --- /dev/null +++ b/toolchain/riscv/examples/hint/program/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +[package] +version = "0.1.0" +name = "axvm-hint-program" +edition = "2021" + +[dependencies] +axvm = { path = "../../../axvm" } diff --git a/toolchain/riscv/examples/hint/program/src/main.rs b/toolchain/riscv/examples/hint/program/src/main.rs new file mode 100644 index 0000000000..b2ca6ac68e --- /dev/null +++ b/toolchain/riscv/examples/hint/program/src/main.rs @@ -0,0 +1,13 @@ +#![no_main] +#![no_std] +use axvm::io::read_vec; + +axvm::entry!(main); + +pub fn main() { + let vec = read_vec(); + assert_eq!(vec.len(), 4); + for i in 0..4 { + assert_eq!(vec[i], i as u8); + } +} diff --git a/toolchain/riscv/platform/src/intrinsics/mod.rs b/toolchain/riscv/platform/src/intrinsics/mod.rs new file mode 100644 index 0000000000..b264654605 --- /dev/null +++ b/toolchain/riscv/platform/src/intrinsics/mod.rs @@ -0,0 +1,53 @@ +pub const CUSTOM_0: u8 = 0x0b; +pub const CUSTOM_1: u8 = 0x2b; + +#[macro_export] +macro_rules! custom_insn_i { + ($opcode:expr, $funct3:expr, $rd:literal, $rs1:literal, $imm:expr) => { + unsafe { + core::arch::asm!(concat!( + ".insn i {opcode}, {funct3}, ", + $rd, + ", ", + $rs1, + ", {imm}", + ), opcode = const $opcode, funct3 = const $funct3, imm = const $imm) + } + }; + ($opcode:expr, $funct3:expr, $x:ident, $rs1:literal, $imm:expr) => { + unsafe { + core::arch::asm!(concat!( + ".insn i {opcode}, {funct3}, {rd}, ", + $rs1, + ", {imm}", + ), opcode = const $opcode, funct3 = const $funct3, rd = in(reg) $x, imm = const $imm) + } + }; +} + +#[macro_export] +macro_rules! custom_insn_r { + ($opcode:expr, $funct3:expr, $rd:literal, $rs1:literal, $rs2:literal) => { + unsafe { + core::arch::asm!(concat!( + ".insn r {opcode}, {funct3}, ", + $rd, + ", ", + $rs1, + ", ", + $rs2, + ), opcode = const $opcode, funct3 = const $funct3) + } + }; + ($opcode:expr, $funct3:expr, $x:ident, $rs1:literal, $rs2:literal) => { + unsafe { + core::arch::asm!(concat!( + ".insn r {opcode}, {funct3}, {rd}, ", + $rs1, + ", ", + $rs2, + ), opcode = const $opcode, funct3 = const $funct3, rd = out(reg) $x) + } + }; + // TODO: implement more variants with like rs1 = in(reg) $y etc +} diff --git a/toolchain/riscv/platform/src/lib.rs b/toolchain/riscv/platform/src/lib.rs index 1267dc3e7b..9f001cf9fe 100644 --- a/toolchain/riscv/platform/src/lib.rs +++ b/toolchain/riscv/platform/src/lib.rs @@ -26,6 +26,7 @@ pub mod memory; // mod getrandom; #[cfg(all(feature = "rust-runtime", target_os = "zkvm"))] pub mod heap; +pub mod intrinsics; #[cfg(all(feature = "export-libm", target_os = "zkvm"))] mod libm_extern; #[cfg(feature = "rust-runtime")] diff --git a/toolchain/riscv/platform/src/rust_rt.rs b/toolchain/riscv/platform/src/rust_rt.rs index 3b6f90ab9e..de4ea56b00 100644 --- a/toolchain/riscv/platform/src/rust_rt.rs +++ b/toolchain/riscv/platform/src/rust_rt.rs @@ -23,17 +23,17 @@ #[cfg(target_os = "zkvm")] use core::arch::asm; +#[cfg(target_os = "zkvm")] +use crate::{custom_insn_i, custom_insn_r, intrinsics::CUSTOM_0}; + extern crate alloc; #[inline(always)] pub fn terminate() { #[cfg(target_os = "zkvm")] - unsafe { - asm!(".insn i 0x0b, 0, x0, x0, {ec}", ec = const EXIT_CODE) - }; + custom_insn_i!(CUSTOM_0, 0, "x0", "x0", EXIT_CODE); #[cfg(not(target_os = "zkvm"))] { - core::hint::black_box(()); unimplemented!() } } diff --git a/toolchain/riscv/transpiler/src/rrs.rs b/toolchain/riscv/transpiler/src/rrs.rs index 1dc65f37ea..04e3646bdb 100644 --- a/toolchain/riscv/transpiler/src/rrs.rs +++ b/toolchain/riscv/transpiler/src/rrs.rs @@ -9,8 +9,10 @@ use axvm_circuit::{ rv32im::adapters::RV32_REGISTER_NUM_LIMBS, }; use axvm_instructions::{ - instruction::Instruction, riscv::RvIntrinsic, EccOpcode, Rv32ModularArithmeticOpcode, + instruction::Instruction, riscv::RvIntrinsic, EccOpcode, PhantomInstruction, + Rv32HintStoreOpcode, Rv32ModularArithmeticOpcode, }; +use axvm_platform::intrinsics::{CUSTOM_0, CUSTOM_1}; use p3_field::PrimeField32; use rrs_lib::{ instruction_formats::{BType, IType, ITypeShamt, JType, RType, SType, UType}, @@ -245,24 +247,32 @@ fn process_custom_instruction(instruction_u32: u32) -> Instruct let funct3 = ((instruction_u32 >> 12) & 0b111) as u8; // All our instructions are R- or I-type match opcode { - 0x0b => { - match funct3 { - 0b000 => { - let imm = (instruction_u32 >> 20) & 0xfff; - Some(terminate(imm.try_into().expect("exit code must be byte"))) - } - 0b001 => { - // keccak or poseidon - None - } - 0b010 => { - // u256 - todo!("Implement u256 transpiler"); - } - _ => None, + CUSTOM_0 => match funct3 { + 0b000 => { + let imm = (instruction_u32 >> 20) & 0xfff; + Some(terminate(imm.try_into().expect("exit code must be byte"))) } - } - 0x2b => { + 0b001 => { + let rd = (instruction_u32 >> 7) & 0x1f; + let imm = (instruction_u32 >> 20) & 0xfff; + Some(Instruction::from_isize( + Rv32HintStoreOpcode::HINT_STOREW.with_default_offset(), + 0, + (RV32_REGISTER_NUM_LIMBS * rd as usize) as isize, + imm as isize, + 1, + 2, + )) + } + 0b011 => Some(Instruction::phantom( + PhantomInstruction::HintInputRv32, + F::zero(), + F::zero(), + 0, + )), + _ => unimplemented!(), + }, + CUSTOM_1 => { match funct3 { Rv32ModularArithmeticOpcode::FUNCT3 => { // mod operations @@ -315,7 +325,6 @@ pub(crate) fn transpile(instructions_u32: &[u32]) -> Vec(PhantomData); for instruction_u32 in instructions_u32 { - // TODO: we probably want to forbid such instructions, but for now we just skip them assert!(*instruction_u32 != 115, "ecall is not supported"); let instruction = process_instruction(&mut transpiler, *instruction_u32) .unwrap_or_else(|| process_custom_instruction(*instruction_u32)); diff --git a/toolchain/riscv/transpiler/src/tests.rs b/toolchain/riscv/transpiler/src/tests.rs index 756f57baa6..88e4c59856 100644 --- a/toolchain/riscv/transpiler/src/tests.rs +++ b/toolchain/riscv/transpiler/src/tests.rs @@ -12,6 +12,7 @@ use axvm_circuit::{ use axvm_platform::memory::MEM_SIZE; use eyre::Result; use p3_baby_bear::BabyBear; +use p3_field::AbstractField; use tempfile::tempdir; use test_case::test_case; @@ -91,6 +92,27 @@ fn test_rv32i_prove(examples_path: &str, min_segments: usize) -> Result<()> { Ok(()) } +#[test] +fn test_rv32i_prove_with_hint() -> Result<()> { + let pkg = get_package(get_examples_dir().join("hint/program")); + let target_dir = tempdir()?; + let guest_opts = GuestOptions::default().into(); + build_guest_package(&pkg, &target_dir, &guest_opts, None); + let elf_path = guest_methods(&pkg, &target_dir, &[]).pop().unwrap(); + let config = VmConfig { + max_segment_len: (1 << 18) - 1, + ..VmConfig::rv32i() + }; + let (_, exe) = setup_executor_from_elf(elf_path, config.clone())?; + air_test_with_min_segments( + config, + exe, + vec![[0, 1, 2, 3].map(F::from_canonical_u32).to_vec()], + 1, + ); + Ok(()) +} + #[test_case("data/rv32im-intrin-from-as")] fn test_intrinsic_runtime(elf_path: &str) -> Result<()> { setup_tracing(); diff --git a/vm/src/system/phantom/mod.rs b/vm/src/system/phantom/mod.rs index fcb440ffb0..462a433cf7 100644 --- a/vm/src/system/phantom/mod.rs +++ b/vm/src/system/phantom/mod.rs @@ -137,7 +137,7 @@ impl InstructionExecutor for PhantomChip { let value = RefCell::borrow(&self.memory).unsafe_read_cell(addr_space, a); println!("{}", value); } - PhantomInstruction::HintInput => { + PhantomInstruction::HintInput | PhantomInstruction::HintInputRv32 => { let mut streams = self.streams.get().unwrap().lock(); let hint = match streams.input_stream.pop_front() { Some(hint) => hint, @@ -146,9 +146,18 @@ impl InstructionExecutor for PhantomChip { } }; streams.hint_stream.clear(); - streams - .hint_stream - .push_back(F::from_canonical_usize(hint.len())); + if phantom == PhantomInstruction::HintInputRv32 { + streams.hint_stream.extend( + (hint.len() as u32) + .to_le_bytes() + .iter() + .map(|b| F::from_canonical_u8(*b)), + ); + } else { + streams + .hint_stream + .push_back(F::from_canonical_usize(hint.len())); + } streams.hint_stream.extend(hint); drop(streams); }