Complete ere-pico (#139)

This commit is contained in:
Han
2025-09-18 10:28:58 +08:00
committed by GitHub
parent 7ef4598594
commit 9804776022
10 changed files with 289 additions and 106 deletions

39
Cargo.lock generated
View File

@@ -3696,9 +3696,11 @@ dependencies = [
"bincode 1.3.3",
"build-utils",
"cargo_metadata 0.19.2",
"pico-sdk",
"p3-field 0.1.0 (git+https://github.com/brevis-network/Plonky3.git?rev=a4d376b)",
"pico-vm",
"serde",
"sha2",
"tempfile",
"test-utils",
"thiserror 2.0.12",
"zkvm-interface",
@@ -9628,41 +9630,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "pico-patch-libs"
version = "1.1.6"
source = "git+https://github.com/brevis-network/pico.git?tag=v1.1.7#79b10e613c3a0dd2a92d2a65a149d853f4aface2"
dependencies = [
"bincode 1.3.3",
"serde",
]
[[package]]
name = "pico-sdk"
version = "1.1.6"
source = "git+https://github.com/brevis-network/pico.git?tag=v1.1.7#79b10e613c3a0dd2a92d2a65a149d853f4aface2"
dependencies = [
"anyhow",
"bincode 1.3.3",
"cfg-if",
"env_logger",
"getrandom 0.2.16",
"hex",
"lazy_static",
"log",
"p3-baby-bear 0.1.0 (git+https://github.com/brevis-network/Plonky3.git?rev=a4d376b)",
"p3-challenger 0.1.0 (git+https://github.com/brevis-network/Plonky3.git?rev=a4d376b)",
"p3-field 0.1.0 (git+https://github.com/brevis-network/Plonky3.git?rev=a4d376b)",
"p3-koala-bear 0.1.0 (git+https://github.com/brevis-network/Plonky3.git?rev=a4d376b)",
"p3-mersenne-31 0.1.0 (git+https://github.com/brevis-network/Plonky3.git?rev=a4d376b)",
"pico-patch-libs",
"pico-vm",
"rand 0.8.5",
"serde",
"serde_json",
"sha2",
]
[[package]]
name = "pico-vm"
version = "1.1.6"

View File

@@ -78,8 +78,8 @@ openvm-stark-sdk = { git = "https://github.com/openvm-org/stark-backend.git", ta
openvm-transpiler = { git = "https://github.com/openvm-org/openvm.git", tag = "v1.4.0" }
# Pico dependencies
pico-p3-field = { git = "https://github.com/brevis-network/Plonky3.git", package = "p3-field", rev = "a4d376b" }
pico-vm = { git = "https://github.com/brevis-network/pico.git", tag = "v1.1.7" }
pico-sdk = { git = "https://github.com/brevis-network/pico.git", tag = "v1.1.7" }
# Risc0 dependencies
risc0-build = "3.0.3"

View File

@@ -31,7 +31,7 @@ fn generate_zkvm_sdk_version_impl() {
"jolt-sdk",
"nexus-sdk",
"openvm-sdk",
"pico-sdk",
"pico-vm",
"risc0-zkvm",
"sp1-sdk",
"zkm-sdk",

View File

@@ -9,12 +9,14 @@ license.workspace = true
anyhow.workspace = true
bincode.workspace = true
cargo_metadata.workspace = true
sha2.workspace = true
serde.workspace = true
tempfile.workspace = true
thiserror.workspace = true
# Pico dependencies
pico-p3-field.workspace = true
pico-vm.workspace = true
pico-sdk.workspace = true
# Local dependencies
zkvm-interface.workspace = true

View File

@@ -1,5 +1,5 @@
use build_utils::detect_and_generate_name_and_sdk_version;
fn main() {
detect_and_generate_name_and_sdk_version("pico", "pico-sdk");
detect_and_generate_name_and_sdk_version("pico", "pico-vm");
}

View File

@@ -0,0 +1,99 @@
// Copied and modified from https://github.com/brevis-network/pico/blob/v1.1.7/sdk/sdk/src/client.rs.
// The `EmbedProver` is removed because we don't need the proof to be verified
// on chain. Issue for tracking: https://github.com/eth-act/ere/issues/140.
use anyhow::{Error, Ok, Result};
use pico_vm::{
compiler::riscv::program::Program,
configs::{config::StarkGenericConfig, stark_config::KoalaBearPoseidon2},
emulator::stdin::EmulatorStdinBuilder,
instances::compiler::shapes::{
recursion_shape::RecursionShapeConfig, riscv_shape::RiscvShapeConfig,
},
machine::proof,
proverchain::{
CombineProver, CompressProver, ConvertProver, InitialProverSetup, MachineProver,
ProverChain, RiscvProver,
},
};
use zkvm_interface::PublicValues;
pub type SC = KoalaBearPoseidon2;
pub type MetaProof = proof::MetaProof<SC>;
pub struct ProverClient {
riscv: RiscvProver<SC, Program>,
convert: ConvertProver<SC, SC>,
combine: CombineProver<SC, SC>,
compress: CompressProver<SC, SC>,
}
impl ProverClient {
pub fn new(elf: &[u8]) -> Self {
let riscv = RiscvProver::new_initial_prover(
(SC::new(), elf),
Default::default(),
Some(RiscvShapeConfig::default()),
);
let convert = ConvertProver::new_with_prev(
&riscv,
Default::default(),
Some(RecursionShapeConfig::default()),
);
let combine = CombineProver::new_with_prev(
&convert,
Default::default(),
Some(RecursionShapeConfig::default()),
);
let compress = CompressProver::new_with_prev(&combine, (), None);
Self {
riscv,
convert,
combine,
compress,
}
}
pub fn new_stdin_builder(&self) -> EmulatorStdinBuilder<Vec<u8>, SC> {
EmulatorStdinBuilder::default()
}
/// Execute the program and return the cycles and public values
pub fn execute(&self, stdin: EmulatorStdinBuilder<Vec<u8>, SC>) -> (u64, Vec<u8>) {
let (stdin, _) = stdin.finalize();
self.riscv.emulate(stdin)
}
/// Prove until `CompressProver`.
pub fn prove(
&self,
stdin: EmulatorStdinBuilder<Vec<u8>, SC>,
) -> Result<(PublicValues, MetaProof), Error> {
let (stdin, _) = stdin.finalize();
let riscv_proof = self.riscv.prove(stdin);
if !self.riscv.verify(&riscv_proof.clone(), self.riscv.vk()) {
return Err(Error::msg("verify riscv proof failed"));
}
let proof = self.convert.prove(riscv_proof.clone());
if !self.convert.verify(&proof, self.riscv.vk()) {
return Err(Error::msg("verify convert proof failed"));
}
let proof = self.combine.prove(proof);
if !self.combine.verify(&proof, self.riscv.vk()) {
return Err(Error::msg("verify combine proof failed"));
}
let proof = self.compress.prove(proof);
if !self.compress.verify(&proof, self.riscv.vk()) {
return Err(Error::msg("verify compress proof failed"));
}
Ok((riscv_proof.pv_stream.clone().unwrap_or_default(), proof))
}
/// Verify a compressed proof.
pub fn verify(&self, proof: &MetaProof) -> Result<(), Error> {
if !self.compress.verify(proof, self.riscv.vk()) {
return Err(Error::msg("verify compress proof failed"));
}
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
use crate::error::PicoError;
use crate::error::CompileError;
use cargo_metadata::MetadataCommand;
use std::fs;
use std::path::Path;
@@ -38,18 +38,18 @@ const CARGO_ARGS: &[&str] = &[
pub fn compile_pico_program_stock_rust(
guest_directory: &Path,
toolchain: &String,
) -> Result<Vec<u8>, PicoError> {
) -> Result<Vec<u8>, CompileError> {
compile_program_stock_rust(guest_directory, toolchain)
}
fn compile_program_stock_rust(
guest_directory: &Path,
toolchain: &String,
) -> Result<Vec<u8>, PicoError> {
) -> Result<Vec<u8>, CompileError> {
let metadata = MetadataCommand::new().current_dir(guest_directory).exec()?;
let package = metadata
.root_package()
.ok_or_else(|| PicoError::MissingPackageName {
.ok_or_else(|| CompileError::MissingPackageName {
path: guest_directory.to_path_buf(),
})?;
@@ -76,7 +76,7 @@ fn compile_program_stock_rust(
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.map_err(|source| PicoError::BuildFailure {
.map_err(|source| CompileError::BuildFailure {
source: source.into(),
crate_path: guest_directory.to_path_buf(),
});
@@ -87,7 +87,7 @@ fn compile_program_stock_rust(
let elf_path = target_direcotry.join(&package.name);
fs::read(&elf_path).map_err(|e| PicoError::ReadFile {
fs::read(&elf_path).map_err(|e| CompileError::ReadFile {
path: elf_path,
source: e,
})

View File

@@ -10,22 +10,35 @@ impl From<PicoError> for zkVMError {
#[derive(Debug, Error)]
pub enum PicoError {
#[error(transparent)]
Compile(#[from] CompileError),
#[error(transparent)]
Execute(#[from] ExecuteError),
#[error(transparent)]
Prove(#[from] ProveError),
#[error(transparent)]
Verify(#[from] VerifyError),
}
#[derive(Debug, Error)]
pub enum CompileError {
#[error("Failed to create temporary directory: {0}")]
Tempdir(io::Error),
/// Guest program directory does not exist.
#[error("guest program directory not found: {0}")]
PathNotFound(PathBuf),
/// Failed to spawn or run `cargo pico build`.
#[error("failed to run `cargo pico build`: {0}")]
Spawn(#[from] io::Error),
CargoPicoBuild(#[from] io::Error),
/// `cargo pico build` exited with a non-zero status.
#[error("`cargo pico build` failed with status {status:?}")]
CargoFailed { status: ExitStatus },
CargoPicoBuildFailed { status: ExitStatus },
/// Expected ELF file was not produced.
#[error("ELF file not found at {0}")]
ElfNotFound(PathBuf),
/// Reading the ELF file failed.
#[error("failed to read ELF file at {path}: {source}")]
ReadElf {
@@ -50,3 +63,33 @@ pub enum PicoError {
#[error("`cargo metadata` failed: {0}")]
MetadataCommand(#[from] cargo_metadata::Error),
}
#[derive(Debug, Error)]
pub enum ExecuteError {
#[error("Pico execution failed: {0}")]
Client(anyhow::Error),
}
#[derive(Debug, Error)]
pub enum ProveError {
#[error("Pico proving failed: {0}")]
Client(anyhow::Error),
#[error("Serialising proof with `bincode` failed: {0}")]
Bincode(#[from] bincode::Error),
}
#[derive(Debug, Error)]
pub enum VerifyError {
#[error("Pico verifying failed: {0}")]
Client(anyhow::Error),
#[error("Deserialising proof with `bincode` failed: {0}")]
Bincode(#[from] bincode::Error),
#[error("Invalid base proof length {0}, expected 1")]
InvalidBaseProofLength(usize),
#[error("Invalid public values length {0}, expected at least 32")]
InvalidPublicValuesLength(usize),
#[error("First 32 public values are expected in byte")]
InvalidPublicValues,
#[error("Public values digest are expected in bytes")]
InvalidPublicValuesDigest,
}

View File

@@ -1,20 +1,32 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
use compile_stock_rust::compile_pico_program_stock_rust;
use pico_sdk::client::DefaultProverClient;
use crate::{
client::{MetaProof, ProverClient},
compile_stock_rust::compile_pico_program_stock_rust,
error::{CompileError, PicoError, ProveError, VerifyError},
};
use pico_p3_field::PrimeField32;
use pico_vm::{configs::stark_config::KoalaBearPoseidon2, emulator::stdin::EmulatorStdinBuilder};
use serde::de::DeserializeOwned;
use std::{env, io::Read, path::Path, process::Command, time::Instant};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sha2::{Digest, Sha256};
use std::{
env, fs,
io::Read,
path::Path,
process::Command,
time::{self, Instant},
};
use tempfile::tempdir;
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 client;
mod compile_stock_rust;
mod error;
use error::PicoError;
#[allow(non_camel_case_types)]
pub struct PICO_TARGET;
@@ -36,32 +48,36 @@ impl Compiler for PICO_TARGET {
}
}
fn compile_pico_program(guest_directory: &Path) -> Result<Vec<u8>, PicoError> {
fn compile_pico_program(guest_directory: &Path) -> Result<Vec<u8>, CompileError> {
let tempdir = tempdir().map_err(CompileError::Tempdir)?;
// 1. Check guest path
if !guest_directory.exists() {
return Err(PicoError::PathNotFound(guest_directory.to_path_buf()));
return Err(CompileError::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<io::Error> → Spawn
.args(["pico", "build", "--output-directory"])
.arg(tempdir.path())
.status()
.map_err(CompileError::CargoPicoBuild)?;
if !status.success() {
return Err(PicoError::CargoFailed { status });
return Err(CompileError::CargoPicoBuildFailed { status });
}
// 3. Locate the ELF file
let elf_path = guest_directory.join("elf/riscv32im-pico-zkvm-elf");
let elf_path = tempdir.path().join("riscv32im-pico-zkvm-elf");
if !elf_path.exists() {
return Err(PicoError::ElfNotFound(elf_path));
return Err(CompileError::ElfNotFound(elf_path));
}
// 4. Read the ELF file
let elf_bytes = std::fs::read(&elf_path).map_err(|e| PicoError::ReadElf {
let elf_bytes = fs::read(&elf_path).map_err(|e| CompileError::ReadElf {
path: elf_path,
source: e,
})?;
@@ -69,29 +85,38 @@ fn compile_pico_program(guest_directory: &Path) -> Result<Vec<u8>, PicoError> {
Ok(elf_bytes)
}
#[derive(Serialize, Deserialize)]
pub struct PicoProofWithPublicValues {
proof: MetaProof,
public_values: Vec<u8>,
}
pub struct ErePico {
program: <PICO_TARGET as Compiler>::Program,
}
impl ErePico {
pub fn new(
program_bytes: <PICO_TARGET as Compiler>::Program,
_resource_type: ProverResourceType,
) -> Self {
ErePico {
program: program_bytes,
pub fn new(program: <PICO_TARGET as Compiler>::Program, resource: ProverResourceType) -> Self {
if !matches!(resource, ProverResourceType::Cpu) {
panic!("Network or GPU proving not yet implemented for Pico. Use CPU resource type.");
}
ErePico { program }
}
pub fn client(&self) -> ProverClient {
ProverClient::new(&self.program)
}
}
impl zkVM for ErePico {
fn execute(&self, inputs: &Input) -> Result<(PublicValues, ProgramExecutionReport), zkVMError> {
let client = DefaultProverClient::new(&self.program);
let client = self.client();
let mut stdin = client.new_stdin_builder();
serialize_inputs(&mut stdin, inputs);
let start = Instant::now();
let (total_num_cycles, public_values) = client.emulate(stdin);
let (total_num_cycles, public_values) = client.execute(stdin);
Ok((
public_values,
@@ -107,47 +132,47 @@ impl zkVM for ErePico {
&self,
inputs: &Input,
) -> Result<(PublicValues, Proof, zkvm_interface::ProgramProvingReport), zkVMError> {
let client = DefaultProverClient::new(&self.program);
let client = self.client();
let mut stdin = client.new_stdin_builder();
serialize_inputs(&mut stdin, inputs);
let now = std::time::Instant::now();
let meta_proof = client.prove(stdin).expect("Failed to generate proof");
let now = time::Instant::now();
let (public_values, proof) = client
.prove(stdin)
.map_err(|err| PicoError::Prove(ProveError::Client(err)))?;
let elapsed = now.elapsed();
let mut proof_serialized = Vec::new();
for p in meta_proof.0.proofs().iter() {
bincode::serialize_into(&mut proof_serialized, p).unwrap();
}
for p in meta_proof.1.proofs().iter() {
bincode::serialize_into(&mut proof_serialized, p).unwrap();
}
for p in meta_proof.0.pv_stream.iter() {
bincode::serialize_into(&mut proof_serialized, p).unwrap();
}
for p in meta_proof.1.pv_stream.iter() {
bincode::serialize_into(&mut proof_serialized, p).unwrap();
}
// TODO: Public values
let public_values = Vec::new();
let proof_bytes = bincode::serialize(&PicoProofWithPublicValues {
proof,
public_values: public_values.clone(),
})
.map_err(|err| PicoError::Prove(ProveError::Bincode(err)))?;
Ok((
public_values,
proof_serialized,
proof_bytes,
ProgramProvingReport::new(elapsed),
))
}
fn verify(&self, _proof: &[u8]) -> Result<PublicValues, zkVMError> {
let client = DefaultProverClient::new(&self.program);
let _vk = client.riscv_vk();
// TODO: Verification method missing from sdk
// TODO: Public values
let public_values = Vec::new();
Ok(public_values)
fn verify(&self, proof: &[u8]) -> Result<PublicValues, zkVMError> {
let client = self.client();
let proof: PicoProofWithPublicValues = bincode::deserialize(proof)
.map_err(|err| PicoError::Verify(VerifyError::Bincode(err)))?;
client
.verify(&proof.proof)
.map_err(|err| PicoError::Verify(VerifyError::Client(err)))?;
if extract_public_values_sha256_digest(&proof.proof).map_err(PicoError::Verify)?
!= <[u8; 32]>::from(Sha256::digest(&proof.public_values))
{
return Err(PicoError::Verify(VerifyError::InvalidPublicValuesDigest))?;
}
Ok(proof.public_values)
}
fn name(&self) -> &'static str {
@@ -174,11 +199,36 @@ fn serialize_inputs(stdin: &mut EmulatorStdinBuilder<Vec<u8>, KoalaBearPoseidon2
}
}
/// Extract public values sha256 digest from base proof of compressed proof.
/// The sha256 digest will be placed at the first 32 field elements of the
/// public values of the only base proof.
fn extract_public_values_sha256_digest(proof: &MetaProof) -> Result<[u8; 32], VerifyError> {
if proof.proofs().len() != 1 {
return Err(VerifyError::InvalidBaseProofLength(proof.proofs().len()));
}
if proof.proofs()[0].public_values.len() < 32 {
return Err(VerifyError::InvalidPublicValuesLength(
proof.proofs()[0].public_values.len(),
));
}
Ok(proof.proofs()[0].public_values[..32]
.iter()
.map(|value| u8::try_from(value.as_canonical_u32()))
.collect::<Result<Vec<_>, _>>()
.map_err(|_| VerifyError::InvalidPublicValues)?
.try_into()
.unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
use std::{panic, sync::OnceLock};
use test_utils::host::{BasicProgramIo, run_zkvm_execute, testing_guest_directory};
use test_utils::host::{
BasicProgramIo, run_zkvm_execute, run_zkvm_prove, testing_guest_directory,
};
static BASIC_PROGRAM: OnceLock<Vec<u8>> = OnceLock::new();
@@ -228,7 +278,30 @@ mod tests {
BasicProgramIo::invalid_type,
BasicProgramIo::invalid_data,
] {
panic::catch_unwind(|| zkvm.execute(&inputs_gen()).unwrap_err()).unwrap_err();
panic::catch_unwind(|| zkvm.execute(&inputs_gen())).unwrap_err();
}
}
#[test]
fn test_prove() {
let program = basic_program();
let zkvm = ErePico::new(program, ProverResourceType::Cpu);
let io = BasicProgramIo::valid();
run_zkvm_prove(&zkvm, &io);
}
#[test]
fn test_prove_invalid_inputs() {
let program = basic_program();
let zkvm = ErePico::new(program, ProverResourceType::Cpu);
for inputs_gen in [
BasicProgramIo::empty,
BasicProgramIo::invalid_type,
BasicProgramIo::invalid_data,
] {
panic::catch_unwind(|| zkvm.prove(&inputs_gen())).unwrap_err();
}
}
}

View File

@@ -31,7 +31,6 @@ fn __start(_argc: isize, _argv: *const *const u8) -> isize {
main();
syscall_halt(0);
unreachable!()
}
/// Halts the program with the given exit code.