Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Add the functionality for custom RISC-V intrinsics, support hints in RISC-V #726

Merged
merged 15 commits into from
Oct 31, 2024
Merged
2 changes: 1 addition & 1 deletion toolchain/build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,11 @@ pub fn build_guest_package<P>(
"--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())
Expand Down
2 changes: 2 additions & 0 deletions toolchain/instructions/src/phantom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions toolchain/riscv/axvm/src/intrinsics/io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// use crate::custom_insn_i;

use alloc::{alloc::Layout, vec::Vec};

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) => {
custom_insn_i!(CUSTOM_0, 0b001, $x, "x0", $imm)
};
}

/// Read the next 4 bytes from the hint stream.
#[allow(asm_sub_register)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this magic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok it seems it's because we hint to memory and register is stack. please add some comments because you're too clever

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was just doing what compiler suggested

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<u8> {
// Note: this expect message doesn't matter until our panic handler actually cares about it
let layout = Layout::from_size_align((len + 3) / 4 * 4, 4).expect("vec is too large");
let ptr = unsafe { alloc::alloc::alloc(layout) };
let mut x: u32 = 0;
// Note: if len % 4 != 0, this will discard some last bytes
for i in 0..len {
if i % 4 == 0 {
// TODO: it probably makes sense not to juggle the data between registers and memory here.
// On the other hand, maybe it's not a big deal.
x = read_u32();
}
unsafe {
ptr.add(i).write_volatile((x & 255) as u8);
}
x >>= 8;
}
unsafe { Vec::from_raw_parts(ptr, len, len) }
}

/// Read `size: u32` and then `size` bytes from the hint stream into a vector.
pub fn read_vec() -> Vec<u8> {
read_vec_by_len(read_u32() as usize)
}

/// Reset the hint stream with the next hint.
pub fn hint_input() {
custom_insn_i!(CUSTOM_0, 0b011, "x0", "x0", 0);
}
13 changes: 13 additions & 0 deletions toolchain/riscv/axvm/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
//! 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::*;

/// Exit the program with exit code 0.
pub fn exit() {
jonathanpwang marked this conversation as resolved.
Show resolved Hide resolved
axvm_platform::rust_rt::terminate::<0>();
}

/// Exit the program with exit code 1.
pub fn panic() {
axvm_platform::rust_rt::terminate::<1>();
}
1 change: 1 addition & 0 deletions toolchain/riscv/axvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![feature(asm_const)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a nightly feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the imm = const $imm in asm! macro doesn't work without this. I don't know if it's nightly or not, here is its origin: rust-lang/rust#93332


extern crate alloc;

Expand Down
5 changes: 5 additions & 0 deletions toolchain/riscv/examples/hint/program/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build]
target = "riscv32im-risc0-zkvm-elf"

[unstable]
build-std = ["core", "alloc", "proc_macro", "panic_abort"]
8 changes: 8 additions & 0 deletions toolchain/riscv/examples/hint/program/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[workspace]
[package]
version = "0.1.0"
name = "axvm-hint-program"
edition = "2021"

[dependencies]
axvm = { path = "../../../axvm" }
12 changes: 12 additions & 0 deletions toolchain/riscv/examples/hint/program/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#![no_main]
#![no_std]

axvm::entry!(main);

pub fn main() {
axvm::intrinsics::io::hint_input();
let vec = axvm::intrinsics::io::read_size_and_vec();
if vec.iter().sum::<u8>() == 0 {
axvm::intrinsics::panic();
}
}
53 changes: 53 additions & 0 deletions toolchain/riscv/platform/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
@@ -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) => {
jonathanpwang marked this conversation as resolved.
Show resolved Hide resolved
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
}
1 change: 1 addition & 0 deletions toolchain/riscv/platform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
8 changes: 4 additions & 4 deletions toolchain/riscv/platform/src/rust_rt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<const EXIT_CODE: u8>() {
#[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!()
}
}
Expand Down
Golovanov399 marked this conversation as resolved.
Show resolved Hide resolved
Binary file not shown.
47 changes: 28 additions & 19 deletions toolchain/riscv/transpiler/src/rrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -245,24 +247,32 @@ fn process_custom_instruction<F: PrimeField32>(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 => {
jonathanpwang marked this conversation as resolved.
Show resolved Hide resolved
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;
jonathanpwang marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -315,7 +325,6 @@ pub(crate) fn transpile<F: PrimeField32>(instructions_u32: &[u32]) -> Vec<Instru
let mut instructions = Vec::new();
let mut transpiler = InstructionTranspiler::<F>(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));
Expand Down
25 changes: 25 additions & 0 deletions toolchain/riscv/transpiler/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -91,6 +92,30 @@ 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![[1, 0, 0, 0]
.iter()
.map(|x| F::from_canonical_u32(*x))
.collect()],
1,
);
Ok(())
}

#[test_case("data/rv32im-intrin-from-as")]
fn test_intrinsic_runtime(elf_path: &str) -> Result<()> {
setup_tracing();
Expand Down
17 changes: 13 additions & 4 deletions vm/src/system/phantom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ impl<F: PrimeField32> InstructionExecutor<F> for PhantomChip<F> {
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,
Expand All @@ -146,9 +146,18 @@ impl<F: PrimeField32> InstructionExecutor<F> for PhantomChip<F> {
}
};
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);
}
Expand Down
Loading