diff --git a/Cargo.lock b/Cargo.lock index dacda8d5..193d7a4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -727,10 +727,10 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -770,6 +770,12 @@ dependencies = [ "hex-conservative", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" @@ -1492,7 +1498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -1507,6 +1513,37 @@ dependencies = [ "walkdir", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "der" version = "0.7.10" @@ -1667,7 +1704,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -2035,6 +2072,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "ethlambda-prover-core" +version = "0.1.0" +dependencies = [ + "ethlambda-state-transition", + "ethlambda-types", + "thiserror 2.0.18", +] + [[package]] name = "ethlambda-rpc" version = "0.1.0" @@ -2762,7 +2808,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", "libgit2-sys", "log", @@ -3116,7 +3162,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -3516,6 +3562,59 @@ dependencies = [ "tracing", ] +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4969,7 +5068,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", "log", "netlink-packet-core", @@ -5008,7 +5107,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -5020,7 +5119,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -5190,7 +5289,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.11.1", "objc2", ] @@ -5644,6 +5743,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postcard" version = "1.1.3" @@ -5803,7 +5911,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.11.1", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", @@ -5831,7 +5939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5927,7 +6035,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -5964,7 +6072,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] @@ -6205,27 +6313,27 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] name = "ref-cast" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -6515,7 +6623,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -6803,9 +6911,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" dependencies = [ "base64", "bs58", @@ -6813,6 +6921,7 @@ dependencies = [ "hex", "indexmap 1.9.3", "indexmap 2.14.0", + "jiff", "schemars 0.9.0", "schemars 1.2.2", "serde_core", @@ -6823,9 +6932,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -7180,9 +7289,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -7229,7 +7338,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.11.1", "core-foundation", "system-configuration-sys", ] @@ -7559,7 +7668,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.11.1", "bytes", "futures-util", "http", @@ -8018,7 +8127,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver 1.0.28", @@ -8512,7 +8621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap 2.14.0", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1d41c5a7..b9e8926a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,19 @@ members = [ "crates/net/api", "crates/net/p2p", "crates/net/rpc", + "crates/prover/core", "crates/storage", ] +# NOTE: zkVM guest programs are their own workspaces: they build for the respective +# target ISA via their respective build commands, not for the host, so `cargo {clippy,test} +# --workspace` must not try to compile them. +# +# `ethlambda-prover-sp1` targets the host, but its build script compiles the +# guest ELF and needs the SP1 (`cargo prove`) toolchain, so it is kept out of +# the default `--workspace` build too. +exclude = ["crates/guest-program/sp1", "crates/prover/sp1"] + [workspace.package] authors = ["LambdaClass"] edition = "2024" @@ -64,6 +74,8 @@ ethlambda-network-api = { path = "crates/net/api" } ethlambda-p2p = { path = "crates/net/p2p" } ethlambda-rpc = { path = "crates/net/rpc" } ethlambda-storage = { path = "crates/storage" } +ethlambda-prover-core = { path = "crates/prover/core" } +ethlambda-prover-sp1 = { path = "crates/prover/sp1" } tracing = "0.1" thiserror = "2.0.9" @@ -98,11 +110,17 @@ rand = "0.10" lru = "0.16" rocksdb = "0.24" libc = "0.2" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = [ + "rustls-tls", +] } eyre = "0.6" # Allocator + heap profiling -tikv-jemallocator = { version = "0.6", features = ["stats", "unprefixed_malloc_on_supported_platforms", "profiling"] } +tikv-jemallocator = { version = "0.6", features = [ + "stats", + "unprefixed_malloc_on_supported_platforms", + "profiling", +] } jemalloc_pprof = { version = "0.8", features = ["flamegraph"] } # NOTE: Shadow-simulator builds also replace quinn-udp with the fallback crate diff --git a/crates/blockchain/state_transition/Cargo.toml b/crates/blockchain/state_transition/Cargo.toml index dc26096e..f74efc0f 100644 --- a/crates/blockchain/state_transition/Cargo.toml +++ b/crates/blockchain/state_transition/Cargo.toml @@ -11,9 +11,13 @@ version.workspace = true [dependencies] ethlambda-types.workspace = true -ethlambda-metrics.workspace = true - thiserror.workspace = true + +# Host-only: prometheus (metrics) and tracing have no meaning in the zkVM guest +# and prometheus doesn't build for the target. Included on every target except +# the zkVM guest (`target_os = "zkvm"`), which drops them automatically. +[target.'cfg(not(target_os = "zkvm"))'.dependencies] +ethlambda-metrics.workspace = true tracing.workspace = true [dev-dependencies] diff --git a/crates/blockchain/state_transition/src/lib.rs b/crates/blockchain/state_transition/src/lib.rs index d53b089b..ebd7ebac 100644 --- a/crates/blockchain/state_transition/src/lib.rs +++ b/crates/blockchain/state_transition/src/lib.rs @@ -1,3 +1,7 @@ +// On the zkVM guest (`target_os = "zkvm"`) `info!`/`warn!` become no-ops, so +// imports and bindings that only feed them go unused — silence those there. +#![cfg_attr(target_os = "zkvm", allow(unused_imports, unused_variables))] + use std::collections::{HashMap, HashSet}; use ethlambda_types::{ @@ -8,9 +12,29 @@ use ethlambda_types::{ primitives::{H256, HashTreeRoot as _}, state::{HISTORICAL_ROOTS_LIMIT, JustificationValidators, State}, }; +#[cfg(not(target_os = "zkvm"))] use tracing::{info, warn}; +// for the zkVM guest, tracing is not compiled in, so set the macros to no-ops. +// `info!`/`warn!` call sites stay unchanged. Defined before first use. +#[cfg(target_os = "zkvm")] +macro_rules! info { + ($($t:tt)*) => {{}}; +} +#[cfg(target_os = "zkvm")] +macro_rules! warn { + ($($t:tt)*) => {{}}; +} + pub mod justified_slots_ops; + +// `metrics` is prometheus-backed on the host, but the zkVM guest +// (`target_os = "zkvm"`) has no prometheus, so there it is swapped for a no-op +// stub with identical signatures. Call sites are the same in both builds. +#[cfg(not(target_os = "zkvm"))] +pub mod metrics; +#[cfg(target_os = "zkvm")] +#[path = "metrics_noop.rs"] pub mod metrics; #[derive(Debug, thiserror::Error)] diff --git a/crates/blockchain/state_transition/src/metrics.rs b/crates/blockchain/state_transition/src/metrics.rs index 8df30909..f0816d5b 100644 --- a/crates/blockchain/state_transition/src/metrics.rs +++ b/crates/blockchain/state_transition/src/metrics.rs @@ -2,6 +2,7 @@ use std::sync::LazyLock; +pub use ethlambda_metrics::TimingGuard; use ethlambda_metrics::*; static LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL: LazyLock = LazyLock::new(|| { @@ -30,21 +31,6 @@ static LEAN_FINALIZATIONS_TOTAL: LazyLock = LazyLock::new(|| { .unwrap() }); -/// Increment the slots processed counter by the given amount. -pub fn inc_slots_processed(count: u64) { - LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL.inc_by(count); -} - -/// Increment the attestations processed counter by the given amount. -pub fn inc_attestations_processed(count: u64) { - LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSED_TOTAL.inc_by(count); -} - -/// Increment the finalization counter with the given result. -pub fn inc_finalizations(result: &str) { - LEAN_FINALIZATIONS_TOTAL.with_label_values(&[result]).inc(); -} - static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( "lean_state_transition_time_seconds", @@ -84,6 +70,21 @@ static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock TimingGuard { TimingGuard::new(&LEAN_STATE_TRANSITION_TIME_SECONDS) diff --git a/crates/blockchain/state_transition/src/metrics_noop.rs b/crates/blockchain/state_transition/src/metrics_noop.rs new file mode 100644 index 00000000..20ef0baf --- /dev/null +++ b/crates/blockchain/state_transition/src/metrics_noop.rs @@ -0,0 +1,23 @@ +//! No-op metrics stub for the zkVM guest (`target_os = "zkvm"`), where +//! prometheus is unavailable. Compiled in place of `metrics.rs` +//! every function mirrors its host counterpart but records nothing. + +/// Zero-sized stand-in for `ethlambda_metrics::TimingGuard`; records nothing. +pub struct TimingGuard; + +pub fn inc_slots_processed(_count: u64) {} +pub fn inc_attestations_processed(_count: u64) {} +pub fn inc_finalizations(_result: &str) {} + +pub fn time_state_transition() -> TimingGuard { + TimingGuard +} +pub fn time_slots_processing() -> TimingGuard { + TimingGuard +} +pub fn time_block_processing() -> TimingGuard { + TimingGuard +} +pub fn time_attestations_processing() -> TimingGuard { + TimingGuard +} diff --git a/crates/common/types/src/lib.rs b/crates/common/types/src/lib.rs index 88ba98b9..47a4c7e6 100644 --- a/crates/common/types/src/lib.rs +++ b/crates/common/types/src/lib.rs @@ -6,6 +6,7 @@ pub mod constants; pub mod genesis; pub mod primitives; pub mod state; +pub mod stf; /// Display helper for truncated root hashes (8 hex chars) pub struct ShortRoot<'a>(pub &'a [u8; 32]); diff --git a/crates/common/types/src/stf.rs b/crates/common/types/src/stf.rs new file mode 100644 index 00000000..4244b5e8 --- /dev/null +++ b/crates/common/types/src/stf.rs @@ -0,0 +1,49 @@ +use libssz::{SszDecode, SszEncode}; +use serde::{Deserialize, Serialize}; + +use crate::{block::Block, primitives::H256, state::State}; + +/// Input type for the zkVM's +/// +/// The inputs to the zkVM need to derive serde Serialize/deserialize +/// which is not derived for state (libssz traits) +/// wrapping pre-serialized SSZ bytes sidesteps that part. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StfInput { + state: Vec, + block: Vec, +} + +/// Public values committed by the STF guest program. +/// +/// These bind a proof to one concrete transition: applying the block with +/// `block_root` to the state with `pre_state_root` yields the state with +/// `post_state_root`. A verifier reads these back from the proof without ever +/// seeing the full pre-state or block, and can chain proofs by matching one +/// transition's `post_state_root` to the next's `pre_state_root`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StfPublicValues { + /// `hash_tree_root` of the pre-state. + pub pre_state_root: H256, + /// `hash_tree_root` of the block being applied. + pub block_root: H256, + /// `hash_tree_root` of the post-state (equals `block.state_root`). + pub post_state_root: H256, +} + +impl StfInput { + pub fn new(state: State, block: Block) -> Self { + StfInput { + state: state.to_ssz(), + block: block.to_ssz(), + } + } + + pub fn state(&self) -> State { + State::from_ssz_bytes(&self.state).expect("error decoding State") + } + + pub fn block(&self) -> Block { + Block::from_ssz_bytes(&self.block).expect("error decoding block") + } +} diff --git a/crates/guest-program/sp1/Cargo.toml b/crates/guest-program/sp1/Cargo.toml new file mode 100644 index 00000000..1e2a93c7 --- /dev/null +++ b/crates/guest-program/sp1/Cargo.toml @@ -0,0 +1,15 @@ +# Standalone workspace: this guest builds for a RISC-V zkVM target via +# `cargo prove build`, so it is excluded from the root workspace and cannot +# inherit its `[workspace.package]` / `[workspace.dependencies]` values. +[workspace] + +[package] +name = "zkvm_guest_sp1" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +sp1-zkvm = "6.1.0" +ethlambda-types = { path = "../../common/types" } +ethlambda-state-transition = { path = "../../blockchain/state_transition" } diff --git a/crates/guest-program/sp1/src/main.rs b/crates/guest-program/sp1/src/main.rs new file mode 100644 index 00000000..859fb72a --- /dev/null +++ b/crates/guest-program/sp1/src/main.rs @@ -0,0 +1,29 @@ +#![no_main] +sp1_zkvm::entrypoint!(main); + +use ethlambda_state_transition::state_transition; +use ethlambda_types::{ + primitives::HashTreeRoot, + stf::{StfInput, StfPublicValues}, +}; + +fn main() { + let input: StfInput = sp1_zkvm::io::read(); + let mut state = input.state(); + let block = input.block(); + + // Capture the pre-state and block roots before mutating the state, so the + // committed public values bind the proof to this specific transition. + let pre_state_root = state.hash_tree_root(); + let block_root = block.hash_tree_root(); + + state_transition(&mut state, &block).expect("state transition failed"); + + let public_values = StfPublicValues { + pre_state_root, + block_root, + // state_transition already asserts this equals block.state_root. + post_state_root: state.hash_tree_root(), + }; + sp1_zkvm::io::commit(&public_values); +} diff --git a/crates/prover/core/Cargo.toml b/crates/prover/core/Cargo.toml new file mode 100644 index 00000000..0b0a4ac9 --- /dev/null +++ b/crates/prover/core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ethlambda-prover-core" +authors.workspace = true +edition.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +ethlambda-types.workspace = true +# Mock-chain generation replays real transitions to seal block state roots. +ethlambda-state-transition.workspace = true +thiserror.workspace = true + diff --git a/crates/prover/core/src/lib.rs b/crates/prover/core/src/lib.rs new file mode 100644 index 00000000..11b43661 --- /dev/null +++ b/crates/prover/core/src/lib.rs @@ -0,0 +1,63 @@ +pub use ethlambda_types::stf::{StfInput, StfPublicValues}; + +pub mod mock_chain; + +/// A serialized proof of a single state transition. +/// +/// The bytes depend upon the specific zkVM being used, (SP1ProofWithPublicValues) +/// and carry both the proof and the committed public values, so +/// [`StfProver::verify`] can recover the [`StfPublicValues`] without re-running +/// the transition. +#[derive(Debug, Clone)] +pub struct Proof(pub Vec); + +impl Proof { + /// Borrow the raw proof bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl From> for Proof { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +/// Proves and verifies state-transition executions on a zkVM backend. +/// +/// The methods are `async` because real backends (SP1, RISC0, …) drive an +/// async prover client. This makes the trait non–object-safe, so consumers +/// select a backend by concrete type rather than `Box`. +/// +/// `async_fn_in_trait` is allowed deliberately: we don't constrain the returned +/// futures to `Send`, since backend prover clients don't all guarantee it. +#[allow(async_fn_in_trait)] +pub trait StfProver { + /// Prove that applying the input's block to its pre-state is a valid + /// transition, returning [`Proof`]. + async fn prove(&self, input: &StfInput) -> Result; + + /// Verify a proof and return the public values it commits to. + async fn verify(&self, proof: &Proof) -> Result; + + /// Execute the guest program, without generating the proof. + async fn execute(&self, input: &StfInput) -> Result; +} + +/// Errors raised while proving or verifying a state transition. +#[derive(Debug, thiserror::Error)] +pub enum ProverError { + /// The backend failed to produce a proof. + #[error("proving failed: {0}")] + Prove(String), + /// The proof did not verify, or verification could not run. + #[error("verification failed: {0}")] + Verify(String), + /// A proof or its public values could not be (de)serialized. + #[error("proof (de)serialization failed: {0}")] + Serialization(String), + /// The execution of the guest program failed. + #[error("execution failed: {0}")] + Execute(String), +} diff --git a/crates/prover/core/src/mock_chain.rs b/crates/prover/core/src/mock_chain.rs new file mode 100644 index 00000000..2c526af4 --- /dev/null +++ b/crates/prover/core/src/mock_chain.rs @@ -0,0 +1,115 @@ +//! Deterministic mock chain for driving the zkVM prover end-to-end without a live +//! node or the spec-fixtures (similiar to zeam's) + +use ethlambda_state_transition::{process_block, process_slots}; +use ethlambda_types::{ + attestation::{AggregatedAttestation, AggregationBits, AttestationData}, + block::{Block, BlockBody}, + checkpoint::Checkpoint, + primitives::{H256, HashTreeRoot as _}, + state::{State, Validator}, +}; + +use crate::StfInput; + +pub struct MockTransition { + pub input: StfInput, + pub pre_state_root: H256, + pub block_root: H256, + pub post_state_root: H256, + pub justified_slot: u64, + pub finalized_slot: u64, +} + +/// Build a chain of transitions(includes attestations) from +/// genesis with dummy validators. Guarantees the state transition +/// is accurate and (for `num_blocks >= 2`) that the +/// justification/finalization advance across the chain. +pub fn gen_mock_chain(num_blocks: u64, num_validators: u64) -> Vec { + assert!(num_validators > 0, "need at least one validator"); + + let validators = (0..num_validators) + .map(|index| Validator { + attestation_pubkey: [0u8; 52], + proposal_pubkey: [0u8; 52], + index, + }) + .collect(); + let mut state = State::from_genesis(0, validators); + + let mut chain = Vec::with_capacity(num_blocks as usize); + for slot in 1..=num_blocks { + // parent_root as `process_slots` will leave it: prior header, state_root sealed. + // This is the header root recorded in `historical_block_hashes`, so votes + // that reference it match the chain. + let mut parent_header = state.latest_block_header.clone(); + parent_header.state_root = state.hash_tree_root(); + let parent_root = parent_header.hash_tree_root(); + + // From the second block on, every validator votes the parent as target + // (source = the current latest justified checkpoint). Both roots already + // live in `historical_block_hashes`, so the votes are valid and the 2/3 + // threshold is crossed, justifying the parent. + let attestations: Vec = if slot >= 2 { + let target = Checkpoint { + slot: slot - 1, + root: parent_root, + }; + vec![AggregatedAttestation { + aggregation_bits: all_validators_bits(num_validators), + data: AttestationData { + slot: slot - 1, + head: target, + target, + source: state.latest_justified, + }, + }] + } else { + Vec::new() + }; + let body = BlockBody { + attestations: attestations + .try_into() + .expect("attestation count within block limit"), + }; + + let mut block = Block { + slot, + proposer_index: slot % num_validators, // round-robin + parent_root, + state_root: H256::ZERO, + body, + }; + + // Seal `state_root` by running the transition on a scratch copy + // (`process_block` does not check state_root; only the `state_transition` + // wrapper does). This also advances justification/finalization. + let mut scratch = state.clone(); + process_slots(&mut scratch, slot).expect("process_slots"); + process_block(&mut scratch, &block).expect("process_block"); + block.state_root = scratch.hash_tree_root(); + + chain.push(MockTransition { + pre_state_root: state.hash_tree_root(), + block_root: block.hash_tree_root(), + post_state_root: block.state_root, + justified_slot: scratch.latest_justified.slot, + finalized_slot: scratch.latest_finalized.slot, + input: StfInput::new(state.clone(), block), + }); + + // advance real state for the next block + state = scratch; + } + chain +} + +/// An `AggregationBits` with every validator's bit set. +fn all_validators_bits(num_validators: u64) -> AggregationBits { + let n = num_validators as usize; + let mut bits = AggregationBits::with_length(n).expect("valid bitlist length"); + for i in 0..n { + bits.set(i, true).expect("in-bounds bit"); + } + bits +} diff --git a/crates/prover/sp1/Cargo.toml b/crates/prover/sp1/Cargo.toml new file mode 100644 index 00000000..e74df7da --- /dev/null +++ b/crates/prover/sp1/Cargo.toml @@ -0,0 +1,27 @@ +# Standalone workspace: this crate is excluded from the root workspace because +# its build script needs the SP1 (`cargo prove`) toolchain. +[workspace] + +[package] +name = "ethlambda-prover-sp1" +version = "0.1.0" +edition = "2024" +publish = false + +[[bin]] +name = "prove" +path = "src/bin/prove.rs" + +[dependencies] +sp1-sdk = "6.3.1" +ethlambda-prover-core = { path = "../core" } +ethlambda-types = { path = "../../common/types" } +# Serializes the whole SP1ProofWithPublicValues into `Proof` so verify can +# recover the committed public values. +bincode = "1.3" +# `prove` binary: CLI flags and the async runtime the prover client needs. +clap = { version = "4", features = ["derive"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[build-dependencies] +sp1-build = "6.3.1" diff --git a/crates/prover/sp1/build.rs b/crates/prover/sp1/build.rs new file mode 100644 index 00000000..e61913f7 --- /dev/null +++ b/crates/prover/sp1/build.rs @@ -0,0 +1,14 @@ +use sp1_build::{BuildArgs, build_program_with_args}; + +fn main() { + // Compile the RISC-V guest program so `include_elf!("zkvm_guest_sp1")` can + // embed its ELF. Path is relative to this crate's manifest directory. + // + // `ignore_rust_version` removes the compatibility issues with the toolchain + // being used for ethlambda and the sp1 toolchain + let args = BuildArgs { + ignore_rust_version: true, + ..Default::default() + }; + build_program_with_args("../../guest-program/sp1", args); +} diff --git a/crates/prover/sp1/src/bin/prove.rs b/crates/prover/sp1/src/bin/prove.rs new file mode 100644 index 00000000..076f2719 --- /dev/null +++ b/crates/prover/sp1/src/bin/prove.rs @@ -0,0 +1,129 @@ +//! Mock-chain proving driver for the SP1 state-transition backend. +//! +//! Generates a mock chain and, depending on `--mode`, runs the guest over each +//! transition either via `execute` (no proof) or via `prove` + `verify`, +//! Each block carries attestations that advance the justification/finalization, +//! so the runs exercise the full state transition. + +use clap::{Parser, ValueEnum}; +use ethlambda_prover_core::{ + StfProver, + mock_chain::{MockTransition, gen_mock_chain}, +}; +use ethlambda_prover_sp1::Sp1Prover; +use ethlambda_types::ShortRoot; + +/// Run STF transitions over a mock chain. +#[derive(Parser)] +#[command(name = "prove")] +struct Args { + /// Number of blocks (transitions) in the mock chain. + #[arg(short, long, default_value_t = 4)] + blocks: u64, + /// Number of validators in the genesis set. + #[arg(short = 'n', long, default_value_t = 4)] + validators: u64, + /// Which guest path to run over the chain. + #[arg(long, value_enum, default_value_t = Mode::Prove)] + mode: Mode, +} + +#[derive(Clone, Copy, ValueEnum)] +enum Mode { + /// Run the guest via `execute` only (no proof generated). + Execute, + /// Generate and verify a proof for each transition. + Prove, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + println!( + "Generating mock chain: {} blocks, {} validators", + args.blocks, args.validators + ); + let chain = gen_mock_chain(args.blocks, args.validators); + + println!("Setting up the SP1 prover"); + let prover = Sp1Prover::new().await; + + match args.mode { + Mode::Execute => run_execute(&prover, &chain).await, + Mode::Prove => run_prove(&prover, &chain).await, + } +} + +/// Execute path: run the guest without proving, per transition. +async fn run_execute( + prover: &Sp1Prover, + chain: &[MockTransition], +) -> Result<(), Box> { + let mut prev_post = None; + for (i, t) in chain.iter().enumerate() { + print_transition_header(i + 1, chain.len(), t); + check_chaining(prev_post, t, i + 1); + + print!("Execution started"); + let ev = prover.execute(&t.input).await?; + assert_eq!(ev.pre_state_root, t.pre_state_root); + assert_eq!(ev.block_root, t.block_root); + assert_eq!(ev.post_state_root, t.post_state_root); + println!("Execution completed"); + + prev_post = Some(t.post_state_root); + } + println!("\nExecuted {} transitions over the mock chain", chain.len()); + Ok(()) +} + +/// Proving path: generate and verify a proof, per transition. +async fn run_prove( + prover: &Sp1Prover, + chain: &[MockTransition], +) -> Result<(), Box> { + let mut prev_post = None; + for (i, t) in chain.iter().enumerate() { + print_transition_header(i + 1, chain.len(), t); + check_chaining(prev_post, t, i + 1); + + + let proof = prover.prove(&t.input).await?; + println!("Generated Proof ({} bytes)", proof.as_bytes().len()); + + let pv = prover.verify(&proof).await?; + assert_eq!(pv.pre_state_root, t.pre_state_root); + assert_eq!(pv.block_root, t.block_root); + assert_eq!(pv.post_state_root, t.post_state_root); + println!("Proof verified and the post_state_root is {}", ShortRoot(&pv.post_state_root.0)); + + prev_post = Some(pv.post_state_root); + } + println!( + "\nProved + verified {} transitions over the mock chain", + chain.len() + ); + Ok(()) +} + +fn print_transition_header(n: usize, total: usize, t: &MockTransition) { + println!("\n-- transition {n}/{total} --"); + println!(" pre_state_root : {}", ShortRoot(&t.pre_state_root.0)); + println!(" block_root : {}", ShortRoot(&t.block_root.0)); + println!( + "justified slot ={} finalized slot ={}", + t.justified_slot, t.finalized_slot + ); +} + +/// The previous transition's post-state root must equal this one's pre-state root. +fn check_chaining( + prev_post: Option, + t: &MockTransition, + n: usize, +) { + if let Some(prev) = prev_post { + assert_eq!(prev, t.pre_state_root, "chain broken before transition {n}"); + } +} diff --git a/crates/prover/sp1/src/lib.rs b/crates/prover/sp1/src/lib.rs new file mode 100644 index 00000000..63eab1b7 --- /dev/null +++ b/crates/prover/sp1/src/lib.rs @@ -0,0 +1,88 @@ +use ethlambda_prover_core::{Proof, ProverError, StfInput, StfProver, StfPublicValues}; +use sp1_sdk::{ + Elf, MockProver, ProveRequest, Prover, ProvingKey, SP1ProofWithPublicValues, SP1ProvingKey, + SP1Stdin, SP1VerifyingKey, include_elf, +}; + +const STATE_TRANSITION_ELF: Elf = include_elf!("zkvm_guest_sp1"); +const CYCLE_LIMIT: u64 = 10_000_000; + +/// SP1 prover: proves the STF guest and verifies its proofs. +/// +/// The proving/verifying keys are derived once via [`Sp1Prover::new`] because +/// `setup` is expensive and must not run per proof. +/// [TODO!]: check if using Lazylock might be better +pub struct Sp1Prover { + client: MockProver, + pk: SP1ProvingKey, + vk: SP1VerifyingKey, +} + +impl Sp1Prover { + /// Build the prover once, caching the proving/verifying keys and + /// currently uses the MockProver. + pub async fn new() -> Self { + let client = MockProver::new().await; + // let client = ProverClient::builder().cpu().await; + // `setup` returns only the proving key (fallible); the verifying key is + // derived from it. + let pk = client + .setup(STATE_TRANSITION_ELF) + .await + .expect("failed to set up SP1 proving key"); + let vk = pk.verifying_key().clone(); + Self { client, pk, vk } + } +} + +impl StfProver for Sp1Prover { + async fn prove(&self, input: &StfInput) -> Result { + let mut stdin = SP1Stdin::new(); + stdin.write(input); + + // use `.groth16()` instead of `.compressed()` for real proving and verification + let proof = self + .client + .prove(&self.pk, stdin) + .compressed() + .cycle_limit(CYCLE_LIMIT) + .await + .map_err(|err| ProverError::Prove(err.to_string()))?; + + // Store the whole proof (including public values) so `verify` can + // recover the committed `StfPublicValues`. + let bytes = bincode::serialize(&proof) + .map_err(|err| ProverError::Serialization(err.to_string()))?; + Ok(Proof(bytes)) + } + + async fn verify(&self, proof: &Proof) -> Result { + let mut sp1_proof: SP1ProofWithPublicValues = bincode::deserialize(proof.as_bytes()) + .map_err(|err| ProverError::Serialization(err.to_string()))?; + + // `verify` is synchronous in this SDK and takes an optional status code. + self.client + .verify(&sp1_proof, &self.vk, None) + .map_err(|err| ProverError::Verify(err.to_string()))?; + + // The guest committed `StfPublicValues` via `io::commit`; read it back. + Ok(sp1_proof.public_values.read::()) + } + + async fn execute(&self, input: &StfInput) -> Result { + let mut stdin = SP1Stdin::new(); + stdin.write(input); + + // Runs the guest in the SP1 executor without proving; the future yields + // `(SP1PublicValues, ExecutionReport)`. We ignore the report for now. + let (mut public_values, _report) = self + .client + .execute(STATE_TRANSITION_ELF, stdin) + .cycle_limit(CYCLE_LIMIT) + .await + .map_err(|err| ProverError::Execute(err.to_string()))?; + + // The guest committed `StfPublicValues` via `io::commit`; read it back. + Ok(public_values.read::()) + } +}