Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6efa2b3
feat: adding state transition function input type
Sahilgill24 Jul 21, 2026
a1754ed
feat: scaffold sp1 guest program
Sahilgill24 Jul 21, 2026
4b61b80
refactor: update guest program workspace and add public values
Sahilgill24 Jul 21, 2026
da701e1
feat: setup the prover core for zkvm proving and verification
Sahilgill24 Jul 22, 2026
97b2ad4
feat: setup the prover backend for sp1
Sahilgill24 Jul 22, 2026
88ad3fb
feat: add zkVM target for guest program and its dependencies
Sahilgill24 Jul 26, 2026
f5b7ac6
refactor: separate no-op module targetting zkvm for metrics
Sahilgill24 Jul 28, 2026
cb54b4e
feat: add the execute path to the Prover trait and sp1 prover
Sahilgill24 Jul 30, 2026
84e65dc
test: test the execute path for the sp1 prover
Sahilgill24 Jul 30, 2026
3190c45
Merge upstream-main into feat/zk-stf-proving
Sahilgill24 Aug 4, 2026
fc625d4
feat: add mock chain driver for zkVM prover Backend
Sahilgill24 Aug 4, 2026
f4575b4
feat: add binary to run zkVM prover for the mock chain
Sahilgill24 Aug 4, 2026
6cb3b2b
Merge branch 'upstream/main' into feat/zk-stf-proving
Sahilgill24 Aug 6, 2026
e0ae4aa
Merge remote-tracking branch 'upstream/main' into feat/zk-stf-proving
Sahilgill24 Sep 1, 2026
fe526df
feat: add SSZ byte fetching route from rpc for zkVM proving
Sahilgill24 Sep 7, 2026
dd212c2
feat: add replay crate for actual zkVM proving on devnet
Sahilgill24 Sep 7, 2026
b69da2f
Merge remote-tracking branch 'upstream/main' into feat/zk-stf-proving
Sahilgill24 Sep 25, 2026
2088181
Merge remote-tracking branch 'upstream/main' into feat/zk-stf-proving
Sahilgill24 Oct 2, 2026
c3e3bc2
feat: adding timed prove verification and execution
Sahilgill24 Oct 2, 2026
3f04809
feat: adding proper proof gen/exec/verify timing
Sahilgill24 Oct 2, 2026
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
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ members = [
"crates/net/api",
"crates/net/p2p",
"crates/net/rpc",
"crates/prover/core",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the SP1 prover

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the sp1 prover, has a separate toolchain and is excluded from regular builds exclude = ["crates/guest-program/sp1", "crates/prover/sp1"] because otherwise it would force other commands like cargo build to install the sp1 toolchain as well.

"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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -109,11 +121,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
Expand Down
8 changes: 6 additions & 2 deletions crates/blockchain/state_transition/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 24 additions & 0 deletions crates/blockchain/state_transition/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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)]
Expand Down
31 changes: 16 additions & 15 deletions crates/blockchain/state_transition/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::sync::LazyLock;

pub use ethlambda_metrics::TimingGuard;
use ethlambda_metrics::*;

static LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL: LazyLock<IntCounter> = LazyLock::new(|| {
Expand Down Expand Up @@ -30,21 +31,6 @@ static LEAN_FINALIZATIONS_TOTAL: LazyLock<IntCounterVec> = 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<Histogram> = LazyLock::new(|| {
register_histogram!(
"lean_state_transition_time_seconds",
Expand Down Expand Up @@ -84,6 +70,21 @@ static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock<Hist
.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();
}

/// Start timing state transition. Records duration when the guard is dropped.
pub fn time_state_transition() -> TimingGuard {
TimingGuard::new(&LEAN_STATE_TRANSITION_TIME_SECONDS)
Expand Down
23 changes: 23 additions & 0 deletions crates/blockchain/state_transition/src/metrics_noop.rs
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions crates/common/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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]);
Expand Down
49 changes: 49 additions & 0 deletions crates/common/types/src/stf.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
block: Vec<u8>,
}

/// 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")
}
}
15 changes: 15 additions & 0 deletions crates/guest-program/sp1/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
29 changes: 29 additions & 0 deletions crates/guest-program/sp1/src/main.rs
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 1 addition & 1 deletion crates/net/rpc/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub(crate) fn json_response<T: serde::Serialize>(value: T) -> axum::response::Re
response
}

fn ssz_response(bytes: Vec<u8>) -> axum::response::Response {
pub(crate) fn ssz_response(bytes: Vec<u8>) -> axum::response::Response {
let mut response = bytes.into_response();
response.headers_mut().insert(
header::CONTENT_TYPE,
Expand Down
46 changes: 45 additions & 1 deletion crates/net/rpc/src/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ use axum::{
};
use ethlambda_storage::Store;
use ethlambda_types::primitives::H256;
use libssz::SszEncode as _;
use serde_json::json;

use crate::json_response;
use crate::{json_response, ssz_response};

pub(crate) fn routes() -> Router<Store> {
Router::new()
.route("/lean/v0/blocks/{block_id}", get(get_block))
.route("/lean/v0/blocks/{block_id}/header", get(get_block_header))
.route("/lean/v0/blocks/{block_id}/ssz", get(get_block_ssz))
.route("/lean/v0/states/{block_id}", get(get_state_by_id))
}

/// `GET /lean/v0/blocks/:block_id` — returns the block as JSON.
Expand Down Expand Up @@ -53,6 +56,47 @@ pub(crate) async fn get_block_header(
}
}

/// `GET /lean/v0/blocks/:block_id/ssz` — returns the block as SSZ bytes.
pub(crate) async fn get_block_ssz(
Path(block_id): Path<String>,
State(store): State<Store>,
) -> impl IntoResponse {
let root = match resolve_block_id(&store, &block_id) {
Ok(root) => root,
Err(err) => return err.into_response(),
};

match store.get_block(&root) {
Ok(Some(block)) => ssz_response(block.to_ssz()),
Ok(None) => BlockIdError::NotFound.into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}

/// `GET /lean/v0/states/:block_id` — returns that block's post-state as SSZ.
///
/// The replay tool fetches a block's pre-state as the post-state of its parent
/// (`states/{parent_root}`). `state_root` is zeroed to the canonical post-state
/// form, matching `/states/finalized`.
pub(crate) async fn get_state_by_id(
Path(block_id): Path<String>,
State(store): State<Store>,
) -> impl IntoResponse {
let root = match resolve_block_id(&store, &block_id) {
Ok(root) => root,
Err(err) => return err.into_response(),
};

match store.get_state(&root) {
Ok(Some(mut state)) => {
state.latest_block_header.state_root = H256::ZERO;
ssz_response(state.to_ssz())
}
Ok(None) => BlockIdError::NotFound.into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}

/// Resolve a `block_id` (hex root or decimal slot) into a block root.
///
/// Slot lookups use the head state's `historical_block_hashes`, so only
Expand Down
2 changes: 1 addition & 1 deletion crates/net/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ mod node;
mod spec;
pub mod test_driver;

pub(crate) use base::json_response;
pub(crate) use base::{json_response, ssz_response};

#[derive(Debug, Clone)]
pub struct RpcConfig {
Expand Down
15 changes: 15 additions & 0 deletions crates/prover/core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[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
thiserror.workspace = true

Loading