risc0: Guest program compilation with stock rust compiler. (#114)

This commit is contained in:
rodiazet
2025-08-29 20:57:40 +02:00
committed by GitHub
parent 8d3847df28
commit a063344dae
12 changed files with 322 additions and 3 deletions

1
Cargo.lock generated
View File

@@ -2351,6 +2351,7 @@ dependencies = [
"build-utils",
"bytemuck",
"cargo_metadata 0.19.2",
"risc0-binfmt",
"risc0-build",
"risc0-zkvm",
"serde",

View File

@@ -74,6 +74,7 @@ pico-sdk = { git = "https://github.com/brevis-network/pico.git", tag = "v1.1.7"
# Risc0 dependencies
risc0-build = "3.0.1"
risc0-zkvm = { version = "3.0.1", default-features = false }
risc0-binfmt = { version = "3.0.1", default-features = false }
# SP1 dependencies
sp1-sdk = "5.1.0"

View File

@@ -17,6 +17,7 @@ tracing.workspace = true
# Risc0 dependencies
risc0-build = { workspace = true, features = ["unstable"] }
risc0-zkvm = { workspace = true, features = ["client", "unstable"] }
risc0-binfmt.workspace = true
# Local dependencies
zkvm-interface.workspace = true

View File

@@ -26,7 +26,7 @@ pub fn compile_risc0_program(guest_directory: &Path) -> Result<Risc0Program, Com
// `cargo-risczero build` for the `unstable` features.
let guest =
risc0_build::build_package(package, &metadata.target_directory, GuestOptions::default())
.map_err(|source| CompileError::Risc0BuildFailure {
.map_err(|source| CompileError::BuildFailure {
source,
crate_path: guest_directory.to_path_buf(),
})?

View File

@@ -0,0 +1,123 @@
use crate::compile::Risc0Program;
use crate::error::CompileError;
use cargo_metadata::MetadataCommand;
use risc0_binfmt::ProgramBinary;
use std::fs;
use std::path::Path;
use std::process::Command;
use tracing::info;
static CARGO_ENCODED_RUSTFLAGS_SEPARATOR: &str = "\x1f";
// TODO: Make this with `zkos` package building to avoid binary file storing in repo.
// File taken from https://github.com/risc0/risc0/blob/v3.0.1/risc0/zkos/v1compat/elfs/v1compat.elf
const V1COMPAT_ELF: &[u8] = include_bytes!("kernel_elf/v1compat.elf");
const TARGET_TRIPLE: &str = "riscv32ima-unknown-none-elf";
// Rust flags according to https://github.com/risc0/risc0/blob/v3.0.1/risc0/build/src/lib.rs#L455
const RUSTFLAGS: &[&str] = &[
"-C",
"passes=lower-atomic", // Only for rustc > 1.81
"-C",
// Start of the code section
"link-arg=-Ttext=0x00200800",
"-C",
"link-arg=--fatal-warnings",
"-C",
"panic=abort",
"--cfg",
"getrandom_backend=\"custom\"",
];
const CARGO_ARGS: &[&str] = &[
"build",
"--target",
TARGET_TRIPLE,
"--release",
// For bare metal we have to build core and alloc
"-Zbuild-std=core,alloc",
];
pub fn compile_risc0_program_stock_rust(
guest_directory: &Path,
toolchain: &String,
) -> Result<Risc0Program, CompileError> {
wrap_into_risc0_program(compile_program_stock_rust(guest_directory, toolchain)?)
}
fn compile_program_stock_rust(
guest_directory: &Path,
toolchain: &String,
) -> Result<Vec<u8>, CompileError> {
let metadata = MetadataCommand::new().current_dir(guest_directory).exec()?;
let package = metadata
.root_package()
.ok_or_else(|| CompileError::MissingPackageName {
path: guest_directory.to_path_buf(),
})?;
let plus_toolchain = format!("+{}", toolchain);
let mut cargo_args = [plus_toolchain.as_str()].to_vec();
cargo_args.append(&mut CARGO_ARGS.to_vec());
let encoded_rust_flags = RUSTFLAGS.to_vec().join(CARGO_ENCODED_RUSTFLAGS_SEPARATOR);
let target_direcotry = guest_directory
.join("target")
.join(TARGET_TRIPLE)
.join("release");
// Remove target directory.
if target_direcotry.exists() {
fs::remove_dir_all(&target_direcotry).unwrap();
}
let result = Command::new("cargo")
.current_dir(guest_directory)
.env("CARGO_ENCODED_RUSTFLAGS", &encoded_rust_flags)
.args(cargo_args)
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.map_err(|source| CompileError::BuildFailure {
source: source.into(),
crate_path: guest_directory.to_path_buf(),
});
if result.is_err() {
return Err(result.err().unwrap());
}
let elf_path = target_direcotry.join(&package.name);
fs::read(&elf_path).map_err(|e| CompileError::ReadFile {
path: elf_path,
source: e,
})
}
fn wrap_into_risc0_program(elf: Vec<u8>) -> Result<Risc0Program, CompileError> {
let program = ProgramBinary::new(elf.as_slice(), V1COMPAT_ELF);
let image_id = program.compute_image_id()?;
info!("Risc0 program compiled OK - {} bytes", elf.len());
info!("Image ID - {image_id}");
Ok(Risc0Program {
elf: program.encode(),
image_id,
})
}
#[cfg(test)]
mod tests {
use crate::compile_stock_rust::compile_risc0_program_stock_rust;
use test_utils::host::testing_guest_directory;
#[test]
fn test_stock_compiler_impl() {
let guest_directory = testing_guest_directory("risc0", "stock_nightly_no_std");
let result = compile_risc0_program_stock_rust(&guest_directory, &"nightly".to_string());
assert!(result.is_ok(), "Risc0 guest program compilation failure.");
assert!(
!result.unwrap().elf.is_empty(),
"ELF bytes should not be empty."
);
}
}

View File

@@ -20,13 +20,21 @@ pub enum CompileError {
#[error("Could not find `[package].name` in guest Cargo.toml at {path}")]
MissingPackageName { path: PathBuf },
#[error("`risc0_build::build_package` for {crate_path} failed: {source}")]
Risc0BuildFailure {
BuildFailure {
#[source]
source: anyhow::Error,
crate_path: PathBuf,
},
#[error("`risc0_build::build_package` succeeded but failed to find guest")]
Risc0BuildMissingGuest,
#[error("Failed to read file at {path}: {source}")]
ReadFile {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("ELF binary image calculation failure : {0}")]
ImageIDCalculationFailure(#[from] anyhow::Error),
}
impl CompileError {

Binary file not shown.

View File

@@ -2,6 +2,7 @@
use crate::{
compile::{Risc0Program, compile_risc0_program},
compile_stock_rust::compile_risc0_program_stock_rust,
error::Risc0Error,
output::deserialize_from,
};
@@ -20,6 +21,8 @@ use zkvm_interface::{
include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs"));
mod compile;
mod compile_stock_rust;
mod error;
mod output;
@@ -58,7 +61,14 @@ impl Compiler for RV32_IM_RISC0_ZKVM_ELF {
type Program = Risc0Program;
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
compile_risc0_program(guest_directory).map_err(Risc0Error::from)
let toolchain = env::var("ERE_GUEST_TOOLCHAIN").unwrap_or_else(|_error| "risc0".into());
match toolchain.as_str() {
"risc0" => Ok(compile_risc0_program(guest_directory)?),
_ => Ok(compile_risc0_program_stock_rust(
guest_directory,
&toolchain,
)?),
}
}
}
@@ -284,6 +294,17 @@ mod tests {
run_zkvm_execute(&zkvm, &io);
}
#[test]
fn test_execute_nightly() {
let guest_directory = testing_guest_directory("risc0", "stock_nightly_no_std");
let program =
compile_risc0_program_stock_rust(&guest_directory, &"nightly".to_string()).unwrap();
let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap();
let result = zkvm.execute(&BasicProgramIo::empty());
assert!(result.is_ok(), "Risc0 execution failure");
}
#[test]
fn test_execute_invalid_inputs() {
let program = basic_program();

View File

@@ -38,6 +38,9 @@ ENV RISC0_VERSION="3.0.1" \
RISC0_CPP_VERSION="2024.1.5" \
RISC0_RUST_VERSION="1.88.0"
# Add `rust-src` component to enable std build for nightly rust.
RUN rustup +nightly component add rust-src
# Run the Risc0 SDK installation script
# It will use the RISC0_VERSION, RISC0_CPP_VERSION and RISC0_RUST_VERSION defined above.
RUN /tmp/install_risc0_sdk.sh && rm /tmp/install_risc0_sdk.sh

View File

@@ -0,0 +1,7 @@
[package]
name = "addition_no_std"
edition = "2021"
[dependencies]
[workspace]

View File

@@ -0,0 +1,26 @@
#![no_std]
#![no_main]
extern crate alloc;
use alloc::vec::Vec;
use core::sync::atomic::Ordering;
use core::sync::atomic::AtomicU16;
mod risc0_rt;
fn main() {
let a: AtomicU16 = core::hint::black_box(AtomicU16::new(5));
let b: AtomicU16 = core::hint::black_box(AtomicU16::new(7));
if a.load(Ordering::SeqCst) + b.load(Ordering::SeqCst) != 12 {
panic!("Something went wrong!");
}
let mut v: Vec<AtomicU16> = Vec::new();
v.push(AtomicU16::new(5));
v.push(AtomicU16::new(7));
if v[0].load(Ordering::SeqCst) + v[1].load(Ordering::SeqCst) != 12 {
panic!("Something went wrong!");
}
}

View File

@@ -0,0 +1,128 @@
use core::alloc::{GlobalAlloc, Layout};
// Import user `main` function
use crate::main;
// 1. Init global pointer (GP). It's used to optimize jumps by linker. Linker can change jumping from PC(Program Counter) based to GP based.
// 2. Init stack pointer to the value STACK_TOP. It's stored in sp register.
// 3. Call __start function defined below.
// `__global_pointer$` is set by the linker. Its value depends on linker optimization. https://www.programmersought.com/article/77722901592/
core::arch::global_asm!(
r#"
.section .text._start;
.globl _start;
_start:
.option push;
.option norelax;
la gp, __global_pointer$;
.option pop;
la sp, {0}
lw sp, 0(sp)
call __start;
"#,
sym STACK_TOP
);
static STACK_TOP: u32 = 0x0020_0400;
// 1. Call `main` user function
// 2. Call system halt environment function. It's defined by sp1 vm.
#[unsafe(no_mangle)]
fn __start(_argc: isize, _argv: *const *const u8) -> isize {
main();
const EMPTY_OUTPUT: [u32; 8] = [0; 8];
unsafe {
core::arch::asm!(
"ecall",
in("t0") 0,
in("a0") 0,
in("a1") &EMPTY_OUTPUT,
)
};
unreachable!()
}
// Implement panic handling by calling undefined instruction. To be fixed. We need to support `fence` to be able to use e.i. `portable_atomic` lib.
#[panic_handler]
fn panic_impl(_panic_info: &core::panic::PanicInfo) -> ! {
unsafe { core::arch::asm!("fence", options(noreturn)) };
}
/// A simple heap allocator.
///
/// Allocates memory from left to right, without any deallocation.
struct SimpleAlloc;
unsafe impl GlobalAlloc for SimpleAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe {
sys_alloc_aligned(layout.size(), layout.align())
}
}
unsafe fn dealloc(&self, _: *mut u8, _: Layout) {}
}
#[global_allocator]
static HEAP: SimpleAlloc = SimpleAlloc;
pub const MAX_MEMORY: usize = 0x78000000;
static mut HEAP_POS: usize = 0;
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sys_alloc_aligned(bytes: usize, align: usize) -> *mut u8 {
unsafe extern "C" {
// https://lld.llvm.org/ELF/linker_script.html#sections-command
// `_end` is the last global variable defined by the linker. Its address is the beginning of heap data.
unsafe static _end: u8;
}
// SAFETY: Single threaded, so nothing else can touch this while we're working.
let mut heap_pos = unsafe { HEAP_POS };
if heap_pos == 0 {
heap_pos = unsafe { (&_end) as *const u8 as usize };
}
let offset = heap_pos & (align - 1);
if offset != 0 {
heap_pos += align - offset;
}
let ptr = heap_pos as *mut u8;
let (heap_pos, overflowed) = heap_pos.overflowing_add(bytes);
if overflowed || MAX_MEMORY < heap_pos {
panic!("Memory limit exceeded (0x78000000)");
}
unsafe { HEAP_POS = heap_pos };
ptr
}
// Assume single-threaded.
#[cfg(all(target_arch = "riscv32", target_feature = "a"))]
#[unsafe(no_mangle)]
fn _critical_section_1_0_acquire() -> u32
{
return 0;
}
#[cfg(all(target_arch = "riscv32", target_feature = "a"))]
#[unsafe(no_mangle)]
fn _critical_section_1_0_release(_: u32)
{}
// Assume single-threaded.
#[cfg(all(target_arch = "riscv64", target_feature = "a"))]
#[unsafe(no_mangle)]
fn _critical_section_1_0_acquire() -> u64
{
return 0;
}
#[cfg(all(target_arch = "riscv64", target_feature = "a"))]
#[unsafe(no_mangle)]
fn _critical_section_1_0_release(_: u64)
{}