Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,515 changes: 1,454 additions & 61 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ openvm-benchmarks-prove = { workspace = true, default-features = false }
openvm-benchmarks-utils = { workspace = true, default-features = false }
openvm-circuit.workspace = true
openvm-sdk = { workspace = true, default-features = false }
openvm-native-recursion = { workspace = true, default-features = false }

alloy-provider = { workspace = true }
alloy-rpc-client.workspace = true
Expand Down Expand Up @@ -60,6 +59,7 @@ regex = "1.11.1"
sysinfo = { workspace = true, features = ["system"] }
bytesize.workspace = true
url.workspace = true
axiom-sdk = { git = "https://github.com/axiom-crypto/axiom-api-cli.git", tag = "v1.0.0" }

[dev-dependencies]
glob = "0.3"
Expand Down
167 changes: 167 additions & 0 deletions crates/integration/src/axiom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use serde::{Deserialize, Serialize};
use std::{env, path::Path};
use super::TaskProver;
use scroll_zkvm_types::{ProvingTask, proof::ProofEnum, axiom};
use axiom_sdk::prove as axiom_prove;

/// Default Axiom API base URL
pub const DEFAULT_AXIOM_BASE_URL: &str = "https://api.axiom.xyz";

/// Simple blocking client for Axiom Proofs API
pub struct AxiomClient {
base_url: String,
api_key: String,
program_id: String,
}

#[derive(thiserror::Error, Debug)]
pub enum AxiomError {
#[error("http error: {0}")]
Http(String),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
}

impl TaskProver for AxiomClient {
fn name(&self) -> &str { &self.program_id}
fn get_vk(&mut self) -> Vec<u8> {
unimplemented!();
}
fn prove_task(&mut self, t: &ProvingTask, gen_snark: bool) -> eyre::Result<ProofEnum> {
axiom_prove::ProveSdk::
unimplemented!();
}
}

impl AxiomClient {
/// Create a new client
pub fn new(base_url: impl Into<String>) -> Self {
let api_key = env::var("AXIOM_API_KEY").expect("AXIOM_API_KEY env var is required");
let program_id = env::var("AXIOM_PROGRAM_ID").expect("AXIOM_PROGRAM_ID env var is required");
Self { base_url: base_url.into(), api_key, program_id }
}

/// Generate a new proof via POST /v1/proofs
///
/// - program_id: UUID of the program to prove
/// - proof_type: Optional proof type ("stark" | "evm"). Defaults to "stark" if None.
/// - witness: array of byte slices, each encoded as a single input hex string prefixed with 0x01
/// - fields: array of u32 slices, each encoded as a single input hex string (u32 little-endian) prefixed with 0x02
///
/// Returns the proof request id on success.
pub fn generate_proof(
&self,
program_id: &str,
proof_type: Option<&str>,
witness: &[&[u8]],
fields: &[&[u32]],
) -> Result<String, AxiomError> {
let mut inputs: Vec<String> = Vec::with_capacity(witness.len() + fields.len());

// Encode witness entries: 0x01 | bytes
for w in witness {
inputs.push(encode_witness(w));
}

// Encode fields entries: 0x02 | u32 (little-endian bytes)
for f in fields {
inputs.push(encode_fields(f));
}

let body = ProofRequest { input: inputs };

let url = format!("{}/v1/proofs", self.base_url.trim_end_matches('/'));
let client = reqwest::blocking::Client::new();

// Build query
let mut query: Vec<(&str, &str)> = vec![("program_id", program_id)];
if let Some(pt) = proof_type { query.push(("proof_type", pt)); }

let resp = client
.post(url)
.header("Axiom-API-Key", &self.api_key)
.query(&query)
.json(&body)
.send()?;

if resp.status().is_success() {
let pr: ProofResponse = resp.json()?;
Ok(pr.id)
} else {
let status = resp.status();
let text = resp.text().unwrap_or_default();
Err(AxiomError::Http(format!("status {}: {}", status, text)))
}
}
}

#[derive(Serialize)]
struct ProofRequest {
input: Vec<String>,
}

#[derive(Deserialize)]
struct ProofResponse {
id: String,
}

fn encode_witness(w: &[u8]) -> String {
let mut buf = Vec::with_capacity(1 + w.len());
buf.push(0x01);
buf.extend_from_slice(w);
format!("0x{}", hex::encode(buf))
}

fn encode_fields(f: &[u32]) -> String {
let mut buf = Vec::with_capacity(1 + f.len() * 4);
buf.push(0x02);
for &v in f {
buf.extend_from_slice(&v.to_le_bytes());
}
format!("0x{}", hex::encode(buf))
}

