diff --git a/sz-poc-offsite-2025/Cargo.toml b/sz-poc-offsite-2025/Cargo.toml index 0e54241..c4e3157 100644 --- a/sz-poc-offsite-2025/Cargo.toml +++ b/sz-poc-offsite-2025/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["evm/processor", "evm/sequencer-node", "evm/prover"] +members = ["evm/processor", "evm/sequencer-node", "evm/prover", "evm/lightnode"] resolver = "3" [workspace.package] @@ -10,6 +10,7 @@ edition = "2024" evm-processor = { path = "evm/processor" } evm-sequencer-node = { path = "evm/sequencer-node" } evm-prover = { path = "evm/prover" } +evm-lightnode = { path = "evm/lightnode" } # External eyre = { version = "0.6" } diff --git a/sz-poc-offsite-2025/evm/lightnode/Cargo.toml b/sz-poc-offsite-2025/evm/lightnode/Cargo.toml new file mode 100644 index 0000000..8c31a76 --- /dev/null +++ b/sz-poc-offsite-2025/evm/lightnode/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "evm-lightnode" +edition = { workspace = true } + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +reqwest = { version = "0.11", features = ["blocking", "json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +executor-http-client = { git = "https://github.com/logos-co/nomos", branch = "master" } +tokio = { version = "1", features = ["full"] } +crypto-bigint = { version = "0.5.5", features = ["serde"] } +risc0-zkvm = { version = "2" } +indexmap = { version = "1.9.3" } +url = { version = "2" } +hex = { version = "0.4" } diff --git a/sz-poc-offsite-2025/evm/lightnode/src/lib.rs b/sz-poc-offsite-2025/evm/lightnode/src/lib.rs new file mode 100644 index 0000000..95f26e3 --- /dev/null +++ b/sz-poc-offsite-2025/evm/lightnode/src/lib.rs @@ -0,0 +1,94 @@ +use nomos::{CryptarchiaInfo, HeaderId}; +use reqwest::Url; +use tracing::{error, info}; + +pub const CRYPTARCHIA_INFO: &str = "cryptarchia/info"; +pub const STORAGE_BLOCK: &str = "storage/block"; + +pub mod nomos; +pub mod proofcheck; + +#[derive(Clone, Debug)] +pub struct Credentials { + pub username: String, + pub password: Option, +} + +pub struct NomosClient { + base_url: Url, + reqwest_client: reqwest::Client, + basic_auth: Credentials, +} + +impl NomosClient { + pub fn new(base_url: Url, basic_auth: Credentials) -> Self { + Self { + base_url, + reqwest_client: reqwest::Client::new(), + basic_auth, + } + } + + pub async fn get_cryptarchia_info(&self) -> Result { + let url = self.base_url.join(CRYPTARCHIA_INFO).expect("Invalid URL"); + + info!("Requesting cryptarchia info from {}", url); + let request = self.reqwest_client.get(url).basic_auth( + &self.basic_auth.username, + self.basic_auth.password.as_deref(), + ); + + let response = request.send().await.map_err(|e| { + error!("Failed to send request: {}", e); + "Failed to send request".to_string() + })?; + + if !response.status().is_success() { + error!("Failed to get cryptarchia info: {}", response.status()); + return Err("Failed to get cryptarchia info".to_string()); + } + + let info = response.json::().await.map_err(|e| { + error!("Failed to parse response: {}", e); + "Failed to parse response".to_string() + })?; + Ok(info) + } + + pub async fn get_block(&self, id: HeaderId) -> Result { + let url = self.base_url.join(STORAGE_BLOCK).expect("Invalid URL"); + + info!("Requesting block with HeaderId {}", id); + let request = self + .reqwest_client + .post(url) + .header("Content-Type", "application/json") + .basic_auth( + &self.basic_auth.username, + self.basic_auth.password.as_deref(), + ) + .body(serde_json::to_string(&id).unwrap()); + + let response = request.send().await.map_err(|e| { + error!("Failed to send request: {}", e); + "Failed to send request".to_string() + })?; + + if !response.status().is_success() { + error!("Failed to get block: {}", response.status()); + return Err("Failed to get block".to_string()); + } + + let json: serde_json::Value = response.json().await.map_err(|e| { + error!("Failed to parse JSON: {}", e); + "Failed to parse JSON".to_string() + })?; + + info!( + "Block (raw): {}", + serde_json::to_string_pretty(&json).unwrap() + ); + + Ok(json) + } +} diff --git a/sz-poc-offsite-2025/evm/lightnode/src/main.rs b/sz-poc-offsite-2025/evm/lightnode/src/main.rs new file mode 100644 index 0000000..0b907c2 --- /dev/null +++ b/sz-poc-offsite-2025/evm/lightnode/src/main.rs @@ -0,0 +1,48 @@ +use clap::Parser; +use evm_lightnode::proofcheck; +use url::Url; +use std::path::PathBuf; +use std::error; +use tracing_subscriber::{EnvFilter, fmt}; + +#[derive(Parser, Debug)] +#[clap(author, version, about = "Light Node validator")] +struct Args { + #[clap(long, default_value = "info")] + log_level: String, + + #[clap(long, default_value = "http://localhost:8546")] + rpc: Url, + + #[clap(long, default_value = "http://localhost:8070")] + prover_url: Url, + + #[clap(long)] + start_block: u64, + + #[clap(long, default_value = "10")] + batch_size: u64, + + #[clap(long)] + zeth_binary_dir: Option, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + let filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)); + + fmt::fmt().with_env_filter(filter).with_target(false).init(); + + proofcheck::verify_proof( + args.start_block, + args.batch_size, + &args.rpc, + &args.prover_url, + &args.zeth_binary_dir.unwrap_or_else(|| std::env::current_dir().unwrap()).join("zeth-ethereum"), + ).await?; + + Ok(()) +} diff --git a/sz-poc-offsite-2025/evm/lightnode/src/nomos.rs b/sz-poc-offsite-2025/evm/lightnode/src/nomos.rs new file mode 100644 index 0000000..14cdcd1 --- /dev/null +++ b/sz-poc-offsite-2025/evm/lightnode/src/nomos.rs @@ -0,0 +1,52 @@ +use serde::{Deserialize, Deserializer, Serialize}; + +use hex::FromHex; + +// tip "4f573735fb987453f7467688ea4e034b9161e3ca200526faf5c8ce6db09da180" +// slot 5085 +// height 1245 + +#[derive(Serialize, Deserialize, Debug)] +pub struct CryptarchiaInfo { + pub tip: HeaderId, + pub slot: u64, + pub height: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq, Copy, Hash, PartialOrd, Ord, Default)] +pub struct HeaderId([u8; 32]); + +impl<'de> Deserialize<'de> for HeaderId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let hex_str = String::deserialize(deserializer)?; + + let bytes = <[u8; 32]>::from_hex(hex_str) + .map_err(|e| serde::de::Error::custom(format!("Invalid hex string: {}", e)))?; + + Ok(HeaderId(bytes)) + } +} + +impl Serialize for HeaderId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let hex_str = hex::encode(self.0); + serializer.serialize_str(&hex_str) + } +} + +use std::fmt; + +impl fmt::Display for HeaderId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} diff --git a/sz-poc-offsite-2025/evm/lightnode/src/proofcheck.rs b/sz-poc-offsite-2025/evm/lightnode/src/proofcheck.rs new file mode 100644 index 0000000..a3a3b72 --- /dev/null +++ b/sz-poc-offsite-2025/evm/lightnode/src/proofcheck.rs @@ -0,0 +1,57 @@ +use tokio::process::Command; +use std::path::Path; +use reqwest::Url; +use tracing::{error, info}; + +pub async fn verify_proof( + block_number: u64, + block_count: u64, + rpc: &Url, + prover_url: &Url, + zeth_bin: &Path, +) -> Result<(), String> { + info!( + "Verifying proof for blocks {}-{}", + block_number, + block_number + block_count - 1 + ); + + let url = prover_url.join(&format!( + "/?block_start={}&block_count={}", + block_number, block_count + )).map_err(|e| format!("Failed to construct URL: {}", e))?; + let proof = reqwest::get(url).await + .map_err(|e| format!("Failed to fetch proof: {}", e))? + .bytes() + .await + .map_err(|e| format!("Failed to read proof response: {}", e))?; + let filename = format!("{}-{}.zkp", block_number, block_count + block_number); + tokio::fs::write(&filename, &proof) + .await + .map_err(|e| format!("Failed to write proof to file: {}", e))?; + + + let output = Command::new(zeth_bin) + .args([ + "verify", + &format!("--rpc={}", rpc), + &format!("--block-number={}", block_number), + &format!("--block-count={}", block_count), + &format!("--file={}", filename), + ]) + .output().await + .map_err(|e| format!("Failed to execute zeth-ethereum verify: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + error!("zeth-ethereum verify command failed: {}", stderr); + return Err(format!( + "zeth-ethereum verify command failed with status: {}\nStderr: {}", + output.status, stderr + )); + } + + info!("Proof verification successful"); + + Ok(()) +} diff --git a/sz-poc-offsite-2025/evm/processor/Cargo.toml b/sz-poc-offsite-2025/evm/processor/Cargo.toml index e82fcd9..749cbf3 100644 --- a/sz-poc-offsite-2025/evm/processor/Cargo.toml +++ b/sz-poc-offsite-2025/evm/processor/Cargo.toml @@ -8,4 +8,4 @@ executor-http-client = { git = "https://github.com/logos-co/nomos", branch = "ma reqwest = "0.11" kzgrs-backend = { git = "https://github.com/logos-co/nomos", branch = "master" } bincode = "1" -reth-tracing = { workspace = true } \ No newline at end of file +reth-tracing = { workspace = true } diff --git a/sz-poc-offsite-2025/evm/prover/Cargo.toml b/sz-poc-offsite-2025/evm/prover/Cargo.toml index b27505a..e0210cb 100644 --- a/sz-poc-offsite-2025/evm/prover/Cargo.toml +++ b/sz-poc-offsite-2025/evm/prover/Cargo.toml @@ -3,9 +3,11 @@ name = "evm-prover" edition = { workspace = true } [dependencies] +axum = "0.8.3" clap = { version = "4.5", features = ["derive"] } reqwest = { version = "0.11", features = ["blocking", "json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +tokio = { version = "1.44.2", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/sz-poc-offsite-2025/evm/prover/src/http.rs b/sz-poc-offsite-2025/evm/prover/src/http.rs new file mode 100644 index 0000000..0eca425 --- /dev/null +++ b/sz-poc-offsite-2025/evm/prover/src/http.rs @@ -0,0 +1,39 @@ + +use axum::{ + extract::Query, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde::Deserialize; +use std::path::PathBuf; +use tokio::fs; + +#[derive(Deserialize)] +pub struct ProofRequest { + block_start: u64, + block_count: u64 +} + + +/// Handler for GET / +pub async fn serve_proof(Query(query): Query) -> Response { + let file_name = format!("{}-{}.zkp", query.block_start, query.block_count + query.block_start); + + let path = PathBuf::from(&file_name); + + // Read file contents + match fs::read(&path).await { + Ok(bytes) => ( + StatusCode::OK, + bytes, + ).into_response(), + Err(err) => { + let status = if err.kind() == std::io::ErrorKind::NotFound { + StatusCode::NOT_FOUND + } else { + StatusCode::INTERNAL_SERVER_ERROR + }; + (status, format!("Error reading file: {}", err)).into_response() + } + } +} \ No newline at end of file diff --git a/sz-poc-offsite-2025/evm/prover/src/main.rs b/sz-poc-offsite-2025/evm/prover/src/main.rs index 8694f4e..e3c353f 100644 --- a/sz-poc-offsite-2025/evm/prover/src/main.rs +++ b/sz-poc-offsite-2025/evm/prover/src/main.rs @@ -1,9 +1,15 @@ use clap::Parser; use reqwest::blocking::Client; -use serde_json::{Value, json}; -use std::{path::PathBuf, process::Command, thread, time::Duration}; +use serde_json::{json, Value}; +use std::{path::PathBuf, process::Command, thread, time::Duration, net::SocketAddr}; use tracing::{debug, error, info}; -use tracing_subscriber::{EnvFilter, fmt}; +use tracing_subscriber::{fmt, EnvFilter}; +use axum::{ + routing::get, + Router, +}; + +mod http; #[derive(Parser, Debug)] #[clap(author, version, about = "Ethereum Proof Generation Tool")] @@ -117,6 +123,12 @@ fn get_latest_block(client: &Client, rpc: &str) -> Result { fn main() -> Result<(), Box> { let args = Args::parse(); + std::thread::spawn(move || { + if let Err(e) = run_server() { + error!("Error running server: {}", e); + } + }); + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)); @@ -166,3 +178,20 @@ fn main() -> Result<(), Box> { } } } + + + +#[tokio::main] +async fn run_server() -> Result<(), Box> { + // Build our application with a route + let app = Router::new() + .route("/", get(http::serve_proof)); + + let addr = SocketAddr::from(([127, 0, 0, 1], 8070)); + // Run it on localhost:8070 + tracing::info!("Serving files on http://{}", addr); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); + + Ok(()) +} \ No newline at end of file diff --git a/sz-poc-offsite-2025/evm/sequencer-node/src/main.rs b/sz-poc-offsite-2025/evm/sequencer-node/src/main.rs index 82d5ad2..7762e63 100644 --- a/sz-poc-offsite-2025/evm/sequencer-node/src/main.rs +++ b/sz-poc-offsite-2025/evm/sequencer-node/src/main.rs @@ -1,4 +1,4 @@ -use evm_processor::{Processor, NomosDa, BasicAuthCredentials}; +use evm_processor::{BasicAuthCredentials, NomosDa, Processor}; use futures::TryStreamExt as _; use reth::{ api::{FullNodeTypes, NodePrimitives, NodeTypes}, @@ -25,13 +25,15 @@ where continue; }; info!(committed_chain = ?new.range(), "Received commit"); - processor.process_blocks( - new.inner() - .0 - .clone() - .into_blocks() - .map(reth_ethereum::primitives::RecoveredBlock::into_block), - ).await; + processor + .process_blocks( + new.inner() + .0 + .clone() + .into_blocks() + .map(reth_ethereum::primitives::RecoveredBlock::into_block), + ) + .await; ctx.events .send(ExExEvent::FinishedHeight(new.tip().num_hash())) @@ -57,7 +59,10 @@ fn main() -> eyre::Result<()> { let url = std::env::var("NOMOS_EXECUTOR").unwrap_or(TESTNET_EXECUTOR.to_string()); let user = std::env::var("NOMOS_USER").unwrap_or_default(); let password = std::env::var("NOMOS_PASSWORD").unwrap_or_default(); - let da = NomosDa::new( BasicAuthCredentials::new(user, Some(password)), url::Url::parse(&url).unwrap()); + let da = NomosDa::new( + BasicAuthCredentials::new(user, Some(password)), + url::Url::parse(&url).unwrap(), + ); let processor = Processor::new(da); let handle = Box::pin( builder