Skip to content
Merged
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
301 changes: 278 additions & 23 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ stateless = { git = "https://github.com/paradigmxyz/stateless", rev = "ed189a519

# ere
ere-server-client = { git = "https://github.com/eth-act/ere", tag = "v0.8.1" }
ere-verifier-core = { git = "https://github.com/eth-act/ere", tag = "v0.8.1" }
ere-verifier-zisk = { git = "https://github.com/eth-act/ere", tag = "v0.8.1" }

# ere-guests
ere-guests-stateless-validator-common = { git = "https://github.com/eth-act/ere-guests", tag = "v0.9.0", package = "stateless-validator-common" }
Expand Down
5 changes: 4 additions & 1 deletion crates/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ path = "src/bin/zkboost.rs"
workspace = true

[features]
default = []
default = ["verifier-zisk"]
otel = [
"dep:opentelemetry",
"dep:opentelemetry-otlp",
"dep:opentelemetry_sdk",
"dep:tracing-opentelemetry",
"ere-server-client/otel",
]
verifier-zisk = ["dep:ere-verifier-core", "dep:ere-verifier-zisk"]

[dependencies]
anyhow.workspace = true
Expand Down Expand Up @@ -65,6 +66,8 @@ stateless.workspace = true

# ere
ere-server-client.workspace = true
ere-verifier-core = { workspace = true, optional = true }
ere-verifier-zisk = { workspace = true, optional = true }

# ere-guests
ere-guests-stateless-validator-common.workspace = true
Expand Down
24 changes: 22 additions & 2 deletions crates/server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ impl Config {
"proof_timeout_secs must be > 0 for {proof_type}"
);
}
zkVMConfig::Verifier { program_vk_url, .. } => {
ensure!(
!program_vk_url.is_empty(),
"program_vk_url must be set for verifier-only zkvm {proof_type}"
);
}
}
if let zkVMConfig::Mock {
mock_proving_time,
Expand Down Expand Up @@ -168,7 +174,8 @@ pub enum MockProvingTime {
},
}

/// zkVM backend configuration, either a remote ere-server or a mock for testing.
/// zkVM backend configuration, either a remote ere-server, a mock, or an
/// in-process verifier-only backend (no proving).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
#[allow(non_camel_case_types)]
Expand Down Expand Up @@ -200,13 +207,26 @@ pub enum zkVMConfig {
#[serde(default)]
mock_failure: bool,
},
/// In-process verifier-only backend. Verifies proofs received via HTTP
/// without running an `ere-server` or pre-loading prover circuits.
/// Returns an error on prove requests.
Verifier {
/// Proof type.
proof_type: ProofType,
/// URL or local path to the program verifying key file (.vk) for the
/// guest program of this proof type. Pre-computed and shipped in
/// `eth-act/ere-guests` releases alongside the .elf.
program_vk_url: String,
},
}

impl zkVMConfig {
/// Returns the proof type identifier for this configuration.
pub fn proof_type(&self) -> ProofType {
match self {
Self::Ere { proof_type, .. } | Self::Mock { proof_type, .. } => *proof_type,
Self::Ere { proof_type, .. }
| Self::Mock { proof_type, .. }
| Self::Verifier { proof_type, .. } => *proof_type,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/server/src/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! (dispatched to per-zkVM worker), and completed (cached in LRU, broadcast via SSE).

pub mod input;
pub mod verifier;
pub mod worker;
pub mod zkvm;

Expand Down
89 changes: 89 additions & 0 deletions crates/server/src/proof/verifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//! In-process verifier-only backends.
//!
//! Wraps the per-zkVM `ere-verifier-*` crates so zkboost can verify proofs
//! without a remote `ere-server` (which loads the full prover circuit). Each
//! verifier is bound to a specific compiled guest program via its
//! `program_vk`, downloaded from the URL configured for that proof_type.

use anyhow::Context;
#[cfg(feature = "verifier-zisk")]
use ere_verifier_core::{PublicValues, codec::Decode, zkVMVerifier};
use zkboost_types::ProofType;

/// Per-zkVM verifier dispatch. Variants are feature-gated.
#[allow(non_camel_case_types)]
pub(crate) enum DynVerifier {
/// In-process ZisK verifier.
#[cfg(feature = "verifier-zisk")]
Zisk(ere_verifier_zisk::ZiskVerifier),
}

impl std::fmt::Debug for DynVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "verifier-zisk")]
Self::Zisk(_) => f.write_str("DynVerifier::Zisk"),
#[cfg(not(any(feature = "verifier-zisk")))]
_ => f.write_str("DynVerifier::<empty>"),
}
}
}