// Custom deserializer: decode hex string (no 0x prefix) into bytes.
fn hex_to_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
hex::decode(&s).map_err(serde::de::Error::custom)
}

#[derive(serde::Deserialize)]
struct AxiomStarkProof {
#[serde(deserialize_with = "hex_to_bytes")]
proof: Vec<u8>,
#[serde(deserialize_with = "hex_to_bytes")]
user_public_values: Vec<u8>,
version: String,
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_encode_witness() {
let w = b"ABC"; // 41 42 43
let s = encode_witness(w);
assert_eq!(s, "0x01414243");
}

#[test]
fn test_encode_fields() {
let f: &[u32] = &[0x1, 0xA0B0C0D0];
let s = encode_fields(f);
// 0x02 | 01 00 00 00 | D0 C0 B0 A0
assert_eq!(s, "0x0201000000d0c0b0a0");
}

#[test]
fn test_decode_axiom_stark_proof_from_file() {
let _ = scroll_zkvm_types::proof::StarkProof::read_from_axiom_cloud(Path::new("./testdata/axiom/stark-proof.json")).unwrap();

}
}
74 changes: 52 additions & 22 deletions crates/integration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use scroll_zkvm_prover::{
utils::{read_json, vm::ExecutionResult, write_json},
};
use scroll_zkvm_types::{
ProvingTask as UniversalProvingTask,
proof::{EvmProof, ProofEnum, StarkProof},
public_inputs::ForkName,
types_agg::ProgramCommitment,
Expand All @@ -25,6 +26,8 @@ pub mod testers;

pub mod utils;

mod axiom;

/// Directory to store proofs on disc.
const DIR_PROOFS: &str = "proofs";

Expand Down Expand Up @@ -82,7 +85,7 @@ pub trait PartialProvingTask: serde::Serialize {

fn legacy_rkyv_archive(&self) -> eyre::Result<Vec<u8>>;

fn write_guest_input(&self, stdin: &mut StdIn) -> eyre::Result<()>
fn archive(&self) -> eyre::Result<Vec<u8>>
where
Self: Sized,
{
Expand All @@ -93,8 +96,7 @@ pub trait PartialProvingTask: serde::Serialize {
bincode::serde::encode_to_vec(self, config)?
}
};
stdin.write_bytes(&bytes);
Ok(())
Ok(bytes)
}
}

Expand Down Expand Up @@ -185,22 +187,50 @@ pub trait ProverTester {
path_proof
}

fn build_universal_task<'a>(
witness: &Self::Witness,
aggregated_proofs: impl Iterator<Item = &'a StarkProof>,
) -> eyre::Result<UniversalProvingTask> {

Ok(UniversalProvingTask {
serialized_witness: vec![witness.archive()?],
aggregated_proofs: aggregated_proofs.cloned().collect(),
fork_name: witness.fork_name().as_str().to_string(),
identifier: witness.identifier(),
vk: Default::default(),
})
}

fn build_guest_input<'a>(
witness: &Self::Witness,
aggregated_proofs: impl Iterator<Item = &'a StarkProof>,
) -> eyre::Result<StdIn> {
use openvm_native_recursion::hints::Hintable;
use scroll_zkvm_prover::task::ProvingTask;
Ok(Self::build_universal_task(witness, aggregated_proofs)?.build_guest_input())
}
}

pub trait TaskProver {
fn name(&self) -> &str;
fn prove_task(&mut self, t: &UniversalProvingTask, gen_snark: bool) -> eyre::Result<ProofEnum>;
fn get_vk(&mut self) -> Vec<u8>;
}

let mut stdin = StdIn::default();
witness.write_guest_input(&mut stdin)?;
impl TaskProver for Prover {
fn name(&self) -> &str {self.prover_name.as_str()}

for proof in aggregated_proofs {
let streams = proof.proofs[0].write();
for s in &streams {
stdin.write_field(s);
}
fn get_vk(&mut self) -> Vec<u8> { self.get_app_vk() }

fn prove_task(&mut self, t: &UniversalProvingTask, gen_snark: bool) -> eyre::Result<ProofEnum>{
use scroll_zkvm_prover::task::ProvingTask;
let stdin = t.build_guest_input();
if !gen_snark {
// gen stark proof
Ok(self.gen_proof_stark(stdin)?.into())
} else {
let proof: EvmProof = self.gen_proof_snark(stdin)?.into();
Ok(proof.into())
}
Ok(stdin)
}
}

Expand Down Expand Up @@ -285,9 +315,9 @@ pub fn tester_execute<T: ProverTester>(
}

/// End-to-end test for proving witnesses of the same prover.
#[instrument(name = "prove_verify", skip_all, fields(task_id, prover_name = prover.prover_name))]
#[instrument(name = "prove_verify", skip_all, fields(task_id, prover_name = prover.name()))]
pub fn prove_verify<T: ProverTester>(
prover: &mut Prover,
prover: &mut impl TaskProver,
witness: &T::Witness,
proofs: &[ProofEnum],
) -> eyre::Result<ProofEnum> {
Expand All @@ -298,7 +328,7 @@ pub fn prove_verify<T: ProverTester>(
.join(T::DIR_ASSETS)
.join(DIR_PROOFS);
std::fs::create_dir_all(&cache_dir)?;
let vk = prover.get_app_vk();
let vk = prover.get_vk();

// Try reading proof from cache if available, and early return in that case.
let task_id = witness.identifier();
Expand All @@ -310,14 +340,14 @@ pub fn prove_verify<T: ProverTester>(
tracing::debug!(name: "early_return_proof", ?task_id);
proof
} else {
let stdin = T::build_guest_input(
let task = T::build_universal_task(
witness,
proofs
.iter()
.map(|p| p.as_stark_proof().expect("must be stark proof")),
)?;
// Construct stark proof for the circuit.
let proof = prover.gen_proof_stark(stdin)?.into();
let proof = prover.prove_task(&task, false)?;
write_json(&path_proof, &proof)?;
tracing::debug!(name: "cached_proof", ?task_id);

Expand All @@ -337,7 +367,7 @@ pub fn prove_verify<T: ProverTester>(
/// End-to-end test for a single proving task to generate an EVM-verifiable SNARK proof.
#[instrument(name = "prove_verify_single_evm", skip_all)]
pub fn prove_verify_single_evm<T>(
prover: &mut Prover,
prover: &mut impl TaskProver,
witness: &T::Witness,
proofs: &[ProofEnum],
) -> eyre::Result<ProofEnum>
Expand Down Expand Up @@ -370,20 +400,20 @@ where
tracing::debug!(name: "early_return_evm_proof", ?task_id);
proof
} else {
let stdin = T::build_guest_input(
let task = T::build_universal_task(
witness,
proofs
.iter()
.map(|p| p.as_stark_proof().expect("must be stark proof")),
)?;
// Construct stark proof for the circuit.
let proof: EvmProof = prover.gen_proof_snark(stdin)?.into();
let proof = prover.prove_task(&task, true)?;
write_json(&path_proof, &proof)?;
tracing::debug!(name: "cached_evm_proof", ?task_id);
proof.into()
proof
};

let vk = prover.get_app_vk();
let vk = prover.get_vk();
// Verify proof.
verifier.verify_evm_proof(
&proof
Expand Down
5 changes: 5 additions & 0 deletions crates/integration/testdata/axiom/stark-proof.json

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions crates/prover/src/prover/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ impl Prover {
let task_id = task.identifier();
tracing::debug!(name: "generate_root_verifier_input", task_id);

let stdin = task
.build_guest_input()
.map_err(|e| Error::GenProof(e.to_string()))?;
let stdin = task.build_guest_input();

// Generate a new proof.
let proof = if !with_snark {
Expand Down
12 changes: 6 additions & 6 deletions crates/prover/src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use scroll_zkvm_types::{public_inputs::ForkName, task::ProvingTask as UniversalP
pub trait ProvingTask: serde::de::DeserializeOwned {
fn identifier(&self) -> String;

fn build_guest_input_inner(&self, stdin: &mut StdIn) -> Result<(), rkyv::rancor::Error>;
fn build_guest_input_inner(&self, stdin: &mut StdIn);

fn build_guest_input(&self) -> Result<StdIn, rkyv::rancor::Error> {
fn build_guest_input(&self) -> StdIn {
let mut stdin = StdIn::default();
self.build_guest_input_inner(&mut stdin)?;
Ok(stdin)
self.build_guest_input_inner(&mut stdin);
stdin
}

fn fork_name(&self) -> ForkName;
Expand All @@ -25,7 +25,7 @@ impl ProvingTask for UniversalProvingTask {
self.identifier.clone()
}

fn build_guest_input_inner(&self, stdin: &mut StdIn) -> Result<(), rkyv::rancor::Error> {
fn build_guest_input_inner(&self, stdin: &mut StdIn) {
for witness in &self.serialized_witness {
stdin.write_bytes(witness);
}
Expand All @@ -36,7 +36,7 @@ impl ProvingTask for UniversalProvingTask {
stdin.write_field(s);
}
}
Ok(())

}

fn fork_name(&self) -> ForkName {
Expand Down
Loading