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
4,621 changes: 4,080 additions & 541 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ members = [
"testing/execution_engine_integration",
"testing/node_test_rig",
"testing/proof_engine",
"testing/proof_engine_zkboost",
"testing/simulator",
"testing/state_transition_vectors",
"testing/validator_test_rig",
Expand Down Expand Up @@ -185,6 +186,7 @@ malloc_utils = { path = "common/malloc_utils" }
maplit = "1"
merkle_proof = { path = "consensus/merkle_proof" }
metrics = { path = "common/metrics" }
metrics-exporter-prometheus = "0.16"
milhouse = { version = "0.9", default-features = false, features = ["context_deserialize"] }
mockall = "0.13"
mockall_double = "0.3"
Expand Down Expand Up @@ -215,7 +217,6 @@ reqwest = { version = "0.12", default-features = false, features = [
"json",
"stream",
"rustls-tls",
"native-tls-vendored",
] }
reqwest-eventsource = "0.6"
ring = "0.17"
Expand Down Expand Up @@ -280,6 +281,8 @@ workspace_members = { path = "common/workspace_members" }
xdelta3 = { git = "https://github.com/sigp/xdelta3-rs", rev = "4db64086bb02e9febb584ba93b9d16bb2ae3825a" }
zeroize = { version = "1", features = ["zeroize_derive", "serde"] }
zip = { version = "6.0", default-features = false, features = ["deflate"] }
zkboost-server = { git = "https://github.com/eth-act/zkboost", branch = "master" }
zkboost-types = { git = "https://github.com/eth-act/zkboost", branch = "master" }
zstd = "0.13"

[profile.maxperf]
Expand Down
4 changes: 3 additions & 1 deletion beacon_node/execution_layer/src/eip8025/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ pub use proof_engine::HttpProofEngine;
pub use proof_node_client::{
HttpProofNodeClient, PROOF_ENGINE_TIMEOUT, ProofNodeClient, ProofRequestResponse,
};
pub use types::{ProofComplete, ProofEvent, ProofEventInfo, ProofFailure, SseEventParts};
pub use types::{
ProofComplete, ProofEvent, ProofEventInfo, ProofFailure, ProofType, SseEventParts,
};
31 changes: 24 additions & 7 deletions beacon_node/execution_layer/src/eip8025/proof_node_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::pin::Pin;
use std::time::Duration;
use tokio_stream::StreamExt;

use super::types::{ProofEvent, SseEventParts};
use super::types::{ProofEvent, ProofType, SseEventParts};
use types::Hash256;
use types::execution::eip8025::{ProofAttributes, ProofStatus};

Expand Down Expand Up @@ -140,17 +140,28 @@ impl HttpProofNodeClient {

#[async_trait::async_trait]
impl ProofNodeClient for HttpProofNodeClient {
/// `POST /v1/execution_proof_requests?proof_types=0,1,2`
/// `POST /v1/execution_proof_requests?proof_types=reth-sp1,ethrex-risc0`
///
/// Converts EIP-8025 `u8` proof types to string identifiers
/// for the wire format.
async fn request_proofs(
&self,
ssz_body: Vec<u8>,
proof_attributes: ProofAttributes,
) -> Result<Hash256, ProofEngineError> {
// Convert u8 proof types to string identifiers.
// proof node expects: `proof_types=reth-sp1,ethrex-risc0`
let proof_types_csv = proof_attributes
.proof_types
.iter()
.map(|t| ProofType::from_u8(*t).map(|pt| pt.as_str().to_string()))
.collect::<Result<Vec<_>, _>>()?
.join(",");

let response: ProofRequestResponse = self
.client
.post(self.url(PATH_PROOF_REQUESTS))
// TODO: Should this be wrapped in a `ProofAttributes` struct instead of just passing the proof types as a query param?
.query(&[(QUERY_PROOF_TYPES, &proof_attributes.proof_types)])
.query(&[(QUERY_PROOF_TYPES, &proof_types_csv)])
.header(HEADER_CONTENT_TYPE, HEADER_VALUE_SSZ)
.body(ssz_body)
.send()
Expand All @@ -162,19 +173,22 @@ impl ProofNodeClient for HttpProofNodeClient {
Ok(response.new_payload_request_root)
}

/// `POST /v1/execution_proof_verifications?new_payload_request_root=...&proof_type=...`
/// `POST /v1/execution_proof_verifications?new_payload_request_root=...&proof_type=reth-sp1`
///
/// Converts the `u8` proof type to a string identifier for the query param.
async fn verify_proof(
&self,
root: Hash256,
proof_type: u8,
proof_data: &[u8],
) -> Result<ProofStatus, ProofEngineError> {
let proof_type_str = ProofType::from_u8(proof_type)?;
let response: ProofVerificationResponse = self
.client
.post(self.url(PATH_PROOF_VERIFICATIONS))
.query(&[
(QUERY_NEW_PAYLOAD_REQUEST_ROOT, &root.to_string()),
(QUERY_PROOF_TYPE, &proof_type.to_string()),
(QUERY_PROOF_TYPE, &proof_type_str.to_string()),
])
.header(HEADER_CONTENT_TYPE, HEADER_VALUE_SSZ)
.body(proof_data.to_vec())
Expand All @@ -191,10 +205,13 @@ impl ProofNodeClient for HttpProofNodeClient {
}

/// `GET /v1/execution_proofs/{root}/{proof_type}`
///
/// Uses string identifier in the URL path (e.g. `/reth-sp1`).
async fn get_proof(&self, root: Hash256, proof_type: u8) -> Result<Bytes, ProofEngineError> {
let proof_type_str = ProofType::from_u8(proof_type)?;
Ok(self
.client
.get(self.url(&format!("{PATH_PROOFS}/{root}/{proof_type}")))
.get(self.url(&format!("{PATH_PROOFS}/{root}/{proof_type_str}")))
.send()
.await?
.error_for_status()?
Expand Down
162 changes: 158 additions & 4 deletions beacon_node/execution_layer/src/eip8025/types.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,134 @@
//! API types for EIP-8025 proof engine communication.
//!
//! This module contains the SSE event types broadcast by the proof engine.
//! This module contains:
//! - [`ProofType`]: an independent string enum that mirrors the
//! proof node API's `ProofType` exactly.
//! - SSE event types broadcast by the proof engine.
//!
//! ## ProofType encoding
//!
//! EIP-8025 uses `u8` for `ProofType` in SSZ containers (consensus layer).
//! The proof node API uses kebab-case string identifiers
//! (`"reth-sp1"`, `"ethrex-risc0"`, etc.) in HTTP query params, URL paths,
//! and SSE event payloads.
//!
//! [`ProofType`] bridges this gap: the [`HttpProofNodeClient`] converts
//! between `u8` (internal) and string (wire) at the HTTP boundary.

use super::errors::ProofEngineError;
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
use std::str::FromStr;
use types::Hash256;

// ─── ProofType ─────────────────────────────────────────────────────────────

/// Proof type identifiers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
#[repr(u8)]
pub enum ProofType {
EthrexRisc0 = 0,
EthrexSP1 = 1,
EthrexZisk = 2,
RethOpenVM = 3,
RethRisc0 = 4,
RethSP1 = 5,
RethZisk = 6,
}

impl ProofType {
/// Canonical string representation, matching exactly.
pub fn as_str(&self) -> &'static str {
match self {
Self::EthrexRisc0 => "ethrex-risc0",
Self::EthrexSP1 => "ethrex-sp1",
Self::EthrexZisk => "ethrex-zisk",
Self::RethOpenVM => "reth-openvm",
Self::RethRisc0 => "reth-risc0",
Self::RethSP1 => "reth-sp1",
Self::RethZisk => "reth-zisk",
}
}

/// Convert from EIP-8025 `u8` proof type to a string identifier.
///
/// The mapping follows the order defined in the `ProofType` enum.
pub fn from_u8(value: u8) -> Result<Self, ProofEngineError> {
match value {
0 => Ok(Self::EthrexRisc0),
1 => Ok(Self::EthrexSP1),
2 => Ok(Self::EthrexZisk),
3 => Ok(Self::RethOpenVM),
4 => Ok(Self::RethRisc0),
5 => Ok(Self::RethSP1),
6 => Ok(Self::RethZisk),
_ => Err(ProofEngineError::InvalidProofType(format!(
"no mapping for proof type {value}"
))),
}
}

/// Convert back to EIP-8025 `u8` proof type.
pub fn to_u8(self) -> u8 {
self as u8
}

/// All known proof type variants.
pub fn all() -> &'static [ProofType] {
&[
Self::EthrexRisc0,
Self::EthrexSP1,
Self::EthrexZisk,
Self::RethOpenVM,
Self::RethRisc0,
Self::RethSP1,
Self::RethZisk,
]
}
}

impl FromStr for ProofType {
type Err = ProofEngineError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ethrex-risc0" => Ok(Self::EthrexRisc0),
"ethrex-sp1" => Ok(Self::EthrexSP1),
"ethrex-zisk" => Ok(Self::EthrexZisk),
"reth-openvm" => Ok(Self::RethOpenVM),
"reth-risc0" => Ok(Self::RethRisc0),
"reth-sp1" => Ok(Self::RethSP1),
"reth-zisk" => Ok(Self::RethZisk),
_ => Err(ProofEngineError::InvalidProofType(format!(
"unknown proof type: {s}"
))),
}
}
}

impl fmt::Display for ProofType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}

impl From<ProofType> for String {
fn from(pt: ProofType) -> Self {
pt.as_str().to_string()
}
}

impl TryFrom<String> for ProofType {
type Error = ProofEngineError;

fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}

// ─── SSE Event Types ────────────────────────────────────────────────────────

/// SSE event types broadcast by the proof engine.
#[derive(Debug, Clone, PartialEq)]
pub enum ProofEvent {
Expand All @@ -19,27 +143,57 @@ pub enum ProofEvent {
}

/// Payload for a successful proof event.
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ProofComplete {
pub new_payload_request_root: Hash256,
#[serde(deserialize_with = "deserialize_proof_type")]
pub proof_type: u8,
}

/// Payload for a failed proof event.
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ProofFailure {
pub new_payload_request_root: Hash256,
#[serde(deserialize_with = "deserialize_proof_type")]
pub proof_type: u8,
pub error: String,
}

/// Common info for timeout events.
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ProofEventInfo {
pub new_payload_request_root: Hash256,
#[serde(deserialize_with = "deserialize_proof_type")]
pub proof_type: u8,
}

/// Deserialize `proof_type` from either a string (`"reth-sp1"`) or a
/// numeric value (`0`). This allows Lighthouse to consume SSE events from both
/// servers (string format) and test mocks (numeric format).
fn deserialize_proof_type<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum ProofTypeValue {
Number(u8),
String(String),
}

match ProofTypeValue::deserialize(deserializer)? {
ProofTypeValue::Number(n) => Ok(n),
ProofTypeValue::String(s) => {
// Try parsing as string identifier first.
if let Ok(pt) = s.parse::<ProofType>() {
return Ok(pt.to_u8());
}
// Fall back to parsing as numeric string (e.g. "0").
s.parse::<u8>().map_err(serde::de::Error::custom)
}
}
}

/// SSE event name + JSON data pair used to construct a [`ProofEvent`].
pub struct SseEventParts<'a>(pub &'a str, pub &'a str);

Expand Down
25 changes: 25 additions & 0 deletions testing/proof_engine_zkboost/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "proof_engine_zkboost_test"
version = "0.1.0"
edition.workspace = true

[dependencies]
anyhow = { workspace = true }
axum = { workspace = true }
bytes = { workspace = true }
execution_layer = { workspace = true }
futures = { workspace = true }
metrics-exporter-prometheus = { workspace = true }
reqwest = { workspace = true }
sensitive_url = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true }
types = { workspace = true }
url = { workspace = true }
zkboost-server = { workspace = true }
zkboost-types = { workspace = true }
Loading
Loading