diff --git a/Cargo.lock b/Cargo.lock index 3fdf260..ad745d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2337,8 +2337,10 @@ dependencies = [ name = "ere-pico" version = "0.0.11" dependencies = [ + "anyhow", "bincode", "build-utils", + "cargo_metadata 0.19.2", "pico-sdk", "pico-vm", "serde", diff --git a/crates/ere-pico/Cargo.toml b/crates/ere-pico/Cargo.toml index 5b1bceb..32c2735 100644 --- a/crates/ere-pico/Cargo.toml +++ b/crates/ere-pico/Cargo.toml @@ -6,7 +6,9 @@ rust-version.workspace = true license.workspace = true [dependencies] +anyhow.workspace = true bincode.workspace = true +cargo_metadata.workspace = true serde.workspace = true thiserror.workspace = true diff --git a/crates/ere-pico/src/compile_stock_rust.rs b/crates/ere-pico/src/compile_stock_rust.rs new file mode 100644 index 0000000..68785b0 --- /dev/null +++ b/crates/ere-pico/src/compile_stock_rust.rs @@ -0,0 +1,111 @@ +use crate::error::PicoError; +use cargo_metadata::MetadataCommand; +use std::fs; +use std::path::Path; +use std::process::Command; + +static CARGO_ENCODED_RUSTFLAGS_SEPARATOR: &str = "\x1f"; + +const TARGET_TRIPLE: &str = "riscv32ima-unknown-none-elf"; +// According to https://github.com/brevis-network/pico/blob/v1.1.7/sdk/cli/src/build/build.rs#L104 +const RUSTFLAGS: &[&str] = &[ + // Replace atomic ops with nonatomic versions since the guest is single threaded. + "-C", + "passes=lower-atomic", + // Specify where to start loading the program in + // memory. The clang linker understands the same + // command line arguments as the GNU linker does; see + // https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#SEC3 + // for details. + "-C", + "link-arg=-Ttext=0x00200800", + // Apparently not having an entry point is only a linker warning(!), so + // error out in this case. + "-C", + "link-arg=--fatal-warnings", + "-C", + "panic=abort", +]; +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_pico_program_stock_rust( + guest_directory: &Path, + toolchain: &String, +) -> Result, PicoError> { + compile_program_stock_rust(guest_directory, toolchain) +} + +fn compile_program_stock_rust( + guest_directory: &Path, + toolchain: &String, +) -> Result, PicoError> { + let metadata = MetadataCommand::new().current_dir(guest_directory).exec()?; + let package = metadata + .root_package() + .ok_or_else(|| PicoError::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| PicoError::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| PicoError::ReadFile { + path: elf_path, + source: e, + }) +} + +#[cfg(test)] +mod tests { + use crate::compile_stock_rust::compile_pico_program_stock_rust; + use test_utils::host::testing_guest_directory; + + #[test] + fn test_stock_compiler_impl() { + let guest_directory = testing_guest_directory("pico", "stock_nightly_no_std"); + let result = compile_pico_program_stock_rust(&guest_directory, &"nightly".to_string()); + assert!(result.is_ok(), "Pico guest program compilation failure."); + assert!( + !result.unwrap().is_empty(), + "ELF bytes should not be empty." + ); + } +} diff --git a/crates/ere-pico/src/error.rs b/crates/ere-pico/src/error.rs index 3eb1096..f8fddca 100644 --- a/crates/ere-pico/src/error.rs +++ b/crates/ere-pico/src/error.rs @@ -33,4 +33,20 @@ pub enum PicoError { #[source] source: io::Error, }, + #[error("Pico build failure for {crate_path} failed: {source}")] + BuildFailure { + #[source] + source: anyhow::Error, + crate_path: PathBuf, + }, + #[error("Could not find `[package].name` in guest Cargo.toml at {path}")] + MissingPackageName { path: PathBuf }, + #[error("Failed to read file at {path}: {source}")] + ReadFile { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("`cargo metadata` failed: {0}")] + MetadataCommand(#[from] cargo_metadata::Error), } diff --git a/crates/ere-pico/src/lib.rs b/crates/ere-pico/src/lib.rs index 06dc7ec..348218a 100644 --- a/crates/ere-pico/src/lib.rs +++ b/crates/ere-pico/src/lib.rs @@ -1,14 +1,17 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] +use compile_stock_rust::compile_pico_program_stock_rust; use pico_sdk::client::DefaultProverClient; use pico_vm::{configs::stark_config::KoalaBearPoseidon2, emulator::stdin::EmulatorStdinBuilder}; use serde::de::DeserializeOwned; -use std::{io::Read, path::Path, process::Command, time::Instant}; +use std::{env, io::Read, path::Path, process::Command, time::Instant}; use zkvm_interface::{ Compiler, Input, InputItem, ProgramExecutionReport, ProgramProvingReport, Proof, ProverResourceType, PublicValues, zkVM, zkVMError, }; +mod compile_stock_rust; + include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); mod error; use error::PicoError; @@ -21,40 +24,51 @@ impl Compiler for PICO_TARGET { type Program = Vec; - fn compile(&self, guest_path: &Path) -> Result { - // 1. Check guest path - if !guest_path.exists() { - return Err(PicoError::PathNotFound(guest_path.to_path_buf())); + fn compile(&self, guest_directory: &Path) -> Result { + let toolchain = env::var("ERE_GUEST_TOOLCHAIN").unwrap_or_else(|_error| "pico".into()); + match toolchain.as_str() { + "pico" => Ok(compile_pico_program(guest_directory)?), + _ => Ok(compile_pico_program_stock_rust( + guest_directory, + &toolchain, + )?), } - - // 2. Run `cargo pico build` - let status = Command::new("cargo") - .current_dir(guest_path) - .env("RUST_LOG", "info") - .args(["pico", "build"]) - .status()?; // From → Spawn - - if !status.success() { - return Err(PicoError::CargoFailed { status }); - } - - // 3. Locate the ELF file - let elf_path = guest_path.join("elf/riscv32im-pico-zkvm-elf"); - - if !elf_path.exists() { - return Err(PicoError::ElfNotFound(elf_path)); - } - - // 4. Read the ELF file - let elf_bytes = std::fs::read(&elf_path).map_err(|e| PicoError::ReadElf { - path: elf_path, - source: e, - })?; - - Ok(elf_bytes) } } +fn compile_pico_program(guest_directory: &Path) -> Result, PicoError> { + // 1. Check guest path + if !guest_directory.exists() { + return Err(PicoError::PathNotFound(guest_directory.to_path_buf())); + } + + // 2. Run `cargo pico build` + let status = Command::new("cargo") + .current_dir(guest_directory) + .env("RUST_LOG", "info") + .args(["pico", "build"]) + .status()?; // From → Spawn + + if !status.success() { + return Err(PicoError::CargoFailed { status }); + } + + // 3. Locate the ELF file + let elf_path = guest_directory.join("elf/riscv32im-pico-zkvm-elf"); + + if !elf_path.exists() { + return Err(PicoError::ElfNotFound(elf_path)); + } + + // 4. Read the ELF file + let elf_bytes = std::fs::read(&elf_path).map_err(|e| PicoError::ReadElf { + path: elf_path, + source: e, + })?; + + Ok(elf_bytes) +} + pub struct ErePico { program: ::Program, } @@ -193,6 +207,17 @@ mod tests { run_zkvm_execute(&zkvm, &io); } + #[test] + fn test_execute_nightly() { + let guest_directory = testing_guest_directory("pico", "stock_nightly_no_std"); + let program = + compile_pico_program_stock_rust(&guest_directory, &"nightly".to_string()).unwrap(); + let zkvm = ErePico::new(program, ProverResourceType::Cpu); + + let result = zkvm.execute(&BasicProgramIo::empty()); + assert!(result.is_ok(), "Pico execution failure"); + } + #[test] fn test_execute_invalid_inputs() { let program = basic_program(); diff --git a/docker/pico/Dockerfile b/docker/pico/Dockerfile index 8e862e2..01f4547 100644 --- a/docker/pico/Dockerfile +++ b/docker/pico/Dockerfile @@ -11,6 +11,9 @@ RUN chmod +x /tmp/install_pico_sdk.sh RUN rustup default nightly +# Add `rust-src` component to enable std build for nightly rust. +RUN rustup +nightly component add rust-src + # Run the Pico SDK installation script. # This script installs the specific Rust toolchain (nightly-2025-08-04) # and installs pico-cli (as cargo-pico). diff --git a/tests/pico/stock_nightly_no_std/Cargo.toml b/tests/pico/stock_nightly_no_std/Cargo.toml new file mode 100644 index 0000000..c00518f --- /dev/null +++ b/tests/pico/stock_nightly_no_std/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "addition_no_std" +edition = "2021" + +[dependencies] + +[workspace] diff --git a/tests/pico/stock_nightly_no_std/src/main.rs b/tests/pico/stock_nightly_no_std/src/main.rs new file mode 100644 index 0000000..920e413 --- /dev/null +++ b/tests/pico/stock_nightly_no_std/src/main.rs @@ -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 pico_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 = 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!"); + } +} diff --git a/tests/pico/stock_nightly_no_std/src/pico_rt.rs b/tests/pico/stock_nightly_no_std/src/pico_rt.rs new file mode 100644 index 0000000..ff58b2c --- /dev/null +++ b/tests/pico/stock_nightly_no_std/src/pico_rt.rs @@ -0,0 +1,136 @@ +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(); + + syscall_halt(0); + unreachable!() +} + +/// Halts the program with the given exit code. +/// According to https://github.com/brevis-network/pico/blob/v1.1.7/sdk/sdk/src/riscv_ecalls/halt.rs#L15 +/// TODO: Check what `coprocessor` feature does and integrate here. +/// Contex https://github.com/brevis-network/pico/blob/v1.1.7/sdk/sdk/src/riscv_ecalls/halt.rs#L18 +#[allow(unused_variables)] +pub extern "C" fn syscall_halt(exit_code: u8) -> ! { + unsafe { + core::arch::asm!( + "ecall", + in("t0") 0x00_00_00_00, + in("a0") exit_code + ); + 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; + +// https://docs.succinct.xyz/docs/sp1/security/rv32im-implementation#reserved-memory-regions +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) +{} \ No newline at end of file