impl DynVerifier {
/// Construct a verifier for the given proof_type by downloading the
/// program verifying key from `url` and decoding it.
pub(crate) async fn from_url(proof_type: ProofType, url: &str) -> anyhow::Result<Self> {
let bytes = download_program_vk(url).await?;
Self::from_bytes(proof_type, &bytes)
}

fn from_bytes(proof_type: ProofType, bytes: &[u8]) -> anyhow::Result<Self> {
match proof_type {
#[cfg(feature = "verifier-zisk")]
ProofType::EthrexZisk | ProofType::RethZisk => {
let program_vk = ere_verifier_zisk::ZiskProgramVk::decode_from_slice(bytes)
.with_context(|| format!("decode ZiskProgramVk for {proof_type}"))?;
Ok(Self::Zisk(ere_verifier_zisk::ZiskVerifier::new(program_vk)))
}
#[allow(unreachable_patterns)]
_ => anyhow::bail!(
"no in-process verifier compiled in for proof_type {proof_type}; \
enable the matching `verifier-*` feature on `zkboost-server`"
),
}
}

/// Verify a serialized proof and return its public values.
pub(crate) async fn verify(&self, proof: &[u8]) -> anyhow::Result<Vec<u8>> {
match self {
#[cfg(feature = "verifier-zisk")]
Self::Zisk(verifier) => {
let proof = ere_verifier_zisk::ZiskProof::decode_from_slice(proof)
.context("decode ZiskProof")?;
let public_values: PublicValues = verifier
.verify(&proof)
.map_err(|error| anyhow::anyhow!("zisk verify failed: {error}"))?;
Ok(Vec::<u8>::from(public_values))
}
}
}
}

async fn download_program_vk(url: &str) -> anyhow::Result<Vec<u8>> {
if let Some(path) = url
.strip_prefix("file://")
.or_else(|| if url.contains("://") { None } else { Some(url) })
{
return std::fs::read(path).with_context(|| format!("read program_vk from {path}"));
}
let bytes = reqwest::get(url)
.await
.with_context(|| format!("GET {url}"))?
.error_for_status()
.with_context(|| format!("status from {url}"))?
.bytes()
.await
.with_context(|| format!("body from {url}"))?
.to_vec();
Ok(bytes)
}
46 changes: 41 additions & 5 deletions crates/server/src/proof/zkvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use zkboost_types::{ElKind, Hash256, ProofType};

use crate::{
config::{MockProvingTime, zkVMConfig},
proof::input::NewPayloadRequestWithWitness,
proof::{input::NewPayloadRequestWithWitness, verifier::DynVerifier},
};

#[derive(Debug, thiserror::Error)]
Expand All @@ -38,7 +38,7 @@ pub(crate) enum zkVMError {
PublicValuesMismatch,
}

/// zkVM instance, either a remote ere-server or a mock.
/// zkVM instance: remote ere-server, in-process mock, or in-process verifier-only.
#[allow(non_camel_case_types)]
#[derive(Clone, Debug)]
pub(crate) enum zkVMInstance {
Expand All @@ -60,6 +60,15 @@ pub(crate) enum zkVMInstance {
/// Mock zkVM implementation.
vm: MockzkVM,
},
/// In-process verifier-only backend. No `ere-server`, no prover circuit
/// loaded — just the lightweight `ere-verifier-*` for this proof type.
/// Returns an error on prove requests.
Verifier {
/// Proof type identifier.
proof_type: ProofType,
/// Verifier implementation, dispatched per proof_type.
verifier: Arc<DynVerifier>,
},
}

impl zkVMInstance {
Expand Down Expand Up @@ -106,6 +115,20 @@ impl zkVMInstance {
*mock_failure,
),
}),
zkVMConfig::Verifier {
proof_type,
program_vk_url,
} => {
let verifier = DynVerifier::from_url(*proof_type, program_vk_url)
.await
.with_context(|| {
format!("init in-process verifier for {proof_type} from {program_vk_url}")
})?;
Ok(Self::Verifier {
proof_type: *proof_type,
verifier: Arc::new(verifier),
})
}
}
}

Expand All @@ -119,6 +142,9 @@ impl zkVMInstance {
.prove(new_payload_request_with_witness.stateless_input())
.await;
}
if let Self::Verifier { proof_type, .. } = self {
anyhow::bail!("prove not supported for verifier-only zkvm {proof_type}");
}

let el_kind = self.proof_type().el_kind();
let input = new_payload_request_with_witness.to_zkvm_input(el_kind)?;
Expand All @@ -127,7 +153,7 @@ impl zkVMInstance {
let (_, proof, _) = client.prove(input).await?;
Ok(proof.0)
}
Self::Mock { .. } => unreachable!(),
Self::Mock { .. } | Self::Verifier { .. } => unreachable!(),
}
}

Expand All @@ -137,7 +163,7 @@ impl zkVMInstance {
new_payload_request_root: Hash256,
proof: Vec<u8>,
) -> Result<(), zkVMError> {
let public_values = match self {
let public_values: PublicValues = match self {
Self::Ere { client, .. } => client
.verify(EncodedProof(proof))
.await
Expand All @@ -146,6 +172,11 @@ impl zkVMInstance {
.verify(&proof)
.await
.map_err(|error| zkVMError::VerificationFailed(error.to_string())),
Self::Verifier { verifier, .. } => verifier
.verify(&proof)
.await
.map(PublicValues::from)
.map_err(|error| zkVMError::VerificationFailed(error.to_string())),
}?;

let expected = expected_public_values(new_payload_request_root)
Expand All @@ -166,14 +197,19 @@ impl zkVMInstance {
/// Returns the proof type identifier for this instance.
pub(crate) fn proof_type(&self) -> ProofType {
match self {
Self::Ere { proof_type, .. } | Self::Mock { proof_type, .. } => *proof_type,
Self::Ere { proof_type, .. }
| Self::Mock { proof_type, .. }
| Self::Verifier { proof_type, .. } => *proof_type,
}
}

/// Returns the proof timeout for this instance.
/// Verifier-only backends never prove, so the timeout is irrelevant — we
/// return the default to keep the signature uniform.
pub(crate) fn proof_timeout(&self) -> Duration {
match self {
Self::Ere { proof_timeout, .. } | Self::Mock { proof_timeout, .. } => *proof_timeout,
Self::Verifier { .. } => Duration::from_secs(12),
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions crates/server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ impl zkBoostServer {

let mut worker_input_txs = HashMap::new();
for zkvm in self.zkvms.values() {
// Verifier-only backends don't prove, so they get no worker. Prove
// requests for those proof_types are dropped at the dispatch layer.
if matches!(zkvm, zkVMInstance::Verifier { .. }) {
continue;
}
let (worker_input_tx, worker_input_rx) = mpsc::channel(CHANNEL_CAPACITY);
worker_input_txs.insert(zkvm.proof_type(), worker_input_tx);
handles.push(tokio::spawn(worker::run_worker(
Expand Down
Loading