Support building openvm guest program with stock rust compiler. Add compilation and execution unit tests.

WiP. Works but needs cleanup
This commit is contained in:
rodiazet
2025-08-26 11:34:22 +02:00
parent 0db60b1467
commit f708b27105
8 changed files with 386 additions and 49 deletions

3
Cargo.lock generated
View File

@@ -2303,7 +2303,9 @@ dependencies = [
name = "ere-openvm"
version = "0.0.11"
dependencies = [
"anyhow",
"build-utils",
"cargo_metadata 0.19.2",
"openvm-build",
"openvm-circuit",
"openvm-continuations",
@@ -2314,6 +2316,7 @@ dependencies = [
"test-utils",
"thiserror 2.0.12",
"toml",
"tracing",
"zkvm-interface",
]

View File

@@ -6,8 +6,11 @@ rust-version.workspace = true
license.workspace = true
[dependencies]
anyhow.workspace = true
serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true
cargo_metadata.workspace = true
tracing.workspace = true
toml.workspace = true
# OpenVM dependencies

View File

@@ -0,0 +1,139 @@
use std::fs;
use crate::error::CompileError;
use std::path::{Path};
use std::process::{Command};
use cargo_metadata::MetadataCommand;
use tracing::info;
use openvm_sdk::config::{AppConfig, SdkVmConfig, DEFAULT_APP_LOG_BLOWUP, DEFAULT_LEAF_LOG_BLOWUP};
use openvm_stark_sdk::config::FriParameters;
use crate::OpenVMProgram;
static CARGO_ENCODED_RUSTFLAGS_SEPARATOR: &str = "\x1f";
pub fn compile_openvm_program_stock_rust(
guest_directory: &Path,
toolchain: &String,
) -> Result<OpenVMProgram, 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 target_name = "riscv32ima-unknown-none-elf";
let plus_toolchain = format!("+{}", toolchain);
let args = [
plus_toolchain.as_str(),
"build",
"--target",
target_name,
"--release",
// For bare metal we have to build core and alloc
"-Zbuild-std=core,alloc",
];
let rust_flags = [
"-C",
"passes=lower-atomic", // Only for rustc > 1.81
"-C",
// Start of the code section
"link-arg=-Ttext=0x00201000",
"-C",
// The lowest memory location that will be used when your program is loaded
"link-arg=--image-base=0x00200800",
"-C",
"panic=abort",
"--cfg",
"getrandom_backend=\"custom\"",
"-C",
"llvm-args=-misched-prera-direction=bottomup",
"-C",
"llvm-args=-misched-postra-direction=bottomup",
];
let encoded_rust_flags = rust_flags
.into_iter()
.collect::<Vec<_>>()
.join(CARGO_ENCODED_RUSTFLAGS_SEPARATOR);
let result = Command::new("cargo")
.current_dir(guest_directory)
.env("CARGO_ENCODED_RUSTFLAGS", &encoded_rust_flags)
.args(args)
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.map_err(|source| CompileError::OpenVMBuildFailure {
source: source.into(),
crate_path: guest_directory.to_path_buf()
});
if result.is_err() {
return Err(result.err().unwrap());
}
let elf_path =
guest_directory
.join("target")
.join(target_name)
.join("release")
.join(&package.name);
let elf = fs::read(&elf_path).map_err(|e| CompileError::ReadElfFailed {
path: elf_path,
source: e,
})?;
let app_config_path = guest_directory.join("openvm.toml");
let app_config = if app_config_path.exists() {
let toml = fs::read_to_string(&app_config_path).map_err(|source| {
CompileError::ReadConfigFailed {
source,
path: app_config_path.to_path_buf(),
}
})?;
toml::from_str(&toml).map_err(CompileError::DeserializeConfigFailed)?
} else {
// The default `AppConfig` copied from https://github.com/openvm-org/openvm/blob/ca36de3/crates/cli/src/default.rs#L31.
AppConfig {
app_fri_params: FriParameters::standard_with_100_bits_conjectured_security(
DEFAULT_APP_LOG_BLOWUP,
)
.into(),
// By default it supports RISCV32IM with IO but no precompiles.
app_vm_config: SdkVmConfig::builder()
.system(Default::default())
.rv32i(Default::default())
.rv32m(Default::default())
.io(Default::default())
.build(),
leaf_fri_params: FriParameters::standard_with_100_bits_conjectured_security(
DEFAULT_LEAF_LOG_BLOWUP,
)
.into(),
compiler_options: Default::default(),
}
};
info!("Openvm program compiled (toolchain {}) OK - {} bytes", toolchain, elf.len());
Ok(OpenVMProgram { elf, app_config })
}
#[cfg(test)]
mod tests {
use test_utils::host::testing_guest_directory;
use crate::compile_stock_rust::compile_openvm_program_stock_rust;
#[test]
fn test_stock_compiler_impl() {
let guest_directory = testing_guest_directory(
"openvm",
"stock_nightly_no_std");
let program = compile_openvm_program_stock_rust(&guest_directory, &"nightly".to_string()).unwrap();
assert!(!program.elf.is_empty(), "ELF bytes should not be empty.");
}
}

View File

@@ -34,6 +34,12 @@ pub enum OpenVMError {
pub enum CompileError {
#[error("Failed to build guest, code: {0}")]
BuildFailed(i32),
#[error("`openvm` build failure for {crate_path} failed: {source}")]
OpenVMBuildFailure {
#[source]
source: anyhow::Error,
crate_path: PathBuf,
},
#[error("Guest building skipped (OPENVM_SKIP_BUILD is set)")]
BuildSkipped,
#[error("Missing to find unique elf: {0}")]
@@ -44,6 +50,10 @@ pub enum CompileError {
ReadConfigFailed { source: io::Error, path: PathBuf },
#[error("Failed to deserialize OpenVM's config file: {0}")]
DeserializeConfigFailed(toml::de::Error),
#[error("`cargo metadata` failed: {0}")]
MetadataCommand(#[from] cargo_metadata::Error),
#[error("Could not find `[package].name` in guest Cargo.toml at {path}")]
MissingPackageName { path: PathBuf },
}
#[derive(Debug, Error)]

View File

@@ -1,6 +1,7 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
use crate::error::{CommonError, CompileError, ExecuteError, OpenVMError, ProveError, VerifyError};
use crate::compile_stock_rust::compile_openvm_program_stock_rust;
use openvm_build::GuestOptions;
use openvm_circuit::arch::instructions::exe::VmExe;
use openvm_continuations::verifier::internal::types::VmStarkProof;
@@ -14,12 +15,14 @@ use openvm_sdk::{
use openvm_stark_sdk::{config::FriParameters, openvm_stark_backend::p3_field::PrimeField32};
use openvm_transpiler::{elf::Elf, openvm_platform::memory::MEM_SIZE};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::{fs, io::Read, path::Path, sync::Arc, time::Instant};
use std::{env, fs, io::Read, path::Path, sync::Arc, 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;
@@ -37,58 +40,67 @@ impl Compiler for OPENVM_TARGET {
type Program = OpenVMProgram;
// Inlining `openvm_sdk::Sdk::build` in order to get raw elf bytes.
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
let pkg = openvm_build::get_package(guest_directory);
let guest_opts = GuestOptions::default().with_profile("release".to_string());
let target_dir = match openvm_build::build_guest_package(&pkg, &guest_opts, None, &None) {
Ok(target_dir) => target_dir,
Err(Some(code)) => return Err(CompileError::BuildFailed(code))?,
Err(None) => return Err(CompileError::BuildSkipped)?,
};
let elf_path = openvm_build::find_unique_executable(guest_directory, target_dir, &None)
.map_err(|e| CompileError::UniqueElfNotFound(e.into()))?;
let elf = fs::read(&elf_path).map_err(|source| CompileError::ReadElfFailed {
source,
path: elf_path.to_path_buf(),
})?;
let app_config_path = guest_directory.join("openvm.toml");
let app_config = if app_config_path.exists() {
let toml = fs::read_to_string(&app_config_path).map_err(|source| {
CompileError::ReadConfigFailed {
source,
path: app_config_path.to_path_buf(),
}
})?;
toml::from_str(&toml).map_err(CompileError::DeserializeConfigFailed)?
} else {
// The default `AppConfig` copied from https://github.com/openvm-org/openvm/blob/ca36de3/crates/cli/src/default.rs#L31.
AppConfig {
app_fri_params: FriParameters::standard_with_100_bits_conjectured_security(
DEFAULT_APP_LOG_BLOWUP,
)
.into(),
// By default it supports RISCV32IM with IO but no precompiles.
app_vm_config: SdkVmConfig::builder()
.system(Default::default())
.rv32i(Default::default())
.rv32m(Default::default())
.io(Default::default())
.build(),
leaf_fri_params: FriParameters::standard_with_100_bits_conjectured_security(
DEFAULT_LEAF_LOG_BLOWUP,
)
.into(),
compiler_options: Default::default(),
}
};
Ok(OpenVMProgram { elf, app_config })
let toolchain =
env::var("ERE_GUEST_TOOLCHAIN").unwrap_or_else(|_error| "openvm".into());
match toolchain.as_str() {
"openvm" => Ok(compile_openvm_program(guest_directory)?),
_ => Ok(compile_openvm_program_stock_rust(guest_directory, &toolchain)?),
}
}
}
// Inlining `openvm_sdk::Sdk::build` in order to get raw elf bytes.
fn compile_openvm_program(guest_directory: &Path) -> Result<OpenVMProgram, OpenVMError> {
let pkg = openvm_build::get_package(guest_directory);
let guest_opts = GuestOptions::default().with_profile("release".to_string());
let target_dir = match openvm_build::build_guest_package(&pkg, &guest_opts, None, &None) {
Ok(target_dir) => target_dir,
Err(Some(code)) => return Err(CompileError::BuildFailed(code))?,
Err(None) => return Err(CompileError::BuildSkipped)?,
};
let elf_path = openvm_build::find_unique_executable(guest_directory, target_dir, &None)
.map_err(|e| CompileError::UniqueElfNotFound(e.into()))?;
let elf = fs::read(&elf_path).map_err(|source| CompileError::ReadElfFailed {
source,
path: elf_path.to_path_buf(),
})?;
let app_config_path = guest_directory.join("openvm.toml");
let app_config = if app_config_path.exists() {
let toml = fs::read_to_string(&app_config_path).map_err(|source| {
CompileError::ReadConfigFailed {
source,
path: app_config_path.to_path_buf(),
}
})?;
toml::from_str(&toml).map_err(CompileError::DeserializeConfigFailed)?
} else {
// The default `AppConfig` copied from https://github.com/openvm-org/openvm/blob/ca36de3/crates/cli/src/default.rs#L31.
AppConfig {
app_fri_params: FriParameters::standard_with_100_bits_conjectured_security(
DEFAULT_APP_LOG_BLOWUP,
)
.into(),
// By default it supports RISCV32IM with IO but no precompiles.
app_vm_config: SdkVmConfig::builder()
.system(Default::default())
.rv32i(Default::default())
.rv32m(Default::default())
.io(Default::default())
.build(),
leaf_fri_params: FriParameters::standard_with_100_bits_conjectured_security(
DEFAULT_LEAF_LOG_BLOWUP,
)
.into(),
compiler_options: Default::default(),
}
};
Ok(OpenVMProgram { elf, app_config })
}
pub struct EreOpenVM {
app_config: AppConfig<SdkVmConfig>,
app_exe: Arc<VmExe<F>>,
@@ -246,6 +258,7 @@ mod tests {
use test_utils::host::{
BasicProgramIo, Io, run_zkvm_execute, run_zkvm_prove, testing_guest_directory,
};
use crate::compile_stock_rust::compile_openvm_program_stock_rust;
fn basic_program() -> OpenVMProgram {
static PROGRAM: OnceLock<OpenVMProgram> = OnceLock::new();
@@ -278,6 +291,14 @@ mod tests {
assert_eq!(io.deserialize_outputs(&zkvm, &public_values), io.outputs());
}
#[test]
fn test_execute_nightly() {
let guest_directory = testing_guest_directory("openvm", "stock_nightly_no_std");
let program = compile_openvm_program_stock_rust(&guest_directory, &"nightly".to_string()).unwrap();
let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap();
run_zkvm_execute(&zkvm, &Input::new());
}
#[test]
fn test_execute_invalid_inputs() {
let zkvm = basic_program_ere_openvm();

View File

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

View File

@@ -0,0 +1,25 @@
#![no_std]
#![no_main]
extern crate alloc;
use alloc::vec::Vec;
use core::sync::atomic::Ordering;
use core::sync::atomic::AtomicU16;
mod openvm_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,129 @@
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();
terminate();
unreachable!()
}
#[inline(always)]
fn terminate() {
unsafe {
core::arch::asm!(
".insn 4, 0x000b"
)
}
}
// 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)
{